Testing單元測(cè)試

一 JUnit介紹

  1. JUnit是一個(gè)由Java語(yǔ)言編寫(xiě)的開(kāi)源的回歸測(cè)試框架,由Erich Gamma和Kent Beck創(chuàng)建,用于編寫(xiě)和運(yùn)行可重復(fù)的測(cè)試,它是用于單元測(cè)試框架體系xUnit的一個(gè)實(shí)例.所謂單元測(cè)試也就是白盒測(cè)試.JUnit是Java開(kāi)發(fā)使用最為廣泛的框架.該框架也得到絕大多數(shù)Java IDE和其他工具(如:Maven)的集成支持.同時(shí),JUnit還有很多的第三方擴(kuò)展和增強(qiáng)包可供使用.
    回歸測(cè)試:指重復(fù)以前全部或部分的相同測(cè)試;
  1. JUnit的相關(guān)概念
    JUnit的相關(guān)概念
  1. JUnit測(cè)試
    JUnit 3.x 版本通過(guò)對(duì)測(cè)試方法的命名(test+方法名)來(lái)確定是否是測(cè)試,且所有的測(cè)試類必須繼承TestCase.JUnit 4.x版本全面引入了注解來(lái)執(zhí)行我們編寫(xiě)的測(cè)試,JUnit 中有兩個(gè)重要的類(Assume和Assert),以及其他一些重要的注解(BeforeClass AfterClass After Before Test 和 Ignore).其中,BeforeClass和AfterClass在每個(gè)類加載的開(kāi)始和結(jié)束時(shí)運(yùn)行,需要設(shè)置static方法,而B(niǎo)efore和After則在每個(gè)測(cè)試方法開(kāi)始之前和結(jié)束之后運(yùn)行.
  1. 代碼片段
import org.apache.commons.lang3.time.DateFormatUtils;
import org.junit.*;

/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
public class Testing {

    @BeforeClass
    public static void beforeClassTest(){
        System.out.println("單元測(cè)試開(kāi)始之前執(zhí)行初始化......");
        System.out.println("-----------------------------");
    }

    @Before
    public void beforeTest(){
        System.out.println("單元測(cè)試方法開(kāi)始之前執(zhí)行......");
    }

    @Test
    public void test1(){
        Date sd = DateFormatUtils.formatStr2Date("2018-05-16");
        Date ed = DateFormatUtils.formatStr2Date("2018-05-25");

        System.out.println("相差天數(shù): " + DateFormatUtils.getBetweenDays(sd,ed));
        assertEquals("相差天數(shù): " , 9, DateFormatUtils.getBetweenDays(sd,ed));
    }

    @Test
    public void test2(){
        Date sd = DateFormatUtils.formatStr2Date("2018-05-16");
        Date ed = DateFormatUtils.formatStr2Date("2018-09-30");

        System.out.println("相差天數(shù): " + DateFormatUtils.getBetweenDays(sd,ed));
        assertEquals("相差天數(shù): " , 9, DateFormatUtils.getBetweenDays(sd,ed));
    }

    @After
    public void afterTest(){
        System.out.println("單元測(cè)試方法結(jié)束后執(zhí)行......");
    }

    @AfterClass
    public static void afterClassTest(){
        System.out.println("-----------------------------");
        System.out.println("單元測(cè)試開(kāi)始之后執(zhí)行......");
    }
}

