一 JUnit介紹
- 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è)試;
- JUnit的相關(guān)概念
JUnit的相關(guān)概念
- 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)行.
- 代碼片段
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é)果看出.
- 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ì)上是一樣的.
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常用方法
- 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>
測(cè)試依賴范圍
如果使用spring-boot-starter-test來(lái)進(jìn)行測(cè)試,就會(huì)發(fā)現(xiàn)提供的一下測(cè)試庫(kù):
測(cè)試庫(kù)
- 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上下文.
- 測(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).
- 測(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.
- 完成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))
- 比較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},兩者依然匹配.
- 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
- 在測(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的局限性
- 通過(guò)腳本來(lái)初始化單元測(cè)試數(shù)據(jù)庫(kù)使得初始化數(shù)據(jù)不夠直觀,特別是需要初始化的數(shù)據(jù)庫(kù)涉及多張表 多條數(shù)據(jù)的時(shí)候;
- 初始化SQL腳本中的數(shù)據(jù)不得不與單元測(cè)試中的代碼數(shù)據(jù)寫(xiě)死,比如user.sql模擬了一條主鍵為1的user數(shù)據(jù),單元測(cè)試中也必須用1來(lái)作為User對(duì)象的id屬性,
- 單元測(cè)試到底能覆蓋多少業(yè)務(wù)場(chǎng)景,對(duì)于項(xiàng)目經(jīng)理或者需求人員并不明顯,需要用一種直觀的表達(dá)方式
- 在業(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í)例.