SpringBoot應用測試
測試Springboot應用需要依賴一個非常重要的註解@SpringBootTest,這個註解會爲測試用例構建Spring容器。@SpringBootTest註解修飾的測試用例默認不會啓動web容器,如果需要啓動web容器需要設置webEnvironment屬性:
- MOCK(默認):會啓動一個mock的web server,可以配合@AutoConfigureMockMvc註解對web應用進行測試(後面會舉例)
- RANDOM_PORT:創建ApplicationContext上下文,啓動一個真實的Web容器,監聽一個隨機的端口。
- DEFINED_PORT:創建ApplicationContext上下文,啓動一個真實的Web容器,監聽SpringBoot配置配置文件中指定的端口,默認是8080端口。
- NONE:只是啓動ApplicationContext,不會啓動任何(Mock或者非Mock)web容器。
如果是使用Junit來進行單元測試,再增加一個@RunWith(SpringRunner.class)或者@RunWith(SpringJUnit4ClassRunner.class)註解。
利用模擬Web容器來測試Restful接口
當測試用例使用@SpringBootTest註解修飾並將webEnvironment屬性設置MOCK之後,測試用例在執行的過程中,就會創建一個模擬的web容器。爲了讓測試用例能夠訪問這個模擬的web容器,還需要增加@AutoConfigureMockMvc註解,這樣就可以把MockMvc對象利用@Autowired註解注入到測試用例的屬性中。MockMvc類的作用就是可以訪問模擬的Web容器。接下來我們看一下使用MockMvc配合MockMvcRequestBuilders及MockMvcResultMatchers來訪問模擬的Web容器進行測試Rest接口的例子:
- GET請求
@Test
public void testCase() throws Exception {
MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/testQueryData.json?id=6"))
.andExpect(
MockMvcResultMatchers.status().isOk()).andReturn();
Assert.assertNotNull(result.getResponse().getContentAsString());
}
- POST請求提交表單
@Test
public void testCase() throws Exception {
UrlEncodedFormEntity formData = new UrlEncodedFormEntity(Arrays.asList(
new BasicNameValuePair("param1", "xxxxxxx"),
new BasicNameValuePair("param2", "xxxxxxx")
), "utf-8");
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/testPostData.json")
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.content(EntityUtils.toString(formData))).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
Assert.assertNotNull(result.getResponse().getContentAsString());
}
- DELETE請求
@Test
public void testCase() throws Exception {
mvc.perform(MockMvcRequestBuilders.delete("/testDeleteData.json?id=1"))
.andExpect(
MockMvcResultMatchers.status().isOk());
}
- PUT請求類似POST
@Test
public void testCase() throws Exception {
MvcResult result = mvc.perform(MockMvcRequestBuilders.put("/testPutData"))
.andExpect(
MockMvcResultMatchers.status().isOk()).andReturn();
Assert.assertNotNull(result.getResponse().getContentAsString());
}
利用TestRestTemplate測試真實的Restful接口
Springboot提供了專門用來測試Restful接口的工具類TestRestTemplate,這個類對RestTemplate進行了封裝,可以讓用戶很快捷的編寫出http客戶端測試代碼。
實例如下:
- GET請求
public void testCase() throws Exception {
String restUrl = "http://localhost:8080/xxxxx/xxxxx.json?id=xxx";
ResponseEntity<RegcoreResp> response = testRestTemplate.getForEntity(restUrl, RegcoreResp.class); //RegcoreResp是用戶自行定義的返回的數據封裝Bean,
System.out.println(response.getBody());
}
- POST請求並採用表單方式提交數據
public void testCase() throws Exception {
String restUrl = "http://localhost:8080/xxxxx/xxxxx.json";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("name", "testName");
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(params, headers);
ResponseEntity<RegcoreResp> response = testRestTemplate.postForEntity(restUrl, requestEntity, RegcoreResp.class);
}
- POST請求並採用request Body方式傳輸數據
public void testCase() throws Exception {
String restUrl = "http://localhost:8080/xxxxx/xxxxx.json";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String body = "{\"id\":\"xxxxxx\",\"status\":\"xxxxx\"}";
HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
ResponseEntity<BaseFacadeResp> response = testRestTemplate.postForEntity(restUrl, requestEntity, BaseFacadeResp.class);
}
- DELETE
public void testCase() throws Exception {
String restUrl = "http://localhost:8080/xxxxx/xxxxx.json";
testRestTemplate.delete(restUrl); //delete方法沒有返回值
}
- PUT方式與POST類似
public void testCase() throws Exception {
String restUrl = "http://localhost:8080/xxxxx/xxxxx.json";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String body = "{\"id\":\"xxxxxx\",\"status\":\"xxxxx\"}";
HttpEntity<String> requestEntity = new HttpEntity<String>(body, headers);
testRestTemplate.put(restUrl, requestEntity); //put方法沒有返回值
}
Mock與Spy
- Mock利用動態代理對一個接口或者類的所有方法進行模擬。
- Spy同樣利用動態代理,與Mock不同的是Spy針對於一個真實對象進行模擬並可以對被監控對象中某個方法進行打樁(stubed)控制該方法的執行情況,其他沒有打樁的方法則按照對象本身的實際邏輯執行。
SpringBoot對Mock Spy的支持
在測試SpringBoot應用過程中,某些環境依賴問題可能導致無法調用真實Bean邏輯,比如:某些外部依賴的接口沒有準備好。這個時候需要使用Mock、Spy來對Bean進行模擬。SpringBoot提供了@MockBean和@SpyBean兩個註解來實現Mock與Spy能力。由於底層還是使用Mockito,用法上與Mockito沒有區別。
示例:
@RunWith(SpringRunner.class) @SpringBootTest
public class MyTests {
@MockBean
private RemoteService remoteService;
@Autowired
private Reverser reverser;
@Test
public void exampleTest() {
// RemoteService has been injected into the reverser bean
given(this.remoteService.someCall()).willReturn("mock");
String reverse = reverser.reverseSomeCall();
assertThat(reverse).isEqualTo("kcom");
}
}
如何Mock方法模擬內部邏輯?
一般來說mock最典型的用法就是對一個方法的返回值(輸出)進行模擬。如果想模擬方法內部邏輯,比如方法對入參進行修改或者對入參對象的某個方法進行回調如何利用mock來實現呢?
下面舉一個實際的場景,比如有這樣一段被測試代碼:
testDOMapper.insert(testBeanDO);
if(testBeanDO.getId()==null) {
throw new RuntimeException();
}
插入數據之後數據庫中新紀錄的id會回填到templateDO.id這個屬性,這個特性是mybatis的一種常用的用法。但是如果我們用Mock如果來實現測試代碼執行過testDOMapper.insert(templateDO)之後將templateDO.id屬性附上值,如何實現呢?這裏要用Answer這個接口
Mockito.when(testDOMapper.insert(Mockito.any(TestBeanDO.class))).thenAnswer(new
Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
TestBeanDO arg = invocation.getArgumentAt(0, TestBeanDO.class);
arg.setId(1L);
return 1;
}
});
上面的代碼可以看到answer可以模擬調用insert()方法時方法體如何執行。answer的入參InvocationOnMock對象可以獲取實際的入參的對象,這樣只要在answer方法體裏對入參屬性進行修改就可以實現我們想要達到的效果了。
類似的方法還可以使用 ArgumentMatcher 來實現對參數進行捕獲修改,這裏給出示例就不再具體說明了。
Mockito.when(testDOMapper.insert(Mockito.argThat(new ArgumentMatcher<TestBeanDO>() {
@Override
public boolean matches(Object argument) {
((TestBeanDO) argument).setId(1L);
return true;
}
}))).thenReturn(1);
給Spy的對象進行打樁
spy一般對監控一個真實的對象,按照之前mock的常見用法:
Mockito.when(spyObj.method()).thenReturn(xxxx);
其實是不會按照thenReturn()指定的結果返回的,這一點要特別的注意,容易採坑。如果想按照用戶自己的定義對spy對象進行打樁的正確方式是:
Mockito.doReturn(xxxx).when(sypObj).method();
提前設置好方法被調用之後的表現doReturn()返回結果還是doThrow()拋出異常,之後再使用when(spyObj)對spy的對象進行包裝之後設置調用的方法就可以了。