JUnit在執(zhí)行每個(gè)@Test方法之前,會(huì)為測(cè)試類創(chuàng)建一個(gè)新的實(shí)例.這有助于提供測(cè)試方法之間的獨(dú)立性,并且避免在測(cè)試代碼中產(chǎn)生意外的副作用.因?yàn)槊總€(gè)測(cè)試方法都運(yùn)行與一個(gè)新的測(cè)試類的實(shí)例上,所以不能再測(cè)試方法之間重用各個(gè)實(shí)例的變量值.
以上代碼Test2未通過(guò)測(cè)試,因?yàn)槲覀冚斎氲念A(yù)期值為9天,實(shí)際上為127天,這一點(diǎn)可以從錯(cuò)誤結(jié)果看出.

  1. JUnit沒(méi)有main()方法作為入口是怎么運(yùn)行的?
    其實(shí)在org.junit.runner包下有個(gè)JUnitCore.class,其中就有一個(gè)是標(biāo)準(zhǔn)的main方法,這就是JUnit入口函數(shù).如此看來(lái),它其實(shí)和我們直接在自己的main方法中跑我們要測(cè)試的方法在本質(zhì)上是一樣的.
  1. Assert

    為了進(jìn)行測(cè)試驗(yàn)證,我們使用了JUnit的Assert類提供的assert方法.正如之前在實(shí)例中所看到的那樣,我們?cè)跍y(cè)試類中靜態(tài)地導(dǎo)入了這些方法.另外,根據(jù)我們隊(duì)靜態(tài)導(dǎo)入的喜好,還可以導(dǎo)入JUnit的Assert類本身,下面是一些常用的assert方法:
    Assert常用方法
  1. Suite
    JUnit設(shè)計(jì)Suite的目的是一次性運(yùn)行一個(gè)或多個(gè)測(cè)試用例,Suite是一個(gè)容器,用來(lái)把幾個(gè)測(cè)試類歸在一起,并把他們作為一個(gè)集合來(lái)運(yùn)行,測(cè)試運(yùn)行器會(huì)啟動(dòng)Suite,而運(yùn)行哪些測(cè)試類由Suite決定.
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
@RunWith(Suite.class)
@Suite.SuiteClasses({TestSuite1.class, TestSuite2.class})
public class TestSuiteMain {
    // 雖然這個(gè)類是空的,但依然可以運(yùn)行JUnit測(cè)試,運(yùn)行時(shí),它會(huì)將Testsuite1.class和TestSuite2.class中
    // 所有的測(cè)試用例都執(zhí)行一遍.
}

二 Spring Boot單元測(cè)試

Spring Boot提供了一些實(shí)用程序和注解,用來(lái)幫助我們測(cè)試應(yīng)用程序.測(cè)試由兩個(gè)模塊支持spring-boot-test和spring-boot-test-autoconfigure.
spring-boot-test:包含了核心項(xiàng)目;
spring-boot-test-autoconfigure:支持自動(dòng)配置測(cè)試.
一般我們會(huì)使用spring-boot-starter-test導(dǎo)入Spring Boot測(cè)試模塊,以及JUnit assertj hamcrest和其他一些有用的庫(kù).集成只需要在pom中添加如下依賴:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artigactId>
</dependency>
  1. 測(cè)試依賴范圍

    如果使用spring-boot-starter-test來(lái)進(jìn)行測(cè)試,就會(huì)發(fā)現(xiàn)提供的一下測(cè)試庫(kù):
    測(cè)試庫(kù)
  1. Spring Boot測(cè)試腳手架
    Spring Boot使用一系列注解來(lái)增強(qiáng)單元測(cè)試以支持Spring Boot測(cè)試.通常Spring Boot單元測(cè)試有類似如下的樣子:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    UserService userService; // 要測(cè)試的service

    @Test
    public void testService(){}
}

@RunWith是JUnit標(biāo)準(zhǔn)的一個(gè)注解,用來(lái)告訴JUnit單元測(cè)試框架不要使用內(nèi)置的方式進(jìn)行單元測(cè)試,而應(yīng)使用RunWith指明的類來(lái)提供單元測(cè)試,所有的Spring單元測(cè)試總是使用SpringRunner.class.
@SpringBootTest用于Spring Boot應(yīng)用測(cè)試,它默認(rèn)會(huì)根據(jù)包名逐級(jí)往上找,一直找到Spring Boot主程序,也就是通過(guò)類注解是否包含@SpringBootApplication來(lái)判斷是否是主程序,并在單元測(cè)試的時(shí)候啟動(dòng)該類來(lái)創(chuàng)建Spring上下文環(huán)境.

