Mockito單元測試的使用

作爲一個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的使用,常見用法有以下幾種

  1. @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(){}
}
  1. @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(){
    }
}
  1. @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也有它的侷限性,主要是兩個:

  1. 不能mock靜態方法。
  2. 不能mock私有方法。

這兩種方法可能只能是通過其他工具,或者通過上層方法調用來做測試了。

十一、測試Controller

這裏總結了2種測試方法,都要在SpringBoot的框架中測試。一般在集成測試中使用,需要啓動Spring容器。

  1. 使用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);
    }
}
  1. 使用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());
    }
}

參考:

  1. Mockito教程
  2. springboot test
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章