一文徹底精通 Spring Boot 單元測試
環境:SpringBoot2.7.18
1. 簡介
Spring Boot提供了許多實用工具和注釋,可以在測試應用程序時提供幫助。測試支持由兩個模塊提供:spring-boot-test包含核心項,spring-boot-test-autoconfigure支持測試的自動配置。
Spring Boot Test不僅提供了對Spring Test的集成,還增強了mock能力,支持多種類型的測試,包括單元測試、切片測試和功能測試。
在單元測試中,我們通常關注代碼的最小可測試單元,這可以是單個程序、類、對象或方法。單元測試的目的是檢測開發者編寫的代碼或功能是否正確。通過使用Spring Boot Test,我們可以方便地編寫和執行單元測試,確保代碼的質量和正確性。
Spring Boot Test提供了一些注解來支持不同類型的測試。例如,@Test注解用于標記單元測試方法;@WebMvcTest、@DataJpaTest等注解用于切片測試,它介于單元測試和功能測試之間;而@RunWith和@SpringBootTest等注解則用于功能測試,它一般面向某個完整的業務功能。
spring-boot-starter-test依賴提供一下庫的支持:
- JUnit 5: 事實上的Java應用程序單元測試標準。
- Spring Test & Spring Boot Test: 對Spring Boot應用程序的實用程序和集成測試支持。
- AssertJ: 一個流暢的斷言庫。
- Hamcrest: 匹配器對象庫(也稱為約束或謂詞)。
- Mockito: 一個Java模擬框架。
- JSONassert: JSON的斷言庫。
- JsonPath: JSON的XPath。
2. 實戰案例
如果你使用的是JUnit 4,請不要忘記在測試中添加@RunWith(SpringRunner .class),否則注釋將被忽略。如果你使用的是JUnit 5,就不需要添加等效的@ExtendWith(SpringExtension.class),因為@SpringBootTest和其他的@…Test注解已經用它做了注解。
默認情況下,@SpringBootTest不會啟動服務器??梢允褂聾SpringBootTest的webEnvironment屬性進一步優化測試的運行方式:
- MOCK(Default) : 加載一個web ApplicationContext并提供一個模擬的web環境。使用此注釋時,嵌入式服務器不會啟動。如果在classpath中沒有web環境,這種模式就會返回到創建常規的非web ApplicationContext。
- RANDOM_PORT: 加載WebServerApplicationContext并提供一個真實的web環境。嵌入式服務器啟動并監聽一個隨機端口。
- DEFINED_PORT: 加載WebServerApplicationContext并提供一個真實的web環境。嵌入式服務器啟動并監聽一個定義好的端口(從application.properties文件中)或者默認的8080端口。
- NONE: 使用SpringApplication加載ApplicationContext,但不提供任何web環境(模擬或其他)。
2.1 什么Web類型
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
public class SpringBootTest01 {
}
通過上面的配置,聲明當前web應用的類型是基于響應式的webflux。
2.2 使用應用程序參數
如果應用需要參數,可以讓@SpringBootTest使用args屬性注入參數。
@SpringBootTest(args = "--app.test=Pack")
public class MailServiceTest {
@Test
public void testAppArgumentsPopulated(@Autowired ApplicationArguments args) {
assertThat(args.getOptionNames()).containsOnly("app.test") ;
assertThat(args.getOptionValues("app.test")).containsOnly("Pack") ;
}
}
2.3 使用模擬環境進行測試
默認情況下,@SpringBootTest不會啟動服務器,而是設置一個模擬環境來測試web端點。在Spring MVC中,可以使用MockMvc或WebTestClient查詢web端點,如下面的例子所示。
// 已知需測試的接口
@RestController
@RequestMapping("/demos")
public class DemoController {
@GetMapping("/index")
public Object index() {
return "demo index" ;
}
}
// 單元測試
@SpringBootTest
@AutoConfigureMockMvc
public class SpringBootTest02{
@Test
public void testMockMvc(@Autowired MockMvc mvc) throws Exception {
mvc.perform(get("/demos/index"))
// 期望返回狀態碼是200
.andExpect(status().isOk())
// 期望返回的內容是demo index1
// 實際接口返回的是demo index
.andExpect(content().string("demo index1"));
}
}
測試輸出結果
圖片
2.4 運行服務進行測試
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SpringBootTest03 {
@Test
public void testRestTemplate(@Autowired TestRestTemplate restTemplate) throws Exception {
String body = restTemplate.getForObject("/demos/index", String.class);
assertThat(body).isEqualTo("123") ;
}
}
通過RestTemplate測試接口,同時指定需要啟動server,端口隨機。最后通過斷言判斷返回結果。
圖片
隨機生成的端口
圖片
2.5 模擬Bean對象
Spring Boot包含一個@MockBean注解,可以用來為ApplicationContext中的bean定義一個Mockito mock??梢允褂米⑨屘砑有碌腷ean或替換單個現有的bean定義。示例代碼如下:
// 現有的一個組件
@Service
public class RemoteService{
public String getValue(){
return "RemoteData" ;
}
}
// 業務處理組件
@Service
public class BusinessService {
@Resource
private RemoteService remoteService ;
// 該方法用來反轉通過RemoteService#getValue返回的字符串
public String getReverseValue(){
return new StringBuilder(this.remoteService.getValue()).reverse().toString() ;
}
}
測試類
@SpringBootTest
public class SpringBootTest04 {
@Resource
private BusinessService businessService ;
// 模擬的bean,這將在BusinessService中使用到
@MockBean
private RemoteService remoteService;
@Test
public void exampleTest() {
// given方法用于設置模擬對象的方法調用應返回的值或應拋出的異常
given(this.remoteService.getValue()).willReturn("pack") ;
String reverse = this.businessService.getReverseValue() ;
// 這里故意寫錯,以便清晰看到結果
assertThat(reverse).isEqualTo("kcap1") ;
}
}
輸出結果
圖片
3. 切片化測試
Spring Boot的自動配置系統對應用程序來說工作得很好,但有時對測試來說有點過頭了。只加載測試應用程序“切片”所需的配置部分通常會有所幫助。例如,你可能只想測試Spring MVC控制器是否正確映射了url,但不想在測試中涉及數據庫調用,或者想測試JPA實體,而且在測試運行時對web層不感興趣。
spring-boot-test-autoconfigure模塊包含很多注解,可以用來自動配置這樣的 "切片"。它們都以類似的方式工作,提供了一個@…Test注解來加載ApplicationContext,以及一個或多個@AutoConfigure…注解,可用于自定義自動配置設置。
3.1 自動配置SpringMVC測試
要測試Spring MVC控制器是否按預期工作,使用@WebMvcTest注解。@WebMvcTest會自動配置Spring MVC的基礎架構,并將掃描的bean限制為@Controller、@ControllerAdvice、@JsonComponent、Converter、GenericConverter、Filter、HandlerInterceptor、WebMvcConfigurer、WebMvcRegistrations和HandlerMethodArgumentResolver。當使用@WebMvcTest注解時,不會掃描常規的@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可以用來包含@ConfigurationProperties bean。如下示例:
@RestController
@RequestMapping("/business")
public class BusinessController{
@Resource
private RemoteService remoteService ;
@GetMapping("/info")
public String info() {
return this.remoteService.getValue() ;
}
}
// 測試接口
@WebMvcTest(BusinessController.class)
public class SpringBootTest05{
@Autowired
private MockMvc mvc;
@MockBean
private RemoteService remoteService;
@Test
public void testInfo() throws Exception {
// 模擬錯誤,看的清晰
given(this.remoteService.getValue()).willReturn("pack1") ;
this.mvc.perform(get("/business/info").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andExpect(content().string("pack"));
}
}
輸出結果:
圖片
3.2 自動配置JPA測試
可以使用@DataJpaTest注釋來測試JPA應用程序。默認情況下,它會掃描@Entity類并配置Spring Data JPA存儲庫。如果類路徑中有嵌入式數據庫,它也會配置一個。默認情況下,將spring.jpa.show-sql屬性設置為true,就會記錄SQL查詢。這可以通過注解的showSql()屬性來禁用。
當使用@DataJpaTest注解時,不會掃描常規的@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可以用來包含@ConfigurationProperties bean。
@DataJpaTest
// 默認是ANY;如果不修改需要你當前環境有個嵌入式的數據庫;
// 設置為NONE后,不會替換當前環境使用的數據庫
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class SpringBootTest06 {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
public void testExample(){
User entity = new User() ;
entity.setAge(20) ;
entity.setName("pack") ;
this.entityManager.persist(entity);
User user = this.repository.findById(100001).orElse(entity);
assertThat(user.getName()).isEqualTo("pack1");
}
}
輸出結果
圖片
同時控制臺輸出SQL
注意:默認情況下,data JPA測試是事務性的,并在每個測試結束時進行回滾。
3.3 自動配置Redis測試
你可以使用@DataRedisTest來測試Redis應用程序。默認情況下,它會掃描@RedisHash類并配置Spring Data Redis倉庫。當使用@DataRedisTest注解時,不會掃描常規的@Component和@ConfigurationProperties bean。
3.4 自動配置Rest Clients
你可以使用@RestClientTest注解來測試REST客戶端。默認情況下,它會自動配置對Jackson、GSON和Jsonb的支持,配置一個RestTemplateBuilder,并添加對MockRestServiceServer的支持。當使用@RestClientTest注解時,常規的@Component和@ConfigurationProperties bean不會被掃描。示例代碼如下:
@Service
public class RemoteService{
@Resource
private RestTemplate restTemplate ;
public String remote() {
return this.restTemplate.getForObject("http://localhost:8088/demos/cf?ids=pack", String.class) ;
}
}
// 測試
@RestClientTest(RemoteService.class)
// 注意,我們代碼中是直接使用RestTemplate,那么這里必須設置registerRestTemplate=true
@AutoConfigureWebClient(registerRestTemplate=true)
public class SpringBootTest07{
@Resource
private RemoteService service;
@Resource
private MockRestServiceServer server;
@Test
public void getInfo() {
this.server.expect(requestTo("http://localhost:8088/demos/cf?ids=pack")).andRespond(withSuccess("pack", MediaType.TEXT_PLAIN)) ;
String greeting = this.service.remote() ;
assertThat(greeting).isEqualTo("pack") ;
}
}
輸出結果
成功
3.5 TestRestTemplate
TestRestTemplate是Spring的RestTemplate的一個方便的替代方案,它在集成測試中很有用。您可以獲得一個普通的模板,或者發送基本HTTP身份驗證(帶有用戶名和密碼)的模板。無論哪種情況,模板都是容錯的。這意味著它以一種測試友好的方式運行,不會對4xx和5xx錯誤拋出異常。相反,可以通過返回的ResponseEntity及其狀態代碼來檢測此類錯誤。
TestRestTemplate可以直接在集成測試中實例化,如下例所示:
public class SpringBootTest08 {
private final TestRestTemplate template = new TestRestTemplate();
@Test
public void testRequest(){
ResponseEntity<String> response = this.template.getForEntity("http://localhost:8088/demos/cf?ids=123pack", String.class) ;
assertThat(response.getStatusCode().is2xxSuccessful()) ;
assertThat(response.getBody()).isEqualTo("pack") ;
}
}
輸出結果
圖片
我們也可以在WebEnvironment中使用@SpringBootTest注解。RANDOM_PORT或WebEnvironment。你可以注入一個完全配置好的TestRestTemplate并開始使用它。
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SpringBootTest09{
@Resource
private TestRestTemplate template;
@Test
public void testRequest(){
ResponseEntity<String> response = this.template.getForEntity("http://localhost:8088/demos/cf?ids=123pack",
String.class) ;
assertThat(response.getStatusCode().is2xxSuccessful()) ;
assertThat(response.getBody()).isEqualTo("\"123pack\"") ;
}
@TestConfiguration(proxyBeanMethods = false)
static class RestTemplateBuilderConfiguration{
@Bean
RestTemplateBuilder restTemplateBuilder(){
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(1))
.setReadTimeout(Duration.ofSeconds(1)) ;
}
}
}