注意:Spring單元測(cè)試并不會(huì)再每個(gè)單元測(cè)試方法前都啟動(dòng)一個(gè)全新的Spring上下文,因?yàn)檫@樣太耗時(shí).Spring單元測(cè)試會(huì)緩存上下文環(huán)境,以提供給每個(gè)單元測(cè)試方法.如果你的單元測(cè)試方法改變了上下文,比如更改了Bean定義,你需要在此單元測(cè)試方法上加上@DirtiesContext以提示Spring重新加載Spring上下文.
  1. 測(cè)試Service
    單元測(cè)試Service代碼跟我們平時(shí)通過(guò)Controller調(diào)用Service代碼來(lái)進(jìn)行測(cè)試相比,有三個(gè)需要特別考慮的地方:
    1?? 單元測(cè)試需要保證可重復(fù)的測(cè)試,因此希望Service測(cè)試完畢后,數(shù)據(jù)能自動(dòng)回滾;
    2?? 單元測(cè)試是開(kāi)發(fā)過(guò)程中的一種測(cè)試手段,Service依賴的其他Service還未開(kāi)發(fā)完畢的情況下如何模擬?
    3?? 大多數(shù)Spring Boot應(yīng)用都是面向數(shù)據(jù)庫(kù)的應(yīng)用,如何在單元測(cè)試前模擬好要測(cè)試的場(chǎng)景?
    對(duì)于第一個(gè)問(wèn)題:Spring Boot單元測(cè)試默認(rèn)會(huì)在單元測(cè)試方法運(yùn)行結(jié)束后進(jìn)行事物回滾;
    對(duì)于第二個(gè)問(wèn)題:Spring Boot會(huì)集成Mockit來(lái)模擬未完成的Service類(或者是在單元測(cè)試中不能隨便調(diào)用的第三方接口Service)
    對(duì)于第三個(gè)問(wèn)題:Spring引入了@Sql,在測(cè)試前執(zhí)行一系列的SQL腳本來(lái)初始化數(shù)據(jù).
    以下是Service單元測(cè)試的腳手架:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ServiceTest {

    @Autowired
    UserService userService;

    @MockBean
    private CreditSystemService creditSystemService;

    @Test
    public void testService(){
        int userId = 10;
        int expectedCredit = 100;
        given(this.creditSystemService.getUserCredit(anyInt())).willReturn(expectedCredit);
        int credit = userService.getCredit(10);
        assertEquals(expectedCredit, credit);
    }
}

在這個(gè)例子中,我們要測(cè)試調(diào)用UserService的getCredit以獲取用戶積分.UserService依賴CreditSystemService的getUserCredit,通過(guò)REST接口從積分系統(tǒng)中獲取用戶的積分.UserService定義如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
@Service
@Transactional
public class UserServiceImpl implements UserService{

    @Autowired
    CreditSystemService creditSystemService;

    @Autowired
    UserDao userDao;

    @Override
    public int getCredit(int userId){
        User user = userDao.single(userId);
        if (user != null){
            return creditSystemService.getUserCredit(userId);
        }else {
            return -1;
        }
    }
}

因?yàn)閱卧獪y(cè)試不能實(shí)際調(diào)用creditSystemService(假設(shè)會(huì)調(diào)用一個(gè)第三方系統(tǒng)),因此,我們?cè)趩卧獪y(cè)試類中使用了@MockBean

@MockBean
    private CreditSystemService creditSystemService;

注解@MockBean可以自動(dòng)注入Spring管理的Service,用來(lái)提供模式實(shí)現(xiàn),因此creditSystemService變量在這里實(shí)際上并不是CreditSystemServiceImpl實(shí)例,而是一個(gè)通過(guò)Mockito創(chuàng)建的CreditSystemServiceMokitoMockxxxxxx實(shí)例(這里的xxxxxx是一組隨記數(shù)字).因此,在Spring上下文中,creditSystemService實(shí)現(xiàn)已經(jīng)被模擬實(shí)現(xiàn)代替了.
以下代碼模擬了Bean的getUserCredit方法,無(wú)論傳入什么參數(shù),總是返回100積分:

given(this.creditSystemService.getUserCredit(anyInt())).willReturn(expectedCredit);

given是Mockito的一個(gè)靜態(tài)方法,用來(lái)模擬一個(gè)Service方法調(diào)用返回,anyInt()指示了可以傳入任何參數(shù),willReturn方法說(shuō)明這個(gè)調(diào)用將返回100.

