Mockito用於測試時進行打樁處理;通過它可以指定某個類的某個方法在什么情況下返回什么樣的值。
例如:測試 controller時,依賴 service,這個時候就可以假設當調用 service 某個方法時返回指定的某些值,從而來降低引用類所帶來的測試復雜度增加的影響。Mockito就用於這種場景。
Mockito常用測試場景描述如下:
- 指定打樁對象的返回值
- 判斷某個打樁對象的某個方法被調用及調用的次數
- 指定打樁對象拋出某個特定異常
Mockito的使用,一般有以下幾種組合:
- do/when:包括doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)
- given/will:包括given(…).willReturn(…)/given(…).willAnswer(…)
- when/then: 包括when(…).thenReturn(…)/when(…).thenAnswer(…)
指定打樁對象返回值
通過Mockito指定打樁對象的返回值時,可以通過以下方式進行:
given
given用於對指定方法進行返回值的定制,它需要與will開頭的方法一起使用,will開頭的方式根據其接收參數的不同,又分成兩類:一是接收直接值的,如直接指定返回結果為某個常量;二是接收Answer參數的,可以騎過Answer類的answer方法來根據傳入參數定制返回結果。
Answer對象
我們實際針對的一般是某個類的某個方法;這個方法可能會有輸入參數;考慮這種場景:如果要假設打樁的這個方法,在某個輸入時返回值A;在另外一個輸入時返回值為B;這種場景就可以通過Answer類來實現。
given + willAnswer/will
案例 根據傳入的參數,返回不同的數據
@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnController2Test {
@Autowired
private WebApplicationContext wac;
private MockMvc mvc;
private MockHttpSession session;
/**
* 1. 對於不需要返回的任何值的類的所有方法,可以直接使用MockBean
* 2. @MockBean 會代理已有的bean的方法,不會執行真實 bean 的具體方法。
*/
@MockBean
private LearnService learnService;
@Before
public void setupMockMvc() {
//初始化MockMvc對象
mvc = MockMvcBuilders.webAppContextSetup(wac).build();
//構建session
session = new MockHttpSession();
User user = new User("root", "root");
//攔截器那邊會判斷用戶是否登錄,所以這里注入一個用戶
session.setAttribute("user", user);
}
/**
* 獲取教程測試用例
* <p>
* get 請求
* <p>
* controller 依賴 service 的方法,這里給 service 方法打樁,不執行真實的方法
*
* @throws Exception
*/
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
// 當調用 selectByKey 函數時,返回指定的值
given(this.learnService.selectByKey(Mockito.any())).willAnswer(new Answer<Object>() {
/**
* InvocationOnMock 通過它可以獲取打樁方法的實際傳入參數清單
* @param invocationOnMock
* @return
* @throws Throwable
*/
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Long argumentAt = invocationOnMock.getArgumentAt(0, Long.class);
System.out.println("調用方法的實際參數: " + argumentAt);
if (argumentAt.equals(Long.parseLong("1001"))) {
return learnResource;
}
return null;
}
});
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
}
given + willReturn
通過willReturn可以直接指定打樁的方法的返回值
案例 在任何場景下,都返回指定的數據
/**
* 獲取教程測試用例
* <p>
* get 請求
* <p>
* controller 依賴 service 的方法,這里給 service 方法打樁,不執行真實的方法
*
* @throws Exception
*/
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
異常信息:
java.lang.AssertionError: JSON path "$.author"
Expected :嘟嘟MD獨立博客
Actual :zhang
<Click to see difference>
when + thenReturn
thenReturn與willReturn類似
/**
* 獲取教程測試用例
* <p>
* get 請求
* <p>
* controller 依賴 service 的方法,這里給 service 方法打樁,不執行真實的方法
*
* @throws Exception
*/
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
when(this.learnService.selectByKey(Mockito.any())).thenReturn(learnResource);
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
when + thenAnswer/then
thenAnswer與willAnswer也類似
/**
* 獲取教程測試用例
* <p>
* get 請求
* <p>
* controller 依賴 service 的方法,這里給 service 方法打樁,不執行真實的方法
*
* @throws Exception
*/
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
when(this.learnService.selectByKey(Mockito.any())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Long argumentAt = invocationOnMock.getArgumentAt(0, Long.class);
System.out.println("調用方法的實際參數: " + argumentAt);
if (argumentAt.equals(Long.parseLong("1001"))) {
return learnResource;
} else if (argumentAt.equals(Long.parseLong("1002"))) {
learnResource.setAuthor("keke");
return learnResource;
}
return null;
}
});
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1002")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
異常:
參數為 1001 時
java.lang.AssertionError: JSON path "$.author"
Expected :嘟嘟MD獨立博客
Actual :zhang
<Click to see difference>
參數為 1002 時
java.lang.AssertionError: JSON path "$.author"
Expected :嘟嘟MD獨立博客
Actual :keke
<Click to see difference>
doAnswer/doReturn + when
// mock 對象不能是 @MockBean 生成的,@MockBean請況下不能用
@Test
public void testAnswer1() {
List<String> mock = Mockito.mock(List.class);
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] args = invocationOnMock.getArguments();
System.out.println(args[0]);
Integer num = (Integer) args[0];
if (num > 3) {
return "大於三";
} else {
return "小於三";
}
}
}).when(mock).get(Mockito.anyInt());
// 當 索引為 4 時,期望 大於三
Assert.assertThat(mock.get(4), equalTo("大於三"));
// 當 索引為 2 時,期望 小於三
Assert.assertThat(mock.get(4), equalTo("小於三"));
}
// mock 對象不能是 @MockBean 生成的,@MockBean請況下不能用
@Test
public void testAnswer1() {
List<String> mock = Mockito.mock(List.class);
Mockito.doReturn("大於三").when(mock).get(Mockito.anyInt());
// 當 索引為 2 時
Assert.assertThat(mock.get(2), equalTo("大於三"));
}
判斷某個打樁對象的某個方法被調用及調用的次數
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
// 判斷 learnService.selectByKey 方法 是否調用了
Mockito.verify(learnService).selectByKey(1001L);
// 判斷 learnService.selectByKey 方法,期望調用 2 次,能過 times 函數指定 selectByKey 函數期望調用幾次
// 也可以通過 Mockito.atLeast 最少幾次,Mockito.atMost 是多幾次 等函數判斷
Mockito.verify(learnService, Mockito.times(2)).selectByKey(1001L);
}
異常:因為 learnService.selectByKey 方法,調用了1次,而期望調用兩次,所以測試出錯
org.mockito.exceptions.verification.TooLittleActualInvocations:
learnServiceImpl bean.selectByKey(1001);
Wanted 2 times:
-> at com.dudu.outher.LearnController7Test.qryLearn(LearnController7Test.java:86)
But was 1 time:
-> at com.dudu.controller.LearnController.qryLearn(LearnController.java:88)
指定打樁對象拋出某個特定異常
given+willThrow
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
// 調用 learnService.selectByKey 方法時,拋出異常
given(this.learnService.selectByKey(Mockito.any())).willThrow(new Exception("查詢出錯"));
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
異常:
org.mockito.exceptions.base.MockitoException:
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯
when+thenThrow
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("zhang");
learnResource.setAuthor("zhang");
learnResource.setId(10L);
when(this.learnService.selectByKey(Mockito.any())).thenThrow(new Exception("查詢出錯"));
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
異常:
org.mockito.exceptions.base.MockitoException:
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯
doThrow+when
不能用於 @MockBean 場景下
@Test
public void testAnswer1() {
List<String> mock = Mockito.mock(List.class);
// 調用 mock.size 時,拋出期望的異常信息
Mockito.doThrow(new Exception("查詢出錯")).when(mock).size();
// 調用 mock 對象的方法
mock.size();
}
異常:
org.mockito.exceptions.base.MockitoException:
Checked exception is invalid for this method!
Invalid: java.lang.Exception: 查詢出錯
參考
doThrow:在模擬對象中調用方法時想要拋出異常時使用.
doReturn:在執行方法時要返回返回值時使用.
doAnswer:需要對傳遞給方法的參數執行一些操作
doNothing:是最簡單的列表,基本上它告訴Mockito在調用模擬對象中的方法時什么也不做.有時用於void返回方法或沒有副作用的方法,或者與您正在進行的單元測試無關
https://blog.csdn.net/icarusliu/article/details/78860351
靜態方法測試
Mockito無法對靜態方法進行Mock,如果需要Mock靜態方法,需要使用到PowerMockito
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.1</version>
</dependency>
單元測試時,需要使用PowerMockRunner及PrepareForTest兩個注解
@RunWith(PowerMockRunner.class)
// 對 StringUtils 靜態方法進行測試
@PrepareForTest({StringUtils.class})
public class TestStatic {
@Test
public void testStaticMethod() {
// 對 StringUtils 打樁
PowerMockito.mockStatic(StringUtils.class);
PowerMockito.when(StringUtils.isNoneBlank(Mockito.anyString())).thenReturn(false);
boolean bbb = StringUtils.isNoneBlank("bbb");
System.out.println(bbb);
}
}
與SpringBootTest一起使用
SpringBootTest必須要使用SpringRunner才能生效;但RunWith沒有辦法指定多個,可以通過PowerMockRunnerDelegate來解決這個問題:
@RunWith(PowerMockRunner.class)//使用powermock提供的代理來使用
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})//忽略一些powermock使用的classloader無法處理的類
@PrepareForTest({StringUtils.class})// @PrepareForTest 可以 mock 多個靜態方法
@SpringBootTest
public class LearnController11Test {
@Autowired
private WebApplicationContext wac;
private MockMvc mvc;
private MockHttpSession session;
@MockBean
private LearnService learnService;
@Before
public void setupMockMvc() {
//初始化MockMvc對象
mvc = MockMvcBuilders.webAppContextSetup(wac).build();
//構建session
session = new MockHttpSession();
User user = new User("root", "root");
//攔截器那邊會判斷用戶是否登錄,所以這里注入一個用戶
session.setAttribute("user", user);
}
/**
* 獲取教程測試用例
* <p>
* get 請求
* <p>
* controller 依賴 service 的方法,這里給 service 方法打樁,不執行真實的方法
*
* @throws Exception
*/
@Test
public void qryLearn() throws Exception {
LearnResource learnResource = new LearnResource();
learnResource.setUrl("http://www.baidu.com");
learnResource.setTitle("Spring Boot干貨系列");
learnResource.setAuthor("嘟嘟MD獨立博客");
learnResource.setId(10L);
// 對 service層中的方法進行 mock
given(this.learnService.selectByKey(Mockito.any())).willReturn(learnResource);
// 對 StringUtils 打樁,mock 靜態方法
PowerMockito.mockStatic(StringUtils.class);
// 當 執行 StringUtils.isNoneBlank 方法時,返回 false
PowerMockito.when(StringUtils.isNoneBlank(Mockito.anyString())).thenReturn(false);
// 實際使用中 StringUtils.isNoneBlank("bbb") 返回 true,但這里返回 false
boolean result = StringUtils.isNoneBlank("bbb");
System.out.println("StringUtils.isNoneBlank: " + result);
mvc.perform(MockMvcRequestBuilders.get("/learn/resource/1001")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session)
)
.andExpect(MockMvcResultMatchers.status().isOk())
//jsonPath用來獲取author字段比對是否為嘟嘟MD獨立博客,不是就測試不通過
.andExpect(MockMvcResultMatchers.jsonPath("$.author").value("嘟嘟MD獨立博客"))
.andExpect(MockMvcResultMatchers.jsonPath("$.title").value("Spring Boot干貨系列"))
.andDo(MockMvcResultHandlers.print());
}
}