- Spring Bootでmockitoを使ってテストする方法
- 依存関係にあるクラスをモックする
- サービスクラスのテストでリポジトリインタフェースをモックする
- when().thenThrow()で注意
- @RunWith(MockitoJUnitRunner.class)アノテーションでモックする ※JUnit5では存在しないアノテーション
- doNothing~when,doReturn~when,doThrow~when
- mockitoではプライベートメソッドをモックすることができない
- org.mockito.mockとorg.mockito.spyの違い
- mockitoでプライベートメソッドをリフレクションでテストする
- メソッドが呼ばれた回数を確認する
- Please remove unnecessary stubbings or use ‘lenient’ strictness. More info: javadoc for UnnecessaryStubbingException class.
- JUnit5 + mockito3.x
- HttpServletRequestをモックする
- S3ObjectクラスのgetObjectContentメソッドをモックする
- Value Objectをモックする
- LocalDateTime.now()をモックする
- 抽象クラスのメソッドのテスト
- 抽象クラスのフィールド
- APIのリクエストボディ
- メソッド呼び出し回数によって異なる結果を返す
- 関連
Spring Bootでmockitoを使ってテストする方法
Spring Bootのgradle.buildで以下があればJUnit,mockito,assertJが使えます。
testImplementation 'org.springframework.boot:spring-boot-starter-test'
「Spring BootのRESTControllerをJUnit4でテストする」にも書きましたが、サービスクラスで使用しているリポジトリクラスをモックするテスト方法を書いてみます。
依存関係にあるクラスをモックする
依存関係にあるクラスのみモックすることが出来ます。例えば、「コントローラ → サービス → リポジトリ」というような構成の場合、コントローラのテストでリポジトリをモックすることは出来ません。コントローラでモック出来るのはサービスクラスとなります。サービスクラスでモック出来るのはリポジトリインタフェースとなります。
以下のようなイメージです。
class TestControllerTest() { @InjectMocks private TestController testController; // テスト対象 @Mock private TestService testService; // 依存するクラス @Mock private CommonService commonService; // 依存するクラス @Mock private TestManager testManager; // 依存するクラス ~
サービスクラスのテストでリポジトリインタフェースをモックする
サービスクラスのメソッド内でリポジトリクラス経由でDBにアクセスしています。
DemoServiceTest.java
package jp.co.confrage; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; import java.util.List; import org.junit.BeforeEach; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; public class DemoServiceTest { @Mock // テスト対象で使用するクラスに対してつけるアノテーション private EmployeeRepository repository; @InjectMocks // テスト対象のクラスに対してつけるアノテーション private DemoService service; @BeforeEach public void setup() { MockitoAnnotations.initMocks(this); } @Test public void サンプルテスト() { // findAllが実行されたらRuntimeExceptionが発生する when(repository.findAll()).thenThrow(new RuntimeException()); List<Employee> list = service.selectEmployee();// テスト対象のメソッドを実行 assertThat(list.size()).isEqualTo(3); // assertJを使用 } }
サービスクラスがテスト対象なので@InjectMocksをつけます。
サービスクラスのメソッドで使用するリポジトリ(依存関係にある)に対して@Mockをつけます。@Mockをつけるのはサービスクラスでモックしたいクラスのみです。
@BeforeでMockitoAnnotations.initMocks(this);
をしておかないと正常にモックが動作しません。※非推奨になってます、mockito3ではMockitoAnnotations.openMocks();
を使用
テストメソッドで、whenの引数にメソッドを指定します。thenThrow(new RuntimeException());でランタイムエクセプションをスローさせます。これでcatch句に遷移させることができます。スローさせずに値を返したい場合はthenReturnメソッドを使用します。
以下はサービスクラスです。
package jp.co.confrage; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Service public class DemoService { @Autowired EmployeeRepository empRepository; @RequestMapping(value = "/method", method = RequestMethod.GET) public List<Employee> selectEmployee() { List<Employee> emplist = new ArrayList<Employee>(); try { emplist = empRepository.findAll(); } catch(Exception e) { // ここでエラーステータスをつけてJSONで返したり } return emplist; } }
JUnit5+Mockitoを使用する場合は、この指定は不要です。
when().thenThrow()で注意
findAll()のような引数のないメソッドはthenThrow()されますが、自作のメソッドで引数が存在する場合はany()などを使用する必要があります。
例えば
when(repository.findBySpec('xx')).thenThrow(new RuntimeException());
というようにすると正常に動作しません。この場合、String型の引数なので以下のように記述します。
when(repository.findBySpec(anyString()).thenThrow(new RuntimeException());
というようにanyString()を使用します。(anyStringはnullを含みません)
引数がオブジェクトの場合はany()を使用します。
import static org.mockito.ArgumentMatchers.*;
上記をstaticインポートすればOKです。
@RunWith(MockitoJUnitRunner.class)アノテーションでモックする ※JUnit5では存在しないアノテーション
JUnit4までは、@RunWith(MockitoJUnitRunner.class)をクラスにつけることによってモックすることも可能です。
@Beforeの部分は不要になります。
@RunWith(MockitoJUnitRunner.class) public class DemoServiceTest {
when~thenThrowが使えない場合がある
when~thenThrowでスローさせようとしていたのですが、どうも戻り値がvoidのメソッドに対してはwhen~thenThrowが使えないようです。「型 Mockito のメソッド when(T) は引数 (void) に適用できません」とエラーになると思います。
代わりにdoThrow~when、doReturn~when、doAnswer~when、doNothing~whenを使います。
JUnit5では@RunWithの代わりに@ExtendWithを使用します。
doNothing~when,doReturn~when,doThrow~when
doNothing,doReturn,doThrowの使い方は以下の通りです。
メソッド | ユースケース |
---|---|
doNothing | 戻り値voidのメソッドで使用 |
doReturn | 戻り値void以外のメソッドで使用 |
doThrow | 例外発生させるテストで使用 |
doNothing().when(repository).deleteByPk(); // 引数なしのケース doReturn(Integer.valueOf(1)).when(repository).deleteByPk(Mockito.any()); // 引数1つのケース doThrow(new RuntimeException()).when(repository).deleteByPk(Mockito.any(), Mockito.any()); // 引数2つのケース
whenの引数はインスタンスでそのメソッドをwhenの中ではなく、外でチェーンします。
これで戻り値がvoid型のメソッドもスローが出来るようになります。when~thenThrowより全てのテストケースでdoThrow~whenを使った方が良い気がします。
assertはassertThatExceptionOfTypeを使用します。
assertThatExceptionOfType(XXException.class).isThrownBy(() -> xxUploader.upload(path, file);
これでuploadメソッドが実行したときにXXExceptionが発生すればテストOKとなります。
mockitoではプライベートメソッドをモックすることができない
JMockitoならプライベートメソッドをモックすることができますが、mockitoではプライベートメソッドをモックすることができません。
mockitoのポリシーでprivateメソッドを使用する場合はコード設計がイケてない、という考えのようです。
org.mockito.mockとorg.mockito.spyの違い
org.mockito.Mockito()メソッドを使えばクラスのインスタンスをモックすることが出来ます。
PrivateKeyのモックです。RSAPrivateKey.classはPrivateKeyインタフェースをインプリメントした具象クラスです。
PrivateKey pk = Mockito.mock(RSAPrivateKey.class);
上記でモックすることが出来ますがmockで生成したインスタンスは偽物です。
これに対してspyで生成したインスタンスはメソッドは実際に動作します。
@Test void test() { List<String> spy = Mockito.spy(new LinkedList<>()); doNothing().when(spy).add(0, "a"); // aをaddしたときだけ無効にする spy.add(0, "a"); // 無視される spy.add(0, "b"); System.out.println(spy.size()); // 1 spy.stream().forEach(System.out::println); // b }
mockitoでプライベートメソッドをリフレクションでテストする
mockitoではプライベートメソッドをモックすることができませんが、プライベートメソッドのテストをすることは可能です。
以下のようにgetDeclaredMethodを使用します。
public void プライベートメソッドのTEST() { Method method = XXCalculator.class.getDeclaredMethod("calc", BigDecimal.class, BigDecimal.class); method.setAccessible(true); String actual = (String)method.invoke(new XXCalculator(), BigDecimal.ONE, BigDecimal.ONE); assertThat(actual).isNull(); }
setAccessible(true)にしておきます。これでプライベートメソッド(上記ではcalcメソッド)のテストができます。
mockitoでコーディングする際、以下はstatic importしておくとよいです。
org.mockito.ArgumentMatchers org.mockito.Mockito
プライベートメソッドで例外テストをする場合、本来発生する例外がInvocationTargetExceptionでラップされて例外発生しますので以下コードのようにcatchする必要があります。※ResponseStatusExceptionが発生するとします
public void プライベートメソッドのTEST() { Method method = XXCalculator.class.getDeclaredMethod("calc", BigDecimal.class, BigDecimal.class); method.setAccessible(true); try { method.invoke(new XXCalculator(), BigDecimal.ONE, BigDecimal.ONE); } catch (InvocationTargetException e) { assertInstanceOf(ResponseStatusException.class, e.getTargetException()); } }
メソッドが呼ばれた回数を確認する
verifyでモックしたメソッドが呼ばれた回数を確認するテストをすることができます。
戻り値がないメソッドはverifyでテストすればよいと思います。以下のように書きます。
verify(モックインスタンス, times(2)).findByPk();
上記は、findByPk()メソッドが2回呼ばれていることを確認します。
サンプルテストコードです。
@RunWith(MockitoJUnitRunner.class) public class DemoServiceTest { @Mock // テスト対象で使用するクラスに対してつけるアノテーション private EmployeeRepository repository; @InjectMocks // テスト対象のクラスに対してつけるアノテーション private DemoService service; @Test public void サンプルテスト() { List<Integer> list = new ArrayList<>(); doReturn(list).when(repository).findByPk(); service.selectEmployee();// テスト対象のメソッドを実行 verify(repository, times(1)).findByPk(); // 1回呼ばれたことを確認する } }
Please remove unnecessary stubbings or use ‘lenient’ strictness. More info: javadoc for UnnecessaryStubbingException class.
このエラーが出たら、使ってもいないインスタンスをモックしているというエラーです。
簡単に解決するには、@RunWith(Mockito.JUnitRunner.class
を以下のように変更します。
@RunWith(MockitoJUnitRunner.Silent.class)
これでエラーがでなくなります。
JUnit5 + mockito3.x
build.gradleで依存関係を追加します。
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' testImplementation 'org.mockito:mockito-core:3.5.10' testImplementation 'org.mockito:mockito-junit-jupiter:3.5.10'
@ExtendWith(MockitoExtension.class)をクラスレベルのアノテーションとして付与します。これを指定しないとモックできません。
JUnit5で「Please remove unnecessary stubbings or use ‘lenient’ strictness. More info: javadoc for UnnecessaryStubbingException class.」エラーが発生した場合は、クラスレベルのアノテーションに@MockitoSettings(strictness = Strictness.LENIENT)
を付与すれば回避することが出来ます。(JUnit4では試していません)
mockito3.5.10で確認しましたが、MockitoAnnotations.initMocksメソッドは非推奨になっているようです。
@TestInstance(Lifecycle.PER_METHOD)をクラスレベルのアノテーションで付与します。
省略した場合、デフォルトはLifecycle.PER_METHODです。
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // @Orderアノテーションででテスト順序を指定したい場合 @ExtendWith(MockitoExtension.class) // JUnit+Mockitoの場合 @MockitoSettings(strictness = Strictness.LENIENT) @TestInstance(Lifecycle.PER_METHOD) // デフォルトはPER_METHOD class SampleTest { @InjectMocks private DemoService service; @Mock private DemoRepository Repository; @BeforeAll static void initAll() {} @BeforeEach void init() {} @Test @Order(1) // 1番目に実行する @DisplayName("更新APIサービスのupdateメソッドテスト") void updateTest() { // ... } @Test @Order(2) // 2番目に実行する @DisplayName("取得APIサービスのselectメソッドテスト") void updateTest() { // ... }
HttpServletRequestをモックする
spring-test-x.x.x.RELEASE.jarにMockHttpServletRequestクラスが用意されているので、このクラスでモックします。
MockHttpServletRequest req = new MockHttpServletRequest();
S3ObjectクラスのgetObjectContentメソッドをモックする
S3のgetObjectメソッドをモックするにはS3ObjectクラスgetObjectContentメソッドをモックする必要があります。
文字列をbyte配列に変換します。
@InjectMocks service; @Mock AmazonS3 s3; @Test @DisplayName("APIテスト") public void api_Test() throws UnsupportedEncodingException { byte[] data = "{ \"accessToken\": \"hoge\", \"key\":\"value\"}".getBytes("UTF-8"); final S3ObjectInputStream stream = new S3ObjectInputStream(new ByteArrayInputStream(data), new HttpGet()); // org.apache.http.client.methods.HttpGet final S3Object s3Object = new S3Object(); s3Object.setObjectContent(stream); doReturn(s3Object) .when(s3) .getObject(Mockito.anyString(), Mockito.anyString()); // org.mockito.Mockito // test service.xxx(); // assert }
S3ObjectInputStreamのインスタンスをsetObjectContentでセットしてモックすることができます。
getObjectメソッドの引数がGetObjectRequestインスタンスの場合は引数は一つに変更します。
doReturn(s3Object) .when(s3) .getObject(Mockito.anyString(), Mockito.anyString()); ↓ doReturn(s3) .when(s3) .getObject(Mockito.any());
Value Objectをモックする
getterがあってsetterがないVOをモックします。mockitoでモックする方法もありますが以下のようにgetterをoverrideして簡単にモックすることが出来ます。
Employee emp = new Employee() { @Override public Long getId() { return Long.valueOf(1); } };
LocalDateTime.now()をモックする
mockitoとPowermockを併用すればモックすることができるようです。※未検証
抽象クラスのメソッドのテスト
抽象クラスは@InjectMockアノテーションを付与できません。
AbstractServiceのメソッドをテストする場合はMockito.mock(AbstractService.class, Mockito.CALLS_REAL_METHODS);
として各メソッドのテストをすることができます。
@Test void AbstractServiceMethodTest() { AbstractService abstractservice = Mockito.mock(AbstractService.class, Mockito.CALLS_REAL_METHODS); String result = abstractservice.method("123"); assertThat(result).isEqualTo("1-2-3"); }
抽象クラスのフィールド
抽象クラスのフィールドは.getClass().getSuperclass().getDeclaredFieldメソッドでインジェクションする必要があります。
Field field = abstractclass.getClass().getSuperclass().getDeclaredField("baseHeaders"); field.setAccessible(true); // アクセス権を与える field.set(abstractclass, new HttpHeaders()); // フィールドセットする
APIのリクエストボディ
APIのテストでリクエストボディをMap<String, Object>型で作成します。
Map<String, Object> body = (new ObjectMapper()) // com.fasterxml.jackson.databind.ObjectMapper .readValue( "{\"token\": \"xxx\", \"records\":{ \"fuga\": [\"0101\",\"0102\"]}}" .getBytes("UTF-8"), new TypeReference<Map<String, Object>>() {});
メソッド呼び出し回数によって異なる結果を返す
when~thenReturnメソッドを使用して、呼び出し回数によって異なる結果を返すことができます。1回目の戻り値はBigDecimal.ZERO、2回目の戻り値はBigDecimal.ONEとなります。
when(インスタンス.メソッド(Mockito.any())) .thenReturn(BigDecimal.ZERO, BigDecimal.ONE);
1回目にスローさせて2回目に戻り値を返したい場合はthenThrow().thenReturn()というようにメソッドチェーンをします。
when(インスタンス.メソッド(Mokito.any())) .thenThrow(new RuntimeException()) .thenReturn("test");
KHI入社して退社。今はCONFRAGEで正社員です。関西で140-170/80~120万から受け付けております^^
得意技はJS(ES6),Java,AWSの大体のリソースです
コメントはやさしくお願いいたします^^
座右の銘は、「狭き門より入れ」「願わくは、我に七難八苦を与えたまえ」です^^
コメント