默認(rèn)情況下,單元測(cè)試完畢,事務(wù)總是回滾,有時(shí)需要通過(guò)數(shù)據(jù)庫(kù)查看數(shù)據(jù)測(cè)試結(jié)果而不希望事務(wù)回滾,可以在方法上使用@Rollback(false).
  1. 測(cè)試MVC
    Spring Boot可以單獨(dú)測(cè)試Controller代碼,用來(lái)驗(yàn)證與Controller相關(guān)的URL路徑映射 文件上傳 參數(shù)綁定 參數(shù)校驗(yàn)等特性.可以通過(guò)@WebMvcTest來(lái)完成MVC單元測(cè)試,腳手架如下所示.
/**
 * Created by wb-yxk397023 on 2018/7/29.
 */
@RunWith(SpringRunner.class)
// 需要測(cè)試的Controller
@WebMvcTest(UserControllerTest.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    UserService userService;

    @Test
    public void testMvc() throws Exception{
        int userId = 10;
        int expectedCredit = 100;
        // 模擬userService
        given(this.userService.getCredit(userId)).willReturn(100);
        // MVC調(diào)用
        mvc.perform(get("/user/{id}", userId)).andExpect(content().string(String.valueOf(expectedCredit)));
    }
}
測(cè)試MVC

在Spring MVC Test中,帶有@Service @Component的類不會(huì)自動(dòng)被掃描注冊(cè)為Spring容器管理的Bean.
MockMvc用來(lái)在Service容器內(nèi)對(duì)Controller進(jìn)行單元測(cè)試,并非發(fā)起了HTTP請(qǐng)求調(diào)用Controller.

  1. 完成MVC請(qǐng)求模擬

MockMvc的核心方法如下:

public ResultActions perform(RequestBuilder requestbuilder)

RequestBuilder類可以通過(guò)使用MockMvcRequestBuilders的get post multipart等方法來(lái)實(shí)現(xiàn),一下是一些常用的例子.
模擬一個(gè)Get請(qǐng)求:

mockMvc.perform(get("/hotels?foo={foo}", "bar"));

模擬一個(gè)post請(qǐng)求:

