Spring Bootで簡易テストを行う
ぶっちゃけ
簡単じゃない
結構めんどくさい
やりたいこと
RestControllerをテストする
Serviceなどを使用している場合はモック化する
実装
Sample01Controller
package com.example.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.example.dto.sample01.Sample01ReqDto; import com.example.dto.sample01.Sample01ResDto; import com.example.service.Check1Service; @RestController public class Sample01Controller { private static final Logger logger = LoggerFactory.getLogger(Sample01Controller.class); @Autowired private Check1Service check1Service; @ResponseBody @RequestMapping(value = "/sample01/test1", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE) public Sample01ResDto test1(@RequestBody Sample01ReqDto req) { Sample01ResDto res = new Sample01ResDto(); if (check(req)) { res.message = "ok"; res.status = 100L; } else { res.message = "ng"; res.status = 900L; } check1Service.sample1(); return res; } private boolean check(Sample01ReqDto req) { return check1Service.check(req.name); } }
Check1Service
package com.example.service; import org.springframework.stereotype.Service; import com.example.dto.sample01.Sample01ReqDto; @Service public class Check1Service { public boolean check(String name) { boolean res = false; if (name != null) { res = true; } return res; } public void sample1() { String str = "1111"; str += "222"; } }
Sample01ReqDto.java
package com.example.dto.sample01; import java.io.Serializable; public class Sample01ReqDto implements Serializable { /** * */ private static final long serialVersionUID = 1L; public String name; public Long age; }
Sample01ResDto.java
package com.example.dto.sample01; import java.io.Serializable; public class Sample01ResDto implements Serializable { /** * */ private static final long serialVersionUID = 1L; public String result; public String message; public Long status; }
↓
実行
curl -X POST -H "Content-type: application/json" -d '{"name": null, "age": 100 }' http://localhost:8080/sample01/test1 {"result":null,"message":"ng","status":900}
テストコード
Sample01ControllerTest.java
package com.example.controller; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import org.hamcrest.Matchers; import org.hamcrest.core.IsNull; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.example.dto.sample01.Sample01ReqDto; import com.example.service.Check1Service; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest @WebAppConfiguration public class Sample01ControllerTest { @Rule public MockitoRule mockitoJUnitRule = MockitoJUnit.rule(); private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); private MockMvc mockMvc; private HttpMessageConverter mappingJackson2HttpMessageConverter; @InjectMocks private Sample01Controller target; @Autowired private WebApplicationContext webApplicationContext; @Autowired void setConverters(HttpMessageConverter<?>[] converters) { this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream() .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) .findAny() .orElse(null); assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter); } @Mock private Check1Service check1Service; @Before public void setup() throws Exception { this.mockMvc = MockMvcBuilders.standaloneSetup(target).build(); } /** * /sample01/test1 * @throws Exception */ @Test public void sample01Test1Post1() throws Exception { // リクエストパラメータ Sample01ReqDto req = new Sample01ReqDto(); req.name = "aaaa"; req.age = 100L; String jsonSample01ReqDto = json(req); // 外部クラスをmock化 check1Service_check(); // 実行 this.mockMvc.perform(post("/sample01/test1") .contentType(contentType) .content(jsonSample01ReqDto)) // ステータスコード .andExpect(status().isOk()) // json項目のNULLチェック .andExpect(jsonPath("result").value(IsNull.nullValue())) // json項目の文字列チェック .andExpect(jsonPath("message", is("ok"))) // json項目のLong型チェック .andExpect(jsonPath("status").value(100L)) ; } /** * /sample01/test1 * @throws Exception */ @Test public void sample01Test1Post2() throws Exception { // リクエストパラメータ Sample01ReqDto req = new Sample01ReqDto(); req.name = null; req.age = 200L; String jsonSample01ReqDto = json(req); // 外部クラスをmock化 check1Service_check(); // 実行 this.mockMvc.perform(post("/sample01/test1") .contentType(contentType) .content(jsonSample01ReqDto)) // ステータスコード .andExpect(status().isOk()) // json項目のNULLチェック .andExpect(jsonPath("result").value(IsNull.nullValue())) // json項目の文字列チェック .andExpect(jsonPath("message", is("ng"))) // json項目のLong型チェック .andExpect(jsonPath("status").value(900L)) ; } /** * /sample01/test1 * @throws Exception */ @Test public void sample01Test1Get() throws Exception { // 実行 this.mockMvc.perform(get("/sample01/test1") .contentType(contentType)) // ステータスコード .andExpect(status().isMethodNotAllowed()) ; } // mock化 private void check1Service_check() { // 値を設定した場合はtrue when(check1Service.check(Mockito.anyString())).thenReturn(true); // 値を未設定の場合はfalse when(check1Service.check(null)).thenReturn(false); // 戻り値がないスタブ doNothing().when(check1Service).sample1(); } protected String json(Object o) throws IOException { MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); this.mappingJackson2HttpMessageConverter.write( o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); return mockHttpOutputMessage.getBodyAsString(); } }
やっていること
@Before
@Before public void setup() throws Exception { this.mockMvc = MockMvcBuilders.standaloneSetup(target).build(); }
全てのテストのメソッド前に実行されるモックオブジェクト?を作るはず
@InjectMocksしたコントローラークラスを設定することでスタブを利用できるような感じかと…
@Mock
@Mock private Check1Service check1Service;
モック化したい処理をこういうふうに定義しておく
whenで実行してモックの実行結果を取得する場合に必要
@Test
@Test public void sample01Test1Post1() throws Exception { // リクエストパラメータ Sample01ReqDto req = new Sample01ReqDto(); req.name = "aaaa"; req.age = 100L; String jsonSample01ReqDto = json(req); // 外部クラスをmock化 check1Service_check(); // 実行 this.mockMvc.perform(post("/sample01/test1") .contentType(contentType) .content(jsonSample01ReqDto)) // ステータスコード .andExpect(status().isOk()) // json項目のNULLチェック .andExpect(jsonPath("result").value(IsNull.nullValue())) // json項目の文字列チェック .andExpect(jsonPath("message", is("ok"))) // json項目のLong型チェック .andExpect(jsonPath("status").value(100L)) ; }
各テスト処理を実施する。@Testアノテーションを付与したメソッドを全て実行する。
when
// mock化 private void check1Service_check() { // 値を設定した場合はtrue when(check1Service.check(Mockito.anyString())).thenReturn(true); // 値を未設定の場合はfalse when(check1Service.check(null)).thenReturn(false); // 戻り値がないスタブ doNothing().when(check1Service).sample1(); }
※スタブに引数と戻り値を設定する。引数を固定値を指定した場合はその値の結果しか返さない
↓
所感
テスト自体はしなくてはいけないとおもいつつも動作確認することで対応すればよいよね…
みたいな感じになるからついやらなくなってしまいます。
今回はきちんとやってみることにする画面関連のテストは無理のような感じがするけどRest APIのようにデータの流れを理解することで対応する。
まあ、あとは実装時にモック化しやすいように考慮しておく