【Java】Mockitoで依存性注入を使わずにユニットテストする


デバイスソフトウエア開発部の本間です。

最近、弊社業務にて Android アプリの保守を担当する機会がありました。そのアプリには自動テストが存在しなかったため、安全に改修を進めるための回帰テストとして、まずユニットテスト(JUnit)を導入しました。

しかしテストを書き始めると、クラス同士が直接依存していたり、I/O などの副作用が各所に埋め込まれており、既存コードに変更を加えなければユニットテストが難しいことが分かりました。そこで、モッキングフレームワークである Mockito を導入することにしました。これにより依存先を任意の処理(モック)へ差し替えることが可能となり、ユニットテストを実施することができました。

本記事では、その際に使った Mockito の機能を、実例を交えてご紹介します。

Logo by Karol Poźniak (CC BY 3.0)

テストと依存性注入について

Java では、依存性注入(Dependency Injection, DI)という設計手法がよく使われます。これは、上位のクラスが下位のクラスを直接生成・参照するのではなく、その間にインタフェースなどの抽象をはさみ、下位クラスを依存先として外から渡してもらう、というパターンの設計です。詳細な説明は本題から外れるため省きますが、DI には一般的に次のような利点があるかと思います。

  • 抽象を介して依存先(下位クラス)を外から渡せるため、クラスのロジックが特定の実装に固定されにくくなる

  • I/O(ファイルや通信)、時刻、乱数などの環境依存の処理を依存先として分離することで、クラスの関心をロジックに寄せやすくなる

この利点により、DI を使うとユニットテストも書きやすくなります。依存先を引数などで受け取る形にしておけば、テストでは、本物の代わりにモックを渡すだけで、狙ったテスト条件を再現できます。例えば、インスタンスの生成(new)、I/O や時刻など環境に左右される処理、static メソッド呼出し、シングルトン参照といった処理をクラス内部に埋め込まずに、依存先として外から渡せる形にしておけば、テストではそれらをモックに差し替えて振る舞いを制御できます。

一方で、今回のように既存コードが DI を前提に設計されていない場合、ユニットテストは難しくなりがちです。new やシングルトン参照がクラス内部に埋め込まれていると、テスト側からそれらの依存先の振る舞いを直接制御できず、狙ったテスト条件を再現しにくいためです。

そこで Mockito の出番です。Mockito には inline mock (mockito-inline / inline mock maker) という仕組みがあり、テスト側から差し替えにくいクラス内部に埋め込まれた依存でも、モックに差し替えることができます。つまり inline mock を使えば、クラス内部に埋め込まれた依存をテスト側から任意に制御できるため、既存コードに DI を導入しなくてもユニットテストが書きやすくなる、というわけです。
 
ちなみに Mockito は”モキート”と読みます(カクテルのモヒートのもじりらしいです)

検証環境

本記事の検証環境は下記の通りです。
Mockito 自体は、特定の開発環境やテストフレームワークに縛られず利用できます。

カテゴリ ツール名
開発環境 Android Studio Koala
テストフレームワーク JUnit 4.13.2
モッキングフレームワーク Mockito 5.2.0

モックを注入してテストする

まずは本題に入る前に、Mockito の基本機能のさわりとして、依存注入(DI)できるサンプルコードを対象にユニットテストを行います。Mockito でモックを作ってテスト対象に注入し、テストの実行結果を検証していきましょう。

なお、本記事で扱うのは Mockito の機能の一部に過ぎません。詳しい内容や最新の仕様は、公式WikiAPIリファレンス をご参照ください。

テスト対象のサンプルクラス

テスト対象のクラスは UserService です。UserService は、依存先として UserRepository の実装をコンストラクタで受け取る設計になっています。UserService 自体は「名前を検索し null の場合は "Unknown" を返す」というロジックを担当し、実際の名前検索処理は UserRepository の実装に委ねています。

今回のテストでは、UserRepository の実装クラスの代わりにモックを注入し、findName() の戻り値を制御することで、UserService のロジックだけを検証したいと思います。

テストコード(正常系)

では正常系のテストとして、存在するユーザーの名前取得を検証してみます。

上記のテストコードでは、UserRepository のモックを生成し、それをテスト対象である UserService に注入しています。モックには repo.findName("u1") が呼ばれたときに "Alice" を返すような振る舞いを設定し、service.getName("u1") の戻り値が "Alice" になることを検証します。これにより、DB やネットワークなどの環境に依存せず、UserService のロジックだけを安定してテストできます。

モックは Mockito.mock() で生成しています。モックしたい型を渡すと、各メソッドの振る舞いを任意に設定できるインスタンスが生成されます。振る舞いを設定していない呼び出しでは、何も行われず、戻り値がある場合はデフォルト値(null / 0 / false など)が返ります。各メソッドの振る舞いは、返り値があるメソッドでは基本的に Mockito.when() を使い、void メソッドでは Mockito.doAnswer() などを使って設定します。

Mockito.verify() を使うと、テスト対象の UserService が 依存先(モック)をどのように呼び出したかを検証できます。上のテストコードでは、テスト実行中に repo.findName("u1") が1回だけ呼ばれたことを verify() で検証しています。さらに verifyNoMoreInteractions(repo) を併用すると、これ以外の想定外の呼び出しが発生していないことも検出できます。

テストコード(異常系)

次に異常系のテストとして、存在しないユーザーの名前取得を検証してみます。