mockMvc.perform(post("/hotels/{id}", 42);

模擬文件上傳:

mockMvc.perform(multipart("/doc").file("file", "文件內(nèi)容".getBytes("UTF-8")));

模擬請(qǐng)求參數(shù):

// 模擬提交message參數(shù)
mvc.perform(get("/user/{id}/{name}", userId, name).param("message", "hello"));
// 模擬一個(gè)checkbox提交
mvc.perform(get("/user/{id}/{name}", userId, name).param("job", "IT", "gov").param(...));
// 直接使用MultiValueMap構(gòu)造參數(shù)
LinkedMultiValueMap params = new LinkedMultiValueMap();
params.put("message", "hello");
params.put("job", "IT");
params.put("job", "gov");
mvc.perform(get("/user/{id}/{name}", userId, name).param(params));

模擬Session和Cookie:

mvc.perform(get("/user.html").sessionAttr(name, value));
mvc.perform(get("/user.html").cookie(new Coolie(name, value)));

設(shè)置HTTP Body內(nèi)容,比如提交的JSON:

String json = ....;
mvc.perform(get("user.html").content(json));

設(shè)置HTTP Header:

mvc.perform(get("/user/{id}/{name}", userId, name)
      // HTTP提交內(nèi)容
      .contentType("application/x-www-form-urlencoded")
      // 期望返回內(nèi)容
      .accept("application/json")
      // 設(shè)置HTTP頭
      .header(header1, value1))
  1. 比較MVC的返回結(jié)果

perform方法返回ResultActions實(shí)例,這個(gè)類代表了MVC調(diào)用的結(jié)果.它提供一系列的andExpect方法來(lái)對(duì)MVC調(diào)用結(jié)果進(jìn)行比較,如:

mockMvc.perform(get("/user/1"))
        // 期望成功調(diào)用,即HTTP Status為200
        .andExpect(status().isOk())
        // 期望返回內(nèi)容是application/json
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        // 檢查返回內(nèi)容
        .andExpect(jsonPath("$.name").value("Jason"));

也可以對(duì)Controller返回的ModeAndView進(jìn)行校驗(yàn),如比較返回的視圖:

mockMvc.perform(post("/form"))
       .andExpect(view().name("/success.btl"));

比較Model:

mockMvc.perform(post("/form"))
       .andExpect(status().isOk()
       .andExpect(model().size(1))
       .andExpect(model().attributeExists("person")
       .andExpect(model().attribute("person", "mufeng"));

比較forward或者redirect:

mockMvc.perform(post("/login"))
       .andExpect(forwardedUrl("/index.html"));
mockMvc.perform(post("/login"))
       .andExpect(redirectedUrl("/index.html"));

比較返回內(nèi)容,使用content():

andExpect(content().string("hello world"));
// 返回內(nèi)容是XML,并且與xmlCotent一樣
andExpect(content().xml(xmlContent));
// 返回內(nèi)容是JSON,并且與jsonContent一樣
andExpect(content().json(jsonContent));
andExpect(content().bytes(bytes));
XML和JSON方法用來(lái)比較返回值和期望值的相似程度,比如返回值是{"success": true},期望值是{"success": true},兩者依然匹配.
  1. JSON比較
    Spring Boot內(nèi)置了JsonPath來(lái)比較返回的JSON內(nèi)容,通常類似如下代碼:
String path = "$.success";
mvc.perform(get("/user/{id}/{name}", userId, name))
   .andExpect(jsonPath(path).value(true));
這段代碼期望返回的JSON的success屬性是true,$代表了JSON的根節(jié)點(diǎn).

以下是一個(gè)JSON文檔,我們可以用JsonPath表達(dá)式來(lái)抽取JSON節(jié)點(diǎn)的內(nèi)容:

{
  "store":{
            "book":[
                    {
                      "category":   "reference",
                      "author"  :   "Nigel Rees",
                      "title"   :   "Sayings of the Century",
                      "price"   :   8.95
                    },
                    {
                      "category":   "fiction",
                      "author"  :   "Evelyn Waugh",
                      "title"   :   "Sword of Honour",
                      "price"   :   12.99
                    }
                   ]
          }
}

下表列舉了一些Spring Boot常用的JsonPath場(chǎng)景,更多的JsonPath使用方法請(qǐng)參考官網(wǎng)

JsonPath常用場(chǎng)景

三 Mockito

  1. 在測(cè)試過(guò)程中,對(duì)那些不容易構(gòu)建的對(duì)象用一個(gè)虛擬對(duì)象來(lái)代替測(cè)試的方法稱為Mock測(cè)試.目前在Java陣營(yíng)中主要的Mock測(cè)試工具有Mockito JMock EasyMock等,Spring Boot內(nèi)置了Mockito.

2.Mockito可以模擬任何類和接口,模擬方法調(diào)用的返回值,模擬拋出異常等.Mockito實(shí)際上同時(shí)也記錄調(diào)用這些模擬方法的輸入/輸出和順序,從而可以校驗(yàn)這些模擬對(duì)象是否被正確的順序調(diào)用,以及按照期望的屬性被調(diào)用.

先來(lái)一段未完成的積分系統(tǒng),模擬CreditSystemService:

public interface CreditSystemService {
    public int getUserCredit(int userId);
    public boolean addCedit(int userId, int score);
}

1?? 學(xué)習(xí)Mockito,我們將暫時(shí)脫離Spring Boot容器測(cè)試,這樣節(jié)省啟動(dòng)時(shí)間.單元測(cè)試使用MockitoJUnitRunner來(lái)運(yùn)行單元測(cè)試,以下代碼是一個(gè)學(xué)習(xí)Mockito的腳手架代碼:

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CreditServiceMockTest{
  @Test
  public void test(){
    int userId = 10;
    // 創(chuàng)建Mock對(duì)象
    CreditSystemService creditService = mock(CreditSystemService.class);
    // 模擬Mock對(duì)象調(diào)用,傳入任何int值都將返回100積分
    when(creditService.getUserCredit(anyInt())).thenReturn(1000);

    // 實(shí)際調(diào)用
    int ret = creditService.getUserCredit(10);
    // 比較期望值和返回值
    assertEquals(1000, ret);
   }
}

org.mockito.Mockito包含了一系列我們會(huì)用到的模擬測(cè)試方法,如mock when thenReturn等.
通過(guò)mock方法可以模擬任何一個(gè)類或者接口,比如模擬一個(gè)java.util.List接口實(shí)現(xiàn),或者模擬一個(gè)java.util.LinkedList類實(shí)現(xiàn):

LinkedList mockedList = mock(LinkedList.class);
List list = mock(List.class);

2??模擬方法參數(shù)
Mockito提供any方法模擬方法的任何參數(shù),比如:

when(creditService.getUserCredit(anyInt())).thenReturn(1000);

anyInt指的是不論傳入任何參數(shù),總是返回100,也可以使用any方法:

when(creditService.getUserCredit(any(int.class))).thenReturn(1000);

單元測(cè)試中,大多數(shù)時(shí)候不推薦使用any方法,因?yàn)闉槟M的對(duì)象提供更明確的輸入/輸出才能更好地完成單元測(cè)試,比如在Spring Boot單元測(cè)試中,通過(guò)UserService調(diào)用CreditSystemService來(lái)獲取用戶積分,傳入的參數(shù)userId是明確的,因?yàn)槲覀冏詈糜镁唧w的方法參數(shù)來(lái)代替any.

int userId = 10;
// 模擬某個(gè)場(chǎng)景需要調(diào)用creditService的getUserCredit
when(creditService.getUserCredit(eq(userId))).thenReturn(1000);

以上這一段代碼模擬了當(dāng)傳入?yún)?shù)是10的時(shí)候,返回100積分.因此,如果在單元測(cè)試中,被測(cè)試代碼并未按照預(yù)期的參數(shù)傳入的時(shí)候,Mockito會(huì)報(bào)錯(cuò).

