LoginSignup
2
1

【JUnit5】MockitoおよびMockBean,SpyBeanの使い方

Last updated at Posted at 2023-07-09

前提:テスト対象のクラス

テストを行いたい対象のクラスは下記であるものとする。
(なお、パッケージの記述は省略)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Autowired
SpringService1 springService1;

@RestController
public class SpringRestController {

    @PostMapping(value = "/bbb")
    public ResponseEntity<Void> bbb() throws Exception {
        List<Articles> resultList = new ArrayList<Articles>();

        try{
            resultList = springService1.getArticles("あ", true);
            resultList = springService1.getArticles2("あ", true);
        }catch(Exception e){

        }
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

今回は、下記のメソッドをモック化したいものとする。
springService1.getArticles("あ", true);
springService1.getArticles2("あ", true);

mockitoのアノテーションである@Mockを使ったテストコードの例

それではspringService1.getArticles()とspringService1.getArticles2()を最も初歩的な形でモック化してみる。

■ポイント
・モック化したいフィールドに@Mockをつける。

・テスト対象のインスタンスに@InjectMocksをつけることで、対象インスタンスのフィールドに@Mockされたインスタンスを差し込むことができる。
@InjectMocksをつけないと、Mockが動かず、verify()でエラーになる。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.times;import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;

import com.example.springTest.controller.SpringRestController;
import com.example.springTest.service.SpringService1;

@SpringBootTest
public class SpringRestControllerTest {

    // テスト対象クラス
    @InjectMocks
    SpringRestController springRestController;

    // mockitoのアノテーション
    @Mock
    SpringService1 springTestService;

    // テストケースにこのアノテーションを付与
    @Test
    @DisplayName("bbbのテスト")    
    void bbbのテスト(){
        try {
            // 実行
            var result = springRestController.bbb();

            // 戻り値チェック
            assertEquals(HttpStatus.OK, result.getStatusCode());

            // モックが機能したかを検証
            verify(springTestService,times(1)).getArticles("あ", true);
            verify(springTestService,times(1)).getArticles2("あ", true);

            System.out.println("bbbのテスト実行した");
        } catch (Exception e) {
            e.printStackTrace();            
            fail();
        }
    }
}

長ったらしいが、@Mock@InjectMocksがポイントのところである。
なお、モック化できるのはメンバ変数であり、ローカル変数はモック化できない。重要なことなのでもう一度言います。テスト対象クラス内で定義しているローカル変数はモック化できない。
例えば下記のmapはモック化できない。

    public void serviceMethod(){
        Map<String, String> map = new HashMap<String, String>();
        map.put("aaa.12", "111");
        if (map.containsKey("aaa.12")){
            System.out.println("aaa.12が含まれている");
        }
    }

when(mapMock.containsKey("aaa.12")).thenReturn(false);としても
verify(mapMock, times(1)).containsKey("aaa.12");
でコケる。

Springのアノテーションである@MockBeanを使ったテストコードの例

上述した@Mockは、Mockitoのアノテーションである。

@Mockを使う場合、テスト対象のインスタンス(この場合はspringRestController)には@Autowiredをつけてはならない。@Autowiredをつけると、SpringRestControllerクラス内で@AutowiredされているSpringService1の方が適用され、モックが機能しない。

テスト対象のインスタンスに@Autowiredをつけたままモック化したい場合は、@MockBeanアノテーションを使用する。これはSpringが提供するアノテーションであり、@Mockで作ったモジュールと異なり、アプリケーションコンテキスト(DI)に注入されるのが特徴である。

下記が@MockBeanを使ったテストケースの例である。

長ったらしいが、@InjectMocks@Autowiredに、@Mock@MockBeanにそれぞれ変更しただけの違いである。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;

import com.example.springTest.controller.SpringRestController;
import com.example.springTest.service.SpringService1;

@SpringBootTest
public class SpringRestControllerTest {

    // テスト対象クラス
    @Autowired
    SpringRestController springRestController;

    // Springのアノテーション
    @MockBean
    SpringService1 springTestService;

    // テストケースにこのアノテーションを付与
    @Test
    @DisplayName("bbbのテスト")    
    void bbbのテスト(){
        try {
            // モック作成(前置記法)
            // List<Articles> returnList = new ArrayList<Articles>();
            // returnList.add(new Articles());
            // doReturn(returnList).when(springTestService).getArticles("あ", true);
            
            // 実行
            var result = springRestController.bbb();

            // 戻り値チェック
            assertEquals(HttpStatus.OK, result.getStatusCode());

            // モックが機能したかを検証
            verify(springTestService,times(1)).getArticles("あ", true);
            verify(springTestService,times(1)).getArticles2("あ", true);

            System.out.println("bbbのテスト実行した");
        } catch (Exception e) {
            e.printStackTrace();            
            fail();
        }
    }
}

@Mock@MockBean、どちらを使うべきかはケースバイケースかもしれないが、後者の方がDIの対象になるため動作は遅くなるので、まずは@Mockを使う方がいいと思われる(異論があればコメントお願いします)。

指定の戻り値を返すようにしたい(前置記法)

上記ではspringTestService.getArticles()とspringTestService.getArticles2()を「do Nothing」みたく、何もしないメソッド化するモックの仕方であった。

しかし、モック化に際しては、何かの戻り値を返すように仕掛けたい場合が多い。

以下は、指定の戻り値を返すモックの例である。
とりあえず、2要素だけ持つリストを返却するように仕掛けてみる。

長ったらしいが、
doReturn(returnList).when(springTestService).getArticles("あ", true);
のところがポイントである。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;

import com.example.springTest.controller.SpringRestController;
import com.example.springTest.model.Articles;
import com.example.springTest.service.SpringService1;

@SpringBootTest
public class SpringRestControllerTest {

    // テスト対象クラス
    @InjectMocks
    SpringRestController springRestController;

    // mockitoのアノテーション
    @Mock
    SpringService1 springTestService;

    // テストケースにこのアノテーションを付与
    @Test
    @DisplayName("bbbのテスト")    
    void bbbのテスト(){
        try {
            // モック作成(前置記法)
            List<Articles> returnList = new ArrayList<Articles>();
            returnList.add(new Articles(){{
                setArticleTitle("JUnitテスト");
            }});
            returnList.add(new Articles(){{
                setArticleTitle("JUnitテスト2");
            }});

            doReturn(returnList).when(springTestService).getArticles("あ", true);
            
            // 実行
            var result = springRestController.bbb();

            // 戻り値チェック
            assertEquals(HttpStatus.OK, result.getStatusCode());

            // モックが機能したかを検証
            verify(springTestService,times(1)).getArticles("あ", true);
            verify(springTestService,times(1)).getArticles2("あ", true);

            System.out.println("bbbのテスト実行した");
        } catch (Exception e) {
            e.printStackTrace();            
            fail();
        }
    }
}

ArgumentMatchersを使って、汎用性の高いモックをつくってみる

上述した

doReturn(returnList).when(springTestService).getArticles("あ", true);

であるが、モック化したいメソッド(この場合はgetArticles)に記述する引数は、プロダクトコード(テスト対象のコード)で実際に入ることになる値と一字一句違わないものにしないと、モックが働かず、verify()でエラーになる。

テストコードを大量に書く上で少々不便とも言えるので、汎用性の高いモックをつくってみたい。この時有用なのがmockitoのArgumentMatchersだ。

doReturn(returnList).when(springTestService).getArticles("あ", true);

をこのように変えてみる。import文は追加する。

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;

doReturn(returnList).when(springTestService).getArticles(anyString(), eq(true));

第一引数であるが、String型でありさえすえば何でもOK、という意味になる。
気をつけたいのは第二引数のところで、

doReturn(returnList).when(springTestService).getArticles(anyString(), true);

とすると、テスト実行時にorg.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers!というエラーになる。

ひとつでもArgumentMatchersを使ったならば、全ての引数をArgumentMatchers形式に合わせる必要がある点に注意。

なお、このArgumentMatchersはモックをつくる場合だけでなくモック検証(verify)でも使用可能。

// モックが機能したかを検証
verify(springTestService,times(1)).getArticles(anyString(), eq(true));

@Spy,@SpyBean

@Mockは、インスタンスの非staticかつpublicのメソッドすべてをMock化する。
そのため一部のメソッドを実装のまま使いたい場合には適さない。

インスタンスの指定のメソッドのみをモック化し、他は実装の通りに動かしたい場合は、@Spyを使う。
これについて詳しくは別記事を参照。

@MockのDIあり版として@MockBeanがあるように、@SpyBeanというSpringのアノテーションもある。

@Spy@SpyBeanの典型的な使いどころとしては、テスト対象のメソッドと、モック化したいメソッドが同じクラスにある場合である。下の例では、method2のみモック化し、method1は実装のまま動くことになる。

例:

テスト対象クラス
import org.springframework.stereotype.Service;

@Service
public class SampleService {
    
    public String method1(){
        System.out.println("method1実行");
        String result = method2();
        System.out.println(result);
        return result;
    }

    public String method2(){
        return "モックではないよ";
    }
}
テストコード
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;

@SpringBootTest
public class SampleServiceTest {
    
    @SpyBean
    SampleService sampleService;

    @Test
    @DisplayName("method2をモック化してmethod1のテスト")
    public void method1のテスト(){
        doReturn("モックだよ").when(sampleService).method2();
        String result = sampleService0224.method1();
        assertEquals("モックだよ", result);
    }
}
実行結果
method1実行
モックだよ
2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1