1. 概要
軟件測(cè)試是一個(gè)應(yīng)用軟件質(zhì)量的保證。java開(kāi)發(fā)者開(kāi)發(fā)接口往往忽視接口單元測(cè)試庐船。作為java開(kāi)發(fā)如果會(huì)Mock單元測(cè)試银酬,那么你的bug量將會(huì)大大降低。spring提供test測(cè)試模塊筐钟,所以現(xiàn)在小胖哥帶你來(lái)玩下springboot下的Mock單元測(cè)試揩瞪,我們將對(duì)controller,service 的單元測(cè)試進(jìn)行實(shí)戰(zhàn)操作篓冲。
2. 依賴引入
??
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
按照上面引入依賴而且scope為test李破。該依賴提供了一下類庫(kù)
- JUnit 4: 目前最強(qiáng)大的java應(yīng)用單元測(cè)試框架
- Spring Test & Spring Boot Test: Spring Boot 集成測(cè)試支持.
- AssertJ: 一個(gè)java斷言庫(kù),提供測(cè)試斷言支持.
- Hamcrest: 對(duì)象匹配斷言和約束組件.
- Mockito: 知名 Java mock 模擬框架.
- JSONassert: JSON斷言庫(kù).
- JsonPath: JSON XPath 操作類庫(kù).
以上都是在單元測(cè)試中經(jīng)常接觸的類庫(kù)壹将。有時(shí)間你最好研究一下嗤攻。
3. 配置測(cè)試環(huán)境
一個(gè)Spring Boot 應(yīng)用程序是一個(gè)Spring ApplicationContext
,一般測(cè)試不會(huì)超出這個(gè)范圍。
測(cè)試框架提供一個(gè)@SpringBootTest
注解來(lái)提供SpringBoot單元測(cè)試環(huán)境支持诽俯。你使用的JUnit版本如果是JUnit 4
不要忘記在測(cè)試類上添加@RunWith(SpringRunner.class)
妇菱,JUnit 5
就不需要了。默認(rèn)情況下暴区,@SpringBootTest不會(huì)啟動(dòng)服務(wù)器闯团。您可以使用其 webEnvironment
屬性進(jìn)一步優(yōu)化測(cè)試的運(yùn)行方式,webEnvironment
相關(guān)講解:
-
MOCK
(默認(rèn)):加載Web ApplicationContext并提供模擬Web環(huán)境仙粱。該選擇下不會(huì)啟動(dòng)嵌入式服務(wù)器房交。如果類路徑上沒(méi)有Web環(huán)境,將創(chuàng)建常規(guī)非Web的ApplicationContext
伐割。你可以配合@AutoConfigureMockMvc
或@AutoConfigureWebTestClient
模擬的Web應(yīng)用程序涌萤。 -
RANDOM_PORT
:加載WebServerApplicationContext
并提供真實(shí)的Web環(huán)境淹遵,啟用的是隨機(jī)web容器端口。 -
DEFINED_PORT
:加載WebServerApplicationContext
并提供真實(shí)的Web環(huán)境 和RANDOM_PORT
不同的是啟用你激活的SpringBoot應(yīng)用端口负溪,通常都聲明在application.yml
配置文件中透揣。 -
NONE
:通過(guò)SpringApplication
加載一個(gè)ApplicationContext
。但不提供 任何 Web環(huán)境(無(wú)論是Mock或其他)川抡。
注意事項(xiàng):如果你的測(cè)試帶有@Transactional
注解時(shí)辐真,默認(rèn)情況下每個(gè)測(cè)試方法執(zhí)行完就會(huì)回滾事務(wù)。但是當(dāng)你的 webEnvironment
設(shè)置為RANDOM_PORT
或者 DEFINED_PORT
崖堤,也就是隱式地提供了一個(gè)真實(shí)的servlet web環(huán)境時(shí)侍咱,是不會(huì)回滾的。這一點(diǎn)特別重要密幔,請(qǐng)確保不會(huì)在生產(chǎn)發(fā)布測(cè)試中寫入臟數(shù)據(jù)楔脯。
4. 編寫測(cè)試類測(cè)試你的api
言歸正傳,首先我們編寫了一個(gè) BookService
作為Service 層
??
package cn.felord.mockspringboot.service;
import cn.felord.mockspringboot.entity.Book;
/**
* The interface Book service.
*
* @author Dax
* @since 14 :54 2019-07-23
*/
public interface BookService {
/**
* Query by title book.
*
* @param title the title
* @return the book
*/
Book queryByTitle(String title);
}
其實(shí)現(xiàn)類如下胯甩,為了簡(jiǎn)單明了沒(méi)有測(cè)試持久層昧廷,如果持久層需要測(cè)試注意增刪改需要Spring事務(wù)注解@Transactional
支持以達(dá)到測(cè)試后回滾的目的。
package cn.felord.mockspringboot.service.impl;
import cn.felord.mockspringboot.entity.Book;
import cn.felord.mockspringboot.service.BookService;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
/**
* @author Dax
* @since 14:55 2019-07-23
*/
@Service
public class BookServiceImpl implements BookService {
@Override
public Book queryByTitle(String title) {
Book book = new Book();
book.setAuthor("dax");
book.setPrice(78.56);
book.setReleaseTime(LocalDate.of(2018, 3, 22));
book.setTitle(title);
return book;
}
}
??
controller層如下:
??
package cn.felord.mockspringboot.api;
import cn.felord.mockspringboot.entity.Book;
import cn.felord.mockspringboot.service.BookService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author Dax
* @since 10:24 2019-07-23
*/
@RestController
@RequestMapping("/book")
public class BookApi {
@Resource
private BookService bookService;
@GetMapping("/get")
public Book getBook(String title) {
return bookService.queryByTitle(title);
}
}
我們?cè)赟pring Boot maven項(xiàng)目的單元測(cè)試包 test
下對(duì)應(yīng)的類路徑 編寫自己的測(cè)試類
??
package cn.felord.mockspringboot;
import cn.felord.mockspringboot.entity.Book;
import cn.felord.mockspringboot.service.BookService;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import javax.annotation.Resource;
import java.time.LocalDate;
/**
* The type Mock springboot application tests.
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockSpringbootApplicationTests {
@Resource
private MockMvc mockMvc;
@MockBean
private BookService bookService;
@Test
public void bookApiTest() throws Exception {
String title = "java learning";
// mockbean 開(kāi)始模擬
bookServiceMockBean(title);
// mockbean 模擬完成
String expect = "{\"title\":\"java learning\",\"author\":\"dax\",\"price\":78.56,\"releaseTime\":\"2018-03-22\"}";
mockMvc.perform(MockMvcRequestBuilders.get("/book/get")
.param("title", title))
.andExpect(MockMvcResultMatchers.content()
.json(expect))
.andDo(MockMvcResultHandlers.print());
// mockbean 重置
}
@Test
public void bookServiceTest() {
String title = "java learning";
bookServiceMockBean(title);
Assertions.assertThat(bookService.queryByTitle("ss").getTitle()).isEqualTo(title);
}
/**
* Mock打樁
* @param title the title
*/
private void bookServiceMockBean(String title) {
Book book = new Book();
book.setAuthor("dax");
book.setPrice(78.56);
book.setReleaseTime(LocalDate.of(2018, 3, 22));
book.setTitle(title);
BDDMockito.given(bookService.queryByTitle(title)).willReturn(book);
}
}
測(cè)試類前兩個(gè)注解不用說(shuō)偎箫,第三個(gè)注解@AutoConfigureMockMvc
可能你們很陌生木柬。這個(gè)是用來(lái)開(kāi)啟Mock Mvc測(cè)試的自動(dòng)化配置的。
然后我們編寫一個(gè)測(cè)試方法bookApiTest()
來(lái)測(cè)試BookApi#getBook(String title)
接口淹办。
??
邏輯是 MockMvc
執(zhí)行一個(gè)模擬的get請(qǐng)求然后期望結(jié)果是expect
Json字符串并且將相應(yīng)對(duì)象打印了出來(lái)(下圖1標(biāo)識(shí))眉枕。一旦請(qǐng)求不通過(guò)將拋出java.lang.AssertionError
錯(cuò)誤, 會(huì)把期望值(Expected
)跟實(shí)際值打印出來(lái)(下圖2標(biāo)識(shí))。如果跟預(yù)期相同只會(huì)出現(xiàn)下圖1怜森。
??
5. 測(cè)試打樁
有個(gè)很常見(jiàn)的情形速挑,在開(kāi)發(fā)中有可能你調(diào)用的其他服務(wù)沒(méi)有開(kāi)發(fā)完,比如你有個(gè)短信發(fā)送接口還在辦理短信接口手續(xù)副硅,但是你還需要短信接口來(lái)進(jìn)行測(cè)試姥宝。你可以通過(guò)@MockBean
構(gòu)建一個(gè)抽象接口的實(shí)現(xiàn)。拿上面的BookService
來(lái)說(shuō)想许,假如其實(shí)現(xiàn)類邏輯還沒(méi)有確定伶授,我們可以通過(guò)規(guī)定其入?yún)⒁约皩?duì)應(yīng)的返回值來(lái)模擬這個(gè)bean的邏輯断序,或者根據(jù)某個(gè)情形下進(jìn)行某個(gè)路由操作的選擇(如果入?yún)⑹茿則結(jié)果為B流纹,如果為C則D)。這種模擬也被成為測(cè)試打樁违诗。 這里我們會(huì)用到Mockito
測(cè)試場(chǎng)景描述如下:
- 指定打樁對(duì)象的返回值
- 判斷某個(gè)打樁對(duì)象的某個(gè)方法被調(diào)用及調(diào)用的次數(shù)
- 指定打樁對(duì)象拋出某個(gè)特定異常
一般有以下幾種組合:
-
do/when:包括
doThrow(…).when(…)
/doReturn(…).when(…)
/doAnswer(…).when(…)
-
given/will:包括
given(…).willReturn(…)
/given(…).willAnswer(…)
-
when/then: 包括
when(…).thenReturn(…)
/when(…).thenAnswer(…)
其他都好理解漱凝,著重介紹一下Answer
, Answer
正是為了解決如果入?yún)⑹茿則結(jié)果為B,如果為C則D這種路由操作的诸迟。接下來(lái)我們實(shí)操一下 ,跟最開(kāi)始基本一樣茸炒,只是更換成@MockBean
??
然后利用Mockito
編寫打樁方法void bookServiceMockBean(String title)
愕乎,模擬上面BookServiceImpl
實(shí)現(xiàn)類。不過(guò)模擬的bean每次測(cè)試完都會(huì)自動(dòng)重置壁公。而且不能用于模擬在應(yīng)用程序上下文刷新期間運(yùn)行的bean的行為感论。
??
然后把這個(gè)方法注入controller 測(cè)試方法就可以測(cè)試了。
??
6. 其他
內(nèi)置的assertj
也是常用的斷言紊册,api非常友好比肄,這里也通過(guò)bookServiceTest()
簡(jiǎn)單演示了一下
??
7. 總結(jié)
本文中實(shí)現(xiàn)了一些簡(jiǎn)單的Spring Boot啟用集成測(cè)試。 對(duì)測(cè)試環(huán)境的搭建囊陡,測(cè)試代碼的編寫進(jìn)行了實(shí)戰(zhàn)操作芳绩,基本能滿足日常開(kāi)發(fā)測(cè)試需要,相信你能從本文學(xué)到不少東西撞反。
相關(guān)的講解代碼可以從gitee獲取妥色。
也可通過(guò)我 個(gè)人博客 及時(shí)獲取更多的干貨分享。
關(guān)注公眾號(hào):碼農(nóng)小胖哥遏片,獲取更多資訊