int ret = creditService.getUserCredit(11);

這段代碼并未按照預(yù)期傳入10,而是傳入了11,運(yùn)行單元測(cè)試會(huì)報(bào)錯(cuò);

Mockito不僅僅能模擬參數(shù)的調(diào)用和返回值,而且也記錄了模擬對(duì)象是如何調(diào)用的,因此,如果我們模擬的調(diào)用并未被實(shí)際調(diào)用,Mockito也會(huì)報(bào)錯(cuò),指示某些模擬并未使用.我們可以通過(guò)verity方法來(lái)更為精準(zhǔn)的校驗(yàn)?zāi)M的對(duì)象是否被調(diào)用.
比如某些場(chǎng)景,我們假設(shè)肯定會(huì)調(diào)用兩次getUserCredit方法.如果沒(méi)有調(diào)用兩次,則判斷單元測(cè)試失敗.

// 模擬getUserCredit
when(creditService.getUserCredit(eq(userId))).thenReturn(1000);
// 實(shí)際調(diào)用,模擬一個(gè)業(yè)務(wù)場(chǎng)景會(huì)調(diào)用兩次getUserCredit接口
int ret = creditService.getUserCredit(userId);
ret = creditService.getUserCredit(userId);
// 比較期望值和返回值
assertEquals(1000,ret);
verify(creditSerbice, times(2)).getUserCredit(eq(userId));

verify方法包含了模擬的對(duì)象和期望的調(diào)用次數(shù),使用times來(lái)構(gòu)造期望調(diào)用的次數(shù),如果在業(yè)務(wù)調(diào)用中只發(fā)生了一次getUserCredit調(diào)用,那么Mockito在單元測(cè)試中就會(huì)報(bào)錯(cuò).

因?yàn)镸ockito能記錄模擬對(duì)象的調(diào)用,因此除了模擬調(diào)用對(duì)象方法的次數(shù),還能驗(yàn)證調(diào)用的順序.使用inOrder方法:

// 創(chuàng)建Mock對(duì)象
CreditSystemService creditService = mock(CreditSystemService.class);
// 模擬Mock對(duì)象調(diào)用
when(creditService.getUserCredit(eq(userId))).thenReturn(1000);
when(creditService.addCedit(eq(userId),anyInt())).thenReturn(true);

// 實(shí)際調(diào)用,先獲取用戶積分,然后增加10分
int ret = creditService.getUserCredit(userId);
creditService.addCedit(userId, ret + 10);

