前提:テスト対象のクラス
テストを行いたい対象のクラスは下記であるものとする。
(なお、パッケージの記述は省略)
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実行
モックだよ