上記のテストコードでは、UserRepository のモックに repo.findName("u2") が呼ばれたとき null を返すような振る舞いを設定し、service.getName("u2") の戻り値が "Unknown" になることを検証しています。このように、依存先の振る舞いをモックでテスト側から制御することで、実際の名前検索処理に依存せず、名前を取得できない場合の分岐も検証することができます。

クラス内部の依存を差し替えてテストする

前置きが長くなりましたが、ここから本題です。Mockito の inline mock を使用して、シングルトン参照や new が埋め込まれたサンプルコードを対象にユニットテストを行います。DI を使用せずに inline mock で依存先の振る舞いを制御し、テストの実行結果を検証していきましょう。

テスト対象のサンプルクラス

SessionHelper がテスト対象のクラスです。isSessionActive() では、現在時刻を基準にセッションが有効期限内かどうかを判定しますが、シングルトンである SessionManager を直接参照し、現在時刻も new Date() で直接取得しています。

SessionManager は SessionHelper から参照されるシングルトンです。getSessionExpiresAtEpochMillis() はセッション有効期限をエポックミリ秒で取得する仕様ですが、現状は未実装として常に 0 を返すものとします。

このままでは、常にセッション期限切れとして isSessionActive() は false を返し、十分なパターンをテストできません。このように、クラス内部に依存先が直接埋め込まれていると、テスト側から依存先を制御できずテストが困難になることがあります。そこで、Mockito の inline mock を使いこれらの依存を制御することで、SessionHelper のロジックを検証していきたいと思います。

テストコード(static メソッドの inline mock)

まずセッションが有効となるパターンを検証してみましょう。そのためにはセッション有効期限を任意の値に設定できる必要があるため、static メソッドである SessionManager.getInstance() を inline mock し、シングルトンインスタンスをモックに差し替えます。

SessionManager の static メソッドは Mockito.mockStatic() で inline mock できます。MockedStatic を生成すると、それを close() するまでの間、SessionManager の static メソッド呼び出しはモックとして扱われるようになります。なお、MockedStaticAutoCloseable なので、try-with-resources 構文を使うとスコープでモックを管理することができます。

テストでは、まずシングルトンの代わりとする SessionManager のモックを生成し、モックにはgetSessionExpiresAtEpochMillis() が「現在時刻 + 1 秒」を返すように振る舞いを設定します。次に MockedStatic.when()SessionManager.getInstance() が先ほどのモックを返すように設定します。これにより、 SessionManager の振る舞いをテスト側から制御できるようになり、セッション有効として isSessionActive() が true を返すパターンも検証できるようになりました。

なお、static メソッドも呼び出し検証できますが、通常のモックとは異なり MockedStatic.verify() を使って呼び出し検証します。

テストコード(コンストラクタの inline mock)

境界テストとして、現在時刻とセッション有効期限が完全に一致するパターンを検証します。そのためには現在時刻を任意に制御できる必要があるため、new Date() が生成するインスタンスをモックに差し替えます。

Mockito.mockConstruction() を使うことで、テスト中に new で呼び出される Date のコンストラクタが inline mock され、new Date() は実インスタンスではなくモックを返すようになります。MockedStatic と同様に、この挙動は MockedConstruction を生成してから close() するまでだけ有効です。

new Date() が返すモックの振る舞いは mockConstruction() のコールバック内で設定します。コールバックには new の結果として扱われる Date モックが渡されるので、ここで getTime() が常にテスト開始時刻を返すように設定します。これにより、 SessionHelper.isSessionActive() が参照する「現在時刻」をテスト開始時刻に固定でき、現在時刻とセッション有効期限が一致するパターンを検証できるようになりました。

まとめ

本記事でご紹介した Mockito の主要機能をまとめると、下記の通りです。

機能 API 使用例
モックの作成 Mockito.mock() UserRepository repo = Mockito.mock(UserRepository.class);
モックの振舞い定義 Mockito.when()
Mockito.doAnswer()
他にも多数あり
Mockito.when(repo.findName("u1")).thenReturn("Alice");
モックの呼出し検証 Mockito.verify() Mockito.verify(repo, Mockito.times(1)).findName("u1");
static のモック Mockito.mockStatic() MockedStatic<SessionManager> mockedManager = Mockito.mockStatic(SessionManager.class);
static の振舞い定義 MockedStatic.when() mockedManager.when(SessionManager::getInstance)
.thenReturn(managerMock);
static の呼出し検証 MockedStatic.verify() mockedManager.verify(SessionManager::getInstance, Mockito.times(1));
コンストラクタのモック Mockito
.mockConstruction()
MockedConstruction<Date> mockedDate = Mockito.mockConstruction(Date.class, ...);

なお、ここで扱ったのは Mockito の機能の一部に過ぎません。たとえば、アノテーション(@Mock / @InjectMocks など)によるモック生成や、Mockito.spy() による既存オブジェクトのスパイなど、他にも便利な機能が多数あります。

詳しい内容や最新の仕様は、公式WikiAPIリファレンス をご参照ください。

おわりに

今回、実例を交えて Mockito の機能の一部をご紹介しました。

Mockito を活用すれば簡単にモックを作成でき、さらに inline mock を使えば static メソッドや new の呼び出し箇所を差し替えられるため、依存が埋め込まれたクラスでもユニットテストしやすくなります。とはいえ、inline mock でも解決できないケースはあり、さらにクラス内部の実装に依存したテストになって結合度が上がりやすいため、基本は依存性注入を活用するなど、ユニットテストしやすいクラス設計を心がけていきましょう。

エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方は、ぜひ採用ページをご覧ください。