// 驗(yàn)證調(diào)用順序,確保模擬對(duì)象先被調(diào)用getUserCredit,然后在調(diào)用addCedit方法
InOrder inOrder = inOrder(creditService);
inOrder.verify(creditService).getUserCredit(userId);
inOrder.verify(creditService).addCedit(userId, ret + 10);

3?? 模擬方法返回值
當(dāng)使用when來(lái)模擬方法調(diào)用的時(shí)候,可以使用thenReturn來(lái)模擬返回的結(jié)果:

when(creditService.getUserCredit(eq(userId))).thenReturn(1000);
when(creditService.addCedit(eq(userId),anyInt())).thenReturn(true);

也可以使用thenThrow來(lái)模擬拋出一個(gè)異常,比如:

CreditSystemService creditService = mock(CreditSystemService.class);
// 模擬Mock對(duì)象調(diào)用
when(creditService.getUserCredit(eq(userId))).thenReturn(1000);
when(creditService.addCedit(lt(0))).thenThrow(new IllegalArgumentException("userId不能小于0"));
// 實(shí)際調(diào)用
int ret = creditService.getUserCredit(-1);
eq:表示參數(shù)相等的情況下
lt:表示參數(shù)小于0的情況下,將拋出異常;

有些情況下,模擬的方法并沒(méi)有返回值,可以使用doThrow方法來(lái)拋出異常:

// 模擬List對(duì)象
List list = mock(List.class);
doThrow(new UnsupportedOperationException("不支持clear方法調(diào)用")).when(list).clear();
//實(shí)際調(diào)用將拋出異常
list.clear();

這一段代碼模擬了一個(gè)List對(duì)象的clear方法調(diào)用將拋出異常.

四 面向數(shù)據(jù)庫(kù)應(yīng)用的單元測(cè)試

對(duì)于絕大部分的Spring Boot應(yīng)用來(lái)說(shuō),都包含了數(shù)據(jù)庫(kù)CRUD操作,在單元測(cè)試中,我們可以通過(guò)模擬Dao類來(lái)返回預(yù)期的CRUD結(jié)果.但對(duì)于復(fù)雜的面向數(shù)據(jù)庫(kù)應(yīng)用,我們有時(shí)候不但需要比較業(yè)務(wù)調(diào)用結(jié)果是否與期望的一致,我們更期望業(yè)務(wù)調(diào)用完畢,數(shù)據(jù)庫(kù)各個(gè)表的行和列與期望的數(shù)據(jù)庫(kù)行和列的值是一樣的.
比如更新用戶手機(jī)號(hào)碼的業(yè)務(wù)調(diào)用,我們期望調(diào)用完畢后,數(shù)據(jù)庫(kù)的User表中這個(gè)用戶的mobile字段與我們期望的mobile一致.

1??@Sql
Spring Boot提供了@Sql來(lái)初始化數(shù)據(jù)庫(kù).需要為單元測(cè)試準(zhǔn)備一個(gè)新的數(shù)據(jù)庫(kù),這個(gè)心得數(shù)據(jù)庫(kù)通常不包含任何數(shù)據(jù),或者只包含一些必要的字典類型的數(shù)據(jù)和初始化數(shù)據(jù).
注解@Sql可以引入一系列SQL腳本來(lái)進(jìn)一步模擬測(cè)試前的數(shù)據(jù)庫(kù)數(shù)據(jù),以下是單元測(cè)試腳手架:

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class UserDbTest {
    
    @Autowired
    UserService userService;
    
    @Test
    @Sql({"user.sql"}) // 初始化一條主鍵為1的用戶數(shù)據(jù)
    public void upateNameTest() {
        User user = new User();
        user.setId(1);
        user.setName("hello123");
        // 修改用戶名稱
        boolean success = userService.updateUser(user);
        assertTrue(success);
    }
}

這段代碼與我們之前的Spring Boot單元測(cè)試腳手架差不多,有兩個(gè)區(qū)別:
1 @ActiveProfiles("test"),因?yàn)槲覀冃枰B接一個(gè)專門用于單元測(cè)試的數(shù)據(jù)庫(kù),因此我們激活了test作為profile.我們可以創(chuàng)建一個(gè)新的名為application-test.property的Spring Boot配置文件,進(jìn)行單元測(cè)試的時(shí)候?qū)⒆x取此配置文件.

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/orm-test?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

