Spring Boot Test
Spring Boot提供了一些注解和工具去幫助開(kāi)發(fā)者測(cè)試他們的應(yīng)用芒澜。相較于SpringBoot1.3,SpringBoot1.4對(duì)測(cè)試有了大的改進(jìn)岁经,以下示例適用SpringBoot1.4.1以及以上版本。在項(xiàng)目中使用Spring Boot Test支持,只需要在pom.xml引入如下配置即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1. spring-boot-start-test
SpringBoot提供了spring-boot-start-test
啟動(dòng)器须教,該啟動(dòng)器提供了常見(jiàn)的單元測(cè)試庫(kù):
JUnit: 一個(gè)Java語(yǔ)言的單元測(cè)試框架
Spring Test & Spring Boot Test:為Spring Boot應(yīng)用提供集成測(cè)試和工具支持
AssertJ:支持流式斷言的Java測(cè)試框架
Hamcrest:一個(gè)匹配器庫(kù)
Mockito:一個(gè)java mock框架
JSONassert:一個(gè)針對(duì)JSON的斷言庫(kù)
JsonPath:JSON XPath庫(kù)
2. 常用注解
這里介紹一些Spring Boot單元測(cè)試常用的注解,更多詳細(xì)請(qǐng)到Spring Boot官網(wǎng)[查看]
(http://docs.spring.io/spring-boot/docs/1.4.1.RELEASE/reference/htmlsingle/#boot-features-testing)斩芭。
-
@RunWith(SpringRunner.class)
JUnit運(yùn)行使用Spring的測(cè)試支持轻腺。SpringRunner是SpringJUnit4ClassRunner的新名字,這樣做的目的
僅僅是為了讓名字看起來(lái)更簡(jiǎn)單一點(diǎn)划乖。 -
@SpringBootTest
該注解為SpringApplication創(chuàng)建上下文并支持Spring Boot特性贬养,其
webEnvironment
提供如下配置:Mock
-加載WebApplicationContext并提供Mock Servlet環(huán)境,嵌入的Servlet容器不會(huì)被啟動(dòng)琴庵。RANDOM_PORT
-加載一個(gè)EmbeddedWebApplicationContext并提供一個(gè)真實(shí)的servlet環(huán)境误算。嵌入的Servlet容器將被啟動(dòng)并在一個(gè)隨機(jī)端口上監(jiān)聽(tīng)。DEFINED_PORT
-加載一個(gè)EmbeddedWebApplicationContext并提供一個(gè)真實(shí)的servlet環(huán)境迷殿。嵌入的Servlet容器將被啟動(dòng)并在一個(gè)默認(rèn)的端口上監(jiān)聽(tīng)
(application.properties配置端口或者默認(rèn)端口8080)儿礼。NONE
-使用SpringApplication加載一個(gè)ApplicationContext,但是不提供任何的servlet環(huán)境庆寺。 -
@MockBean
在你的ApplicationContext里為一個(gè)bean定義一個(gè)Mockito mock蚊夫。
-
@SpyBean
定制化Mock某些方法。使用
@SpyBean
除了被打過(guò)樁的函數(shù)懦尝,其它的函數(shù)都將真實(shí)返回知纷。 -
@WebMvcTest
該注解被限制為一個(gè)單一的controller,需要利用@MockBean去Mock合作者(如service)导披。
測(cè)試用例設(shè)計(jì)
1. 測(cè)試用例設(shè)計(jì)方法
根據(jù)目前現(xiàn)狀屈扎,單元測(cè)試主要用來(lái)進(jìn)行程序核心邏輯測(cè)試。邏輯覆蓋測(cè)試是通過(guò)對(duì)程序邏輯結(jié)構(gòu)的遍歷來(lái)實(shí)現(xiàn)程序邏輯覆蓋撩匕。從對(duì)源代碼的覆蓋程度不同分為以下六種標(biāo)準(zhǔn)鹰晨,本文只對(duì)其中的五種進(jìn)行分析(路徑覆蓋除外),下面從一段代碼開(kāi)始止毕。
public int example(int x, int y, int z){
if (x>1 && z>2){
x = x + y;
}
if (y == 3 || x > 5){
x = x - 2;
}
return x;
}
一般單元測(cè)試不會(huì)根據(jù)代碼來(lái)寫(xiě)用例模蜡,而是會(huì)根據(jù)流程圖來(lái)編寫(xiě)測(cè)試用例,以上代碼畫(huà)出的流程圖如下:
-
語(yǔ)句覆蓋
1. 概念
設(shè)計(jì)足夠多的測(cè)試用例扁凛,使得被測(cè)試程序中的每條可執(zhí)行語(yǔ)句至少被執(zhí)行一次忍疾。
2. 測(cè)試用例
| 數(shù)據(jù) | 執(zhí)行路徑 |
|: ------------------ |:--------------------------:|
| {x=6;y=3;z=3} | a->c->b->d->e->f |
3. 測(cè)試的充分性
假設(shè)語(yǔ)句`x1&&z>2`中的`&&`寫(xiě)成了`||`上面的測(cè)試用例是檢查不出來(lái)的。
-
判定覆蓋
1. 概念
設(shè)計(jì)足夠的測(cè)試用例使得代碼中的判斷
真
谨朝、假
分支至少被執(zhí)行一次卤妒。我們標(biāo)記x>1&&z>2
為P1y==3 || x>5
為P2甥绿。
2. 測(cè)試用例
| 數(shù)據(jù) | P1 | P2 | 執(zhí)行路徑 |
|: ------------------ |:--------------------------:|:------------------:|:------------------:|
| {x=3;y=3;z=3} |T| T | a->c->b->d->e->f |
| {x=0;y=2;z=3} |F|F| a->c->d->f |
3. 測(cè)試的充分性
假設(shè)語(yǔ)句`y==3 || x>5`中的`||`寫(xiě)成了`&&`上面的測(cè)試用例是檢查不出來(lái)的。和語(yǔ)句覆蓋相比:由于判定覆蓋不是在判斷假分支就是在判斷真分支则披,所以滿足了判定覆蓋就一定會(huì)滿足語(yǔ)句覆蓋共缕。
條件覆蓋
1. 概念
設(shè)計(jì)足夠多的測(cè)試用例,使得被測(cè)試程序每個(gè)判斷語(yǔ)句中的每個(gè)邏輯條件的可能值至少滿足一次士复。在本例中有兩個(gè)判斷分支`(x>1&&z>2)`和` (y == 3 || x > 5)`分別記為P1和P2图谷。總共有三個(gè)條件`x>1`阱洪、`z>2`便贵、`y==3`和`x>5`分別記為B1、B2冗荸、B3承璃、B4。
2. 測(cè)試用例
數(shù)據(jù) | P1 | P2 | B1 | B2 | B3 | B4 | 執(zhí)行路徑 |
---|---|---|---|---|---|---|---|
{x=0;y=2;z=3} | F | F | F | T | F | F | a->c->d->f |
{x=3;y=3;z=1} | F | T | T | F | T | T | a->c->d->f |
3. 測(cè)試的充分性
從上面的結(jié)論看俏竞,條件覆蓋沒(méi)法滿足100%的語(yǔ)句覆蓋绸硕,當(dāng)然沒(méi)法滿足100%的判定覆蓋。
判定/條件覆蓋
1. 概念
同時(shí)滿足100%的條件覆蓋和100%的判定覆蓋魂毁。
2. 測(cè)試用例
數(shù)據(jù) | P1 | P2 | B1 | B2 | B3 | B4 | 執(zhí)行路徑 |
---|---|---|---|---|---|---|---|
{x=0;y=2;z=1} | F | F | F | F | F | F | a->c->d->f |
{x=3;y=3;z=3} | T | T | T | T | T | T | a->c->b->d->e->f |
##### 3. 測(cè)試的充分性
達(dá)到100%判定-條件覆蓋標(biāo)準(zhǔn)一定能夠達(dá)到100%條件覆蓋玻佩、100%判定覆蓋和100%語(yǔ)句覆蓋。
-
條件組合覆蓋
1. 概念
設(shè)計(jì)足夠多的測(cè)試用例席楚,使得被測(cè)試程序中的每個(gè)判斷的所有可能條件取值的組合至少被滿足一次咬崔。
注意
:- 條件組合只針對(duì)同一個(gè)判斷語(yǔ)句內(nèi)存在多個(gè)判斷條件,讓這些條件的取值進(jìn)行笛卡爾乘積組合烦秩。
- 不同判斷語(yǔ)句內(nèi)的條件無(wú)需組合
- 對(duì)于單條件語(yǔ)句垮斯,只需要滿足自己的所有取值即可
本例中判斷
(x>1&&z>2)
有如下組合:(1)x>1&&z>2 (2)x>1&&z<=2 (3)x<=1&&z>2 (4) x<=1&&z<=2;判斷(y == 3 || x > 5)
有如下組合(1)y==3||x>5 (2)y==3||x<=5 (3)y!=3||x>5 (4)y!=3||x<=5
2. 測(cè)試用例
條件組合 | 數(shù)據(jù) | 執(zhí)行路徑 |
---|---|---|
x>1 and z>2 y==3 or x>5 | {x=3;z=3;y=3} | a->c->b->d->e->f |
x>1 and z<=2 y==3 or x<=5 | {x=2;z=3;y=3} | a->c->b->d->f |
x<=1 and z>2 y!=3 or x>5 | {x=1;z=3;y=5} | a->c->d->e->f |
x<=1 and z<=2 y!=3 or x<=5 | {x=1;z=2;y=3} | a->c->d->f |
3. 測(cè)試的充分性
100%滿足條件組合標(biāo)準(zhǔn)一定滿足100%條件覆蓋標(biāo)準(zhǔn)和100%判定覆蓋標(biāo)準(zhǔn)怎憋。
在單元測(cè)試中替代Get和Post請(qǐng)求測(cè)試方式
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ScoreControllerTestNew {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testScore(){
String jsonStr = "{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":55}";
String content = this.restTemplate.postForObject("/scoreApi/score", jsonStr, String.class );
assertThat(content).isEqualTo("{\"result\":{\"score\":\"300\",\"logit\":21.144779999999997},\"response_code\":\"00\",\"response_msg\":\"success\"}");
}
}
測(cè)試Controller
在測(cè)試Controller時(shí)需要進(jìn)行隔離測(cè)試承绸,這個(gè)時(shí)候需要Mock Service層的服務(wù)。
@RunWith(SpringRunner.class)
@WebMvcTest(ScoreController.class)
public class ScoreControllerTestNew {
@Autowired
private MockMvc mockMvc;
@MockBean
private ICalculateService calculateService;
@MockBean
private IModelMonitorService modelMonitorService;
@MockBean
private IScoreConfigService scoreConfigService;
@MockBean
private IModelProductService modelProductService;
@Before
public void setUp(){
}
@Test
public void testScore() throws Exception {
given(this.modelProductService.get(anyLong()))
.willReturn(null);
String jsonStr = "{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":5}";
RequestBuilder requestBuilder = null;
requestBuilder = post("/scoreApi/score").contentType(MediaType.APPLICATION_JSON).content(jsonStr);
this.mockMvc.perform(requestBuilder).andExpect(status().isOk()).andExpect(MockMvcResultMatchers.content().string("{}"));
}
}
測(cè)試Service
測(cè)試Service和測(cè)試Controller類(lèi)似厚柳,同樣采用隔離法抛寝。
@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceTest {
@MockBean
private ModelMonitorMapper modelMonitorMapper;
@Autowired
private IModelMonitorService modelServiceServiceImpl;
@Test
public void testModelServiceServiceImpl(){
given(modelMonitorMapper.insert(anyObject()))
.willReturn(0);
int n = modelServiceServiceImpl.insert(new ModelMonitor());
assertThat(n).isEqualTo(0);
}
}
測(cè)試Dao
測(cè)試的時(shí)候?yàn)榱朔乐挂肱K數(shù)據(jù)使用注解@Transactional和@Rollback在測(cè)試完成后進(jìn)行回滾熊杨。
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ScoreControllerTestNew {
@Autowired
private ModelMonitorMapper modelMonitorMapper;
@Test
@Rollback
public void testDao() throws Exception {
ModelMonitor modelMonitor = new ModelMonitor();
modelMonitor.setModelProductId(Long.parseLong("5"));
modelMonitor.setLogit(21.144779999999997);
modelMonitor.setDerivedVariables("{\"debit_account_balance_code\":1.0,\"credit_consume_count\":1.0,\"debit_start_age\":1.0,\"debit_consume_sum_code\":1.0,\"age\":1.0}");
modelMonitor.setScore("300");
modelMonitor.setSrcData("{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":5}");
int n = modelMonitorMapper.insert(modelMonitor);
assertThat(n).as("檢查數(shù)據(jù)是否成功插入").isEqualTo(0);
}
}