m_shige1979のときどきITブログ

プログラムの勉強をしながら学習したことや経験したことをぼそぼそと書いていきます

Github(変なおっさんの顔でるので気をつけてね)

https://github.com/mshige1979

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();
    }

※スタブに引数と戻り値を設定する。引数を固定値を指定した場合はその値の結果しか返さない

f:id:m_shige1979:20170115180018p:plain

所感

テスト自体はしなくてはいけないとおもいつつも動作確認することで対応すればよいよね…
みたいな感じになるからついやらなくなってしまいます。
今回はきちんとやってみることにする画面関連のテストは無理のような感じがするけどRest APIのようにデータの流れを理解することで対応する。

まあ、あとは実装時にモック化しやすいように考慮しておく