orm-test數(shù)據(jù)庫(kù)與ORM類似,不同的是orm-test不包含任何數(shù)據(jù),僅僅用于單元測(cè)試.
@Sql注解可以包含多個(gè)SQL腳本,用來(lái)在單元測(cè)試方法前初始化數(shù)據(jù).如果SQL腳本沒(méi)有以"/"開(kāi)頭,則默認(rèn)在測(cè)試類所在包下.否則,則從根目錄搜索.也可以使用classpath: file: http:作為前綴的資源文件.上述例子中的@Sql({"user.sql"})相當(dāng)于@Sql({"classpath:com/bee/sample/ch9/test/db/user.sql"}), user.sql的內(nèi)容如下:

INSERT INTO 'user' (id, 'name', 'department_id') VALUES(1,'lijz','1');

使用@Sql盡管能在單元測(cè)試前初始化所需要的數(shù)據(jù),但要比較調(diào)用業(yè)務(wù)方法后的數(shù)據(jù)庫(kù)中的數(shù)據(jù)是否與我們期望的數(shù)據(jù)庫(kù)數(shù)據(jù)一致,Spring現(xiàn)在還沒(méi)有提供更多直接的辦法,我們只能通過(guò)Dao從數(shù)據(jù)庫(kù)加載數(shù)據(jù)來(lái)進(jìn)行比較.以BeetlSQL為例,可以做如下改進(jìn):

@Autowired
UserDao userDao;

@Test
@Sql({"user.sql"})
public void upateNameTest() {
    User user = new User();
    user.setId(1);
    user.setName("hello123");
    boolean success = userService.updateUser(user);
    User dbUser = userDao.unique(1);
    assertEquals(dbUser.getName(), "hello123");
}

在單元測(cè)試中注入BeetlSQL的UserDao,并在單元測(cè)試中調(diào)用unique方法以獲得User實(shí)例來(lái)進(jìn)行比較.

五 其他的數(shù)據(jù)庫(kù)單元測(cè)試工具(推薦)

1?? Spring@Sql的局限性

  1. 通過(guò)腳本來(lái)初始化單元測(cè)試數(shù)據(jù)庫(kù)使得初始化數(shù)據(jù)不夠直觀,特別是需要初始化的數(shù)據(jù)庫(kù)涉及多張表 多條數(shù)據(jù)的時(shí)候;
  1. 初始化SQL腳本中的數(shù)據(jù)不得不與單元測(cè)試中的代碼數(shù)據(jù)寫(xiě)死,比如user.sql模擬了一條主鍵為1的user數(shù)據(jù),單元測(cè)試中也必須用1來(lái)作為User對(duì)象的id屬性,
  1. 單元測(cè)試到底能覆蓋多少業(yè)務(wù)場(chǎng)景,對(duì)于項(xiàng)目經(jīng)理或者需求人員并不明顯,需要用一種直觀的表達(dá)方式
  1. 在業(yè)務(wù)方法調(diào)用完畢,需要比較數(shù)據(jù)庫(kù)的數(shù)據(jù)與期望的數(shù)據(jù)是否一致的時(shí)候,還需要調(diào)用Dao加載數(shù)據(jù)后進(jìn)行一一比較,非常不直觀.
鑒于以上幾種局限性,在這里向大家推薦另外一個(gè)面向數(shù)據(jù)庫(kù)的單元測(cè)試工具XLSUnit;

它通過(guò)Excel的工作表模擬數(shù)據(jù)庫(kù)初始化數(shù)據(jù),用其他工作表來(lái)模擬單元測(cè)試后的期望結(jié)果,只需要寫(xiě)少量單元測(cè)試代碼,維護(hù)直觀的Excel文件,就可以完成單元測(cè)試,這里就不在展開(kāi)介紹XLSUnit,如果大家感興趣可以訪問(wèn)官網(wǎng)學(xué)習(xí)以及下載對(duì)應(yīng)的實(shí)例.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容