デバイスソフトウエア開発部の本間です。
最近、弊社業務にて 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 やシングルトン参照がクラス内部に埋め込まれていると、テスト側からそれらの依存先の振る舞いを直接制御できず、狙ったテスト条件を再現しにくいためです。
モックを注入してテストする
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Test public void getName_userExists_returnsName() { // UserRepository のモックを生成 // findName("u1") が呼ばれた時に "Alice" を返すように振る舞う UserRepository repo = Mockito.mock(UserRepository.class); Mockito.when(repo.findName("u1")).thenReturn("Alice"); // モックを DI UserService service = new UserService(repo); // テスト実行 assertEquals("Alice", service.getName("u1")); // 呼び出し検証 Mockito.verify(repo, Mockito.times(1)).findName("u1"); Mockito.verifyNoMoreInteractions(repo); } |
では正常系のテストとして、存在するユーザーの名前取得を検証してみます。
上記のテストコードでは、UserRepository のモックを生成し、それをテスト対象である UserService に注入しています。モックには repo.findName("u1") が呼ばれたときに "Alice" を返すような振る舞いを設定し、service.getName("u1") の戻り値が "Alice" になることを検証します。これにより、DB やネットワークなどの環境に依存せず、UserService のロジックだけを安定してテストできます。
モックは Mockito.mock() で生成しています。モックしたい型を渡すと、各メソッドの振る舞いを任意に設定できるインスタンスが生成されます。振る舞いを設定していない呼び出しでは、何も行われず、戻り値がある場合はデフォルト値(null / 0 / false など)が返ります。各メソッドの振る舞いは、返り値があるメソッドでは基本的に Mockito.when() を使い、void メソッドでは Mockito.doAnswer() などを使って設定します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Test public void getName_userDoesNotExist_returnsUnknown() { // UserRepository のモックを生成 // findName("u2") が呼ばれた時に null を返すように振る舞う UserRepository repo = Mockito.mock(UserRepository.class); Mockito.when(repo.findName("u2")).thenReturn(null); // モックを DI UserService service = new UserService(repo); // テスト実行 assertEquals("Unknown", service.getName("u2")); // 呼び出し検証 Mockito.verify(repo, Mockito.times(1)).findName("u2"); Mockito.verifyNoMoreInteractions(repo); } |
次に異常系のテストとして、存在しないユーザーの名前取得を検証してみます。
上記のテストコードでは、UserRepository のモックに repo.findName("u2") が呼ばれたとき null を返すような振る舞いを設定し、service.getName("u2") の戻り値が "Unknown" になることを検証しています。このように、依存先の振る舞いをモックでテスト側から制御することで、実際の名前検索処理に依存せず、名前を取得できない場合の分岐も検証することができます。
クラス内部の依存を差し替えてテストする
前置きが長くなりましたが、ここから本題です。Mockito の inline mock を使用して、シングルトン参照や new が埋め込まれたサンプルコードを対象にユニットテストを行います。DI を使用せずに inline mock で依存先の振る舞いを制御し、テストの実行結果を検証していきましょう。
テスト対象のサンプルクラス
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import java.util.Date; public class SessionHelper { private SessionHelper() { } public static boolean isSessionActive() { // セッション有効期限を取得 long expiresAtEpochMillis = SessionManager.getInstance().getSessionExpiresAtEpochMillis(); // 現在時刻を取得 Date now = new Date(); long nowEpochMillis = now.getTime(); // セッションが有効期限内かどうかを判定 return nowEpochMillis < expiresAtEpochMillis; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class SessionManager { private static final SessionManager INSTANCE = new SessionManager(); private SessionManager() { } public static SessionManager getInstance() { return INSTANCE; } public long getSessionExpiresAtEpochMillis() { // TODO: セッション有効期限を返す想定だが現状は未実装 return 0; } } |
SessionHelper がテスト対象のクラスです。isSessionActive() では、現在時刻を基準にセッションが有効期限内かどうかを判定しますが、シングルトンである SessionManager を直接参照し、現在時刻も new Date() で直接取得しています。
SessionManager は SessionHelper から参照されるシングルトンです。getSessionExpiresAtEpochMillis() はセッション有効期限をエポックミリ秒で取得する仕様ですが、現状は未実装として常に 0 を返すものとします。
このままでは、常にセッション期限切れとして isSessionActive() は false を返し、十分なパターンをテストできません。このように、クラス内部に依存先が直接埋め込まれていると、テスト側から依存先を制御できずテストが困難になることがあります。そこで、Mockito の inline mock を使いこれらの依存を制御することで、SessionHelper のロジックを検証していきたいと思います。
テストコード(static メソッドの inline mock)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Test public void isSessionActive_sessionNotExpired_returnsTrue() { // SessionManager の static メソッドを inline mock する try (MockedStatic // SessionManager のモックを生成 // セッション有効期限は現在時刻より1秒後とする SessionManager managerMock = Mockito.mock(SessionManager.class); Mockito.when(managerMock.getSessionExpiresAtEpochMillis()).thenReturn(System.currentTimeMillis() + 1000); // SessionManager.getInstance() を inline mock することで、 // シングルトンインスタンスを先ほど生成したモックに差し替える mockedManager.when(SessionManager::getInstance).thenReturn(managerMock); // テスト実行 assertTrue(SessionHelper.isSessionActive()); // 呼び出し検証 mockedManager.verify(SessionManager::getInstance, Mockito.times(1)); Mockito.verify(managerMock, Mockito.times(1)).getSessionExpiresAtEpochMillis(); } } |
テストでは、まずシングルトンの代わりとする SessionManager のモックを生成し、モックにはgetSessionExpiresAtEpochMillis() が「現在時刻 + 1 秒」を返すように振る舞いを設定します。次に MockedStatic.when() で SessionManager.getInstance() が先ほどのモックを返すように設定します。これにより、 SessionManager の振る舞いをテスト側から制御できるようになり、セッション有効として isSessionActive() が true を返すパターンも検証できるようになりました。
なお、static メソッドも呼び出し検証できますが、通常のモックとは異なり MockedStatic.verify() を使って呼び出し検証します。
テストコード(コンストラクタの inline mock)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Test public void isSessionActive_sessionExpired_returnsFalse() { // テスト開始時の時刻を取得 long currentEpochMillis = System.currentTimeMillis(); // SessionManager の static メソッドと、Date クラスのコンストラクタを inline mock する try (MockedStatic MockedConstruction // Date.getTime() が常にテスト開始時の時刻を返すようにする Mockito.when(mock.getTime()).thenReturn(currentEpochMillis); })) { // SessionManager のモックを生成 // セッション有効期限はテスト開始時の時刻で固定する SessionManager managerMock = Mockito.mock(SessionManager.class); Mockito.when(managerMock.getSessionExpiresAtEpochMillis()).thenReturn(currentEpochMillis); // SessionManager.getInstance() を inline mock することで、 // シングルトンインスタンスを先ほど生成したモックに差し替える mockedManager.when(SessionManager::getInstance).thenReturn(managerMock); // テスト実行 assertFalse(SessionHelper.isSessionActive()); // 呼び出し検証 mockedManager.verify(SessionManager::getInstance, Mockito.times(1)); Mockito.verify(managerMock, Mockito.times(1)).getSessionExpiresAtEpochMillis(); } } |
本記事でご紹介した 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) |
| static の呼出し検証 | MockedStatic.verify() |
mockedManager.verify(SessionManager::getInstance, Mockito.times(1)); |
| コンストラクタのモック | Mockito |
MockedConstruction<Date> mockedDate = Mockito.mockConstruction(Date.class, ...); |
なお、ここで扱ったのは Mockito の機能の一部に過ぎません。たとえば、アノテーション(@Mock / @InjectMocks など)によるモック生成や、Mockito.spy() による既存オブジェクトのスパイなど、他にも便利な機能が多数あります。
詳しい内容や最新の仕様は、公式Wiki や APIリファレンス をご参照ください。





