1. 概要
軟件測試是一個應用軟件質量的保證。java開發者開發接口往往忽視接口單元測試。作爲java開發如果會Mock單元測試,那麼你的bug量將會大大降低。spring提供test測試模塊,所以現在小胖哥帶你來玩下springboot下的Mock單元測試,我們將對controller,service 的單元測試進行實戰操作。
2. 依賴引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
按照上面引入依賴而且scope爲test。該依賴提供了一下類庫
- JUnit 4: 目前最強大的java應用單元測試框架
- Spring Test & Spring Boot Test: Spring Boot 集成測試支持.
- AssertJ: 一個java斷言庫,提供測試斷言支持.
- Hamcrest: 對象匹配斷言和約束組件.
- Mockito: 知名 Java mock 模擬框架.
- JSONassert: JSON斷言庫.
- JsonPath: JSON XPath 操作類庫.
以上都是在單元測試中經常接觸的類庫。有時間你最好研究一下。
3. 配置測試環境
一個Spring Boot 應用程序是一個Spring ApplicationContext
,一般測試不會超出這個範圍。
測試框架提供一個@SpringBootTest
註解來提供SpringBoot單元測試環境支持。你使用的JUnit版本如果是JUnit 4
不要忘記在測試類上添加@RunWith(SpringRunner.class)
,JUnit 5
就不需要了。默認情況下,@SpringBootTest不會啓動服務器。您可以使用其 webEnvironment
屬性進一步優化測試的運行方式,webEnvironment
相關講解:
-
MOCK
(默認):加載Web ApplicationContext並提供模擬Web環境。該選擇下不會啓動嵌入式服務器。如果類路徑上沒有Web環境,將創建常規非Web的ApplicationContext
。你可以配合@AutoConfigureMockMvc
或@AutoConfigureWebTestClient
模擬的Web應用程序。 -
RANDOM_PORT
:加載WebServerApplicationContext
並提供真實的Web環境,啓用的是隨機web容器端口。 -
DEFINED_PORT
:加載WebServerApplicationContext
並提供真實的Web環境 和RANDOM_PORT
不同的是啓用你激活的SpringBoot應用端口,通常都聲明在application.yml
配置文件中。 -
NONE
:通過SpringApplication
加載一個ApplicationContext
。但不提供 任何 Web環境(無論是Mock或其他)。
注意事項:如果你的測試帶有@Transactional
註解時,默認情況下每個測試方法執行完就會回滾事務。但是當你的 webEnvironment
設置爲RANDOM_PORT
或者 DEFINED_PORT
,也就是隱式地提供了一個真實的servlet web環境時,是不會回滾的。這一點特別重要,請確保不會在生產發佈測試中寫入髒數據。
4. 編寫測試類測試你的api
言歸正傳,首先我們編寫了一個 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);
}
其實現類如下,爲了簡單明瞭沒有測試持久層,如果持久層需要測試注意增刪改需要Spring事務註解@Transactional
支持以達到測試後回滾的目的。
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);
}
}
我們在Spring Boot maven項目的單元測試包 test
下對應的類路徑 編寫自己的測試類
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 開始模擬
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);
}
}
測試類前兩個註解不用說,第三個註解@AutoConfigureMockMvc
可能你們很陌生。這個是用來開啓Mock Mvc測試的自動化配置的。
然後我們編寫一個測試方法bookApiTest()
來測試BookApi#getBook(String title)
接口。
邏輯是 MockMvc
執行一個模擬的get請求然後期望結果是expect
Json字符串並且將相應對象打印了出來(下圖1標識)。一旦請求不通過將拋出java.lang.AssertionError
錯誤, 會把期望值(Expected
)跟實際值打印出來(下圖2標識)。如果跟預期相同只會出現下圖1。
5. 測試打樁
有個很常見的情形,在開發中有可能你調用的其他服務沒有開發完,比如你有個短信發送接口還在辦理短信接口手續,但是你還需要短信接口來進行測試。你可以通過@MockBean
構建一個抽象接口的實現。拿上面的BookService
來說,假如其實現類邏輯還沒有確定,我們可以通過規定其入參以及對應的返回值來模擬這個bean的邏輯,或者根據某個情形下進行某個路由操作的選擇(如果入參是A則結果爲B,如果爲C則D)。這種模擬也被成爲測試打樁。 這裏我們會用到Mockito
測試場景描述如下:
- 指定打樁對象的返回值
- 判斷某個打樁對象的某個方法被調用及調用的次數
- 指定打樁對象拋出某個特定異常
一般有以下幾種組合:
-
do/when:包括
doThrow(…).when(…)
/doReturn(…).when(…)
/doAnswer(…).when(…)
-
given/will:包括
given(…).willReturn(…)
/given(…).willAnswer(…)
-
when/then: 包括
when(…).thenReturn(…)
/when(…).thenAnswer(…)
其他都好理解,着重介紹一下Answer
, Answer
正是爲了解決如果入參是A則結果爲B,如果爲C則D這種路由操作的。接下來我們實操一下 ,跟最開始基本一樣,只是更換成@MockBean
然後利用Mockito
編寫打樁方法void bookServiceMockBean(String title)
,模擬上面BookServiceImpl
實現類。不過模擬的bean每次測試完都會自動重置。而且不能用於模擬在應用程序上下文刷新期間運行的bean的行爲。
然後把這個方法注入controller 測試方法就可以測試了。
6. 其他
內置的assertj
也是常用的斷言,api非常友好,這裏也通過bookServiceTest()
簡單演示了一下
7. 總結
本文中實現了一些簡單的Spring Boot啓用集成測試。 對測試環境的搭建,測試代碼的編寫進行了實戰操作,基本能滿足日常開發測試需要,相信你能從本文學到不少東西。
相關的講解代碼可以從gitee獲取。
也可通過我 個人博客 及時獲取更多的乾貨分享。
關注公衆號:Felordcn獲取更多資訊