作爲一個java開發,如果不會用mock工具做單元測試是不合格的。mock可以理解爲一個模擬對象,即一個替代者,可以替換掉依賴的對象,這樣一來我們就可以把注意力集中在業務代碼邏輯,驗證自我代碼的正確性。如下圖所示,A類依賴了B類和C類
假如我們要測試A類,於是我們就需要Mock B和C,也就是用模擬的對象替換B和C
Mockito是一款優秀的mock工具,也可以叫做mocking框架,它簡單易用,可讀性強而且驗證語法簡潔。
一、版本依賴
maven:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
gradle:
testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'
ant:
<dependency org="org.mockito" name="mockito-core" rev="3.3.3"/>
二、Mockito的初始化
當我們要使用註解(比如@Mock)來mock對象的使用,就要初始化Mockito,這樣用@Mock標註的對象纔會被實例化,否則直接使用會報Null指針異常。其有兩種初始化的方法:
1. 使用MockitoAnnotations.initMocks方法
@Mock
private List mockList;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void test(){
mockList.add(1);
verify(mockList).add(1);
}
2. 類中使用@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.class)
public class MockitoExample {
@Mock
private List mockList;
@Test
public void test(){
mockList.add(1);
verify(mockList).add(1);
}
}
這裏解析下@RunWith的使用,常見用法有以下幾種
- @RunWith(Suite.class):代表是一個集合測試類,一般是如下用法,也就是其可一次性測試多個用例。
@RunWith(Suite.class)
@Suite.SuiteClasses({ServiceTest.class,A.class})
public class AllTest {
}
public class ServiceTest{
@Test
public void test01(){ }
@Test
public void test02(){}
}
- @RunWith(SpringRunner.class)或者@RunWith(SpringJUnit4ClassRunner.class):代表在Spring容器中運行單元測試。如果配合@SpringBootTest就是在SpringBoot容器中運行單元測試,如下所示,classes加載啓動類。另外SpringRunner就是SpringJUnit4ClassRunner的別名,他們作用是一樣的。
@SpringBootTest(classes = MyApplication.class)
@RunWith(SpringRunner.class)
public class BaseTest {
public void runUnitTest(){
}
}
- @RunWith(MockitoJUnitRunner.class)
可以理解爲使用Mockito工作運行單元測試,它會初始化Mock和@Spy標註的成員變量。
三、Mock的使用
mock主要功能就是模擬一個對象出來,注意這個是假對象,對其任何方法的操作都不會真正執行的。它有兩種使用方法,直接代碼mock一個對象,或者是用@Mock造一個對象
第一種方法:
// 直接代碼mock一個List對象
List list = Mockito.mock(ArrayList.class);
// 這裏的add操作其實沒有真實調用
list.add("22");
// 打印其大小爲0
System.out.println(list.size());
// 校驗add("22")方法是否執行了,校驗通過
Mockito.verify(list).add("22");
第二種方法:
@Mock
private List mockList;
這裏需要注意的是Mock出來的對象是假對象,對其方法的操作不會真正執行,可以理解爲在真實方法的一個代理,每次對方法的調用其實都是調用了代理方法,這個代理方法是一個空方法,不會做任何事情。方法的返回值都返回默認的:
1. boolean:返回false
2. 基本數值類型:返回0
3. 對象類型:返回null
使用mock的好處是,我們可以對方法的入參和返回值做靈活的設置。比如我們可以在初始化的時候設置某個方法入參和返回值,這樣當單元測試執行到這個方法的時候就不用受到方法依賴的約束。假如這樣一種情況,我們想要測定時任務發郵件,我其實想測的是這個定時任務邏輯是否正常,對發送郵件是否成功並不關心。由於發送郵件需要依賴其他系統,我們單元測試是發不了郵件的。這樣把發送郵件的方法給mock掉就很有必要了。
public class Service01 {
public boolean sendMail(String sender, String receiver){
System.out.println(sender + "向" + receiver + "發送了一封郵件");
return true;
}
}
public class MockExample {
@Mock
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了這個返回值的設置,任何入參調用sendMail方法都是返回false
Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
}
@Test
public void testMock(){
// 這裏直接返回了false,不會進去sendMail的代碼邏輯
boolean isSendSucc = service01.sendMail("張三","李四");
}
}
四、Spy的使用
Spy跟Mock不同之處在於,它是會真正執行方法邏輯的。相同之處是它可以指定方法的返回值。
同樣的,它也有兩種使用方法,直接代碼實例化,或者是用@Spy標註
第一種方法:
@Test
public void mock2(){
// 直接代碼spy一個List對象
List list = Mockito.spy(ArrayList.class);
// 這裏的add操作有真實調用
list.add("22");
// 打印其大小爲1,是真實調用了的
System.out.println(list.size());
// 校驗add("22")方法是否執行了,校驗會通過
Mockito.verify(list).add("22");
}
第二種方法:
@Spy
private List spyList;
下面我們來驗證Spy真正執行方法邏輯:
@Spy
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了這個返回值的設置,那麼方法不會執行,任何入參調用sendMail方法都是返回false
// Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(),Mockito.anyString());
}
@Test
public void testMock(){
// 這裏會執行sendMail方法
boolean isSendSucc = service01.sendMail("張三","李四");
System.out.println("郵件發送是否成功:" + isSendSucc);
}
上面單元測試執行結果:
張三向李四發送了一封郵件
郵件發送是否成功:true
注意:Spy不能標註接口,可以是實現類和抽象類
五、InjectMocks的使用
@InjectMocks跟Mock和Spy的邏輯有點不一樣,他用來給標註的成員變量填充帶有@Mock和@Spy標籤的bean,可以理解爲它會吸取所有@Mock和@Spy標註的bean爲自己所用。先看下面的例子:
首先定義一個book的dao
@Mapper
public interface BookDao {
String getBookById(String id);
}
接着定義book的服務類
public interface BookService {
String getBookById(String id);
}
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public String getBookById(String id) {
return bookDao.getBookById(id);
}
}
測試服務類,這裏看到bookService用了@IninjectMocks,那麼bookService裏面成員變量bookDao就會使用@Mock標註的bookDao。這樣我們就解決了bean的依賴問題了,bookServic裏面的bookDao的任何操作完全可以在單元測試類裏指定返回值。
如果是嵌套的bean可以用ReflectionTestUtils.setFileld()綁定成員變量。上面例子中,要測試service上層的Controller,希望mock bookDao,而不是service,那麼可以這樣用:
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/getBook")
public String getBookById(@RequestParam String id){
return bookService.getBookById(id);
}
}
@RunWith(MockitoJUnitRunner.class)
public class BookControllerTest {
@Spy
private BookServiceImpl bookService;
@InjectMocks
private BookController bookController;
@Mock
private BookDao bookDao;
@Before
public void init(){
Mockito.when(bookDao.getBookById("123")).thenReturn("《java語言》");
// 這裏指定bookService的成員變量bookDao
ReflectionTestUtils.setField(bookService,"bookDao",bookDao);
}
@Test
public void testGetBook(){
System.out.println(bookController.getBookById("123"));
}
}
六、@MockBean的使用
@MockBean是SpringBoot中增加的,用來支持容器中的mock測試。它跟mock的使用邏輯是一樣,只是它修飾的對象是容器中的對象,也就是bean對象。
// 注意這個類是容器中的組件
@Component
public class Service01 {
public boolean sendMail(String sender, String receiver){
System.out.println(sender + "向" + receiver + "發送了一封郵件");
return true;
}
}
// 需要加載SpringBoot的上下文
@RunWith(SpringRunner.class)
@SpringBootTest
public class MockBeanExample {
// 使用容器中對象,用MockBean
@MockBean
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了這個返回值的設置,任何入參調用sendMail方法都是返回false
// Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
}
@Test
public void testMock(){
// 這裏直接返回了true,會執行sendMail的代碼
boolean isSendSucc = service01.sendMail("張三","李四");
}
}
七、@SpyBean的使用
@SpyBean也是SpringBoot增加的一個註解,用來支持Spring容器的單元測試,它與Spy的邏輯基本一致,不同之處就在於它標註的對象是容器對象。具體使用可以參考上面@MockBean的使用方法。
八、返回值設置
我們在上面的例子中也看到了,可以指定mock方法的返回值,常用大概有以下幾種:
1. 調用完方法後指定返回值
格式:Mockito.when(調用的類.方法).thenReturn(指定的返回值);
例如上面的例子:Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
如果是@Mock標註的對象方法,這樣設置後不會進去方法執行,直接返回指定值。
如果是@Spy標註的對象方法,這樣設置後會進去執行方法,但是返回指定的返回值。
2. 直接返回指定值
格式:Mockito.doReturn(方法返回值).when(spy標註的對象).調用的方法;
例如上面的例子:Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(),Mockito.anyString());
這樣設置後,不管是@Mock還是@Spy標註的對象方法都不會進去執行,會直接返回指定值。
另外需要注意的是指定方法時,入參的設置也是有講究的。假如有多個入參的方法,一個入參使用了matcher做匹配,那麼其他入參也要用matcher匹配,例如下面用了any方法,那麼第二入參就不能寫死了,可以用eq方法來做匹配
錯誤的寫法:Mockito.when(service01.sendMail(Mockito.anyString(),“王五”)).thenReturn(true);
正確的寫法:Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.eq(“王五”))).thenReturn(true);
3. 設置拋出異常
格式:Mockito.when(調用方法).thenThrow(拋出的異常類);
比如:
Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenThrow(RuntimeException.class);
九、方法的校驗和斷言
通常我們寫單元測試就是要斷言方法的執行是否符合我們的預期,那用什麼方法來做結果的斷言呢?除了junit提供的Assert類中的方法外,Mockito也給我們提供了幾種校驗方法。
1. 斷言方法是否被調用過
格式: Mockito.verify(對象).對象的方法;
比如:Mockito.verify(list).add(“22”),校驗list對象是否調用了add(“22”)方法。
2. 斷言異常
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 讓方法拋出異常
Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenThrow(RuntimeException.class);
}
// 必須拋出指定的異常纔會通過測試
@Test(expected=RuntimeException.class)
public void testThrowException(){
service01.sendMail("張三","李四");
}
3. Assert類中的斷言方法
這個就自己進去源碼看了,有多個方法可以用。
十、侷限性
Mockito也有它的侷限性,主要是兩個:
- 不能mock靜態方法。
- 不能mock私有方法。
這兩種方法可能只能是通過其他工具,或者通過上層方法調用來做測試了。
十一、測試Controller
這裏總結了2種測試方法,都要在SpringBoot的框架中測試。一般在集成測試中使用,需要啓動Spring容器。
- 使用TestRestTemplate模板
@RestController
public class BookController2 {
@GetMapping("/getBookInfo")
public String getBookById(@RequestParam String id){
System.out.println("查詢書籍信息,bookId=" + id);
return "《java語言》";
}
}
@RunWith(SpringRunner.class)
//指定web環境,隨機端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BookControllerTest {
//這個對象是運行在web環境的時候加載到spring容器中
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void testGetBookInfo(){
String result = testRestTemplate.getForObject("/getBookInfo?id=123456", String.class);
System.out.println(result);
}
}
- 使用AutoConfigureMockMvc
AutoConfigureMockMvc會自動注入MockMvc,可以方便的指定入參或者是header,建議使用這種方法測試Controller
@RestController
public class BookController2 {
@PostMapping("/getBookInfo2")
public String getBookById(@RequestParam String id, @RequestHeader String user){
System.out.println(user + "查詢書籍信息,bookId=" + id);
return "《java語言》";
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookControllerTest2 {
@Autowired
public MockMvc mockMvc;
@Test
public void testGetBookInfo() throws Exception {
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.post("/getBookInfo2").param("id","123").header("user","xiaoming"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
System.out.println(result.getResponse().getContentAsString());
}
}
參考: