Spring Bootでmockitoを使ってテストする方法

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;
  }
}
Using Mockito With JUnit 5 | Code With Arho
Learn how to use the Mockito mocking framework with JUnit 5. Learn both the test framework independent way, and using th...

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);
    }
  };
Test Smell: Everything is mocked
This Test Smell is about a weakness in the design of tests, rather than in the code to be tested. It may sound obvious, ...

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");

コメント

タイトルとURLをコピーしました