之前發了SpringBoot 單元測試的博客, https://www.cnblogs.com/harrychinese/p/springboot_unittesting.html , 內容較少, 現在補齊SpringBoot單元測試的主要知識點.
測試有很多種, 有單元測試 、集成測試 、冒煙測試 、回歸測試 、端到端測試 、功能測試。 它們作用不同, 但又有所重疊.
1. 單元測試: [責任人: 開發人員]針對 class 級別的測試, 針對一個類, 寫一個測試類. 在項目中, class 之間依賴調用是很常見的事情, 如果要測試的 classA, 又調用了classB, 這時候就沒有遵守 "在一個class范圍內測試", 自然不算單元測試, 應歸為集成測試了. 不過這時候, 我們可以使用 mock 技術模擬被依賴的 classB, 這樣整個測試又聚焦在classA 自身的功能, 所以又變回為單元測試.
2. 集成測試: [責任人: 開發人員]是單元測試在粒度上更進一步, 測試不同class之間的組合功能.
3. 冒煙測試: 可以理解為預測試, 一旦預測試失敗(發現重大bug), 就直接停止測試, 打回給開發人員.
4. 回歸測試: 重跑原有測試用例.
=====================
spring-boot-starter-test 依賴包括:
=====================
1. JUnit, 測試框架
2. Spring Test & Spring Boot Test,
3. Mockito, Mock框架, 可以模擬任何 Spring 管理的 bean. 在單元測試過程中, 使用 @MockBean 注解可以將一個業務Service 的模擬器注入到我們要測試的SpringBoot程序中, 而不是真正的業務Service實現類.
4. JSONassert,可以對json內容進行斷言.
5. JsonPath, 提供類類似於 XPath 的 json 路徑訪問形式.
=====================
JUnit 方法級別的注解
=====================
JUnit 4.x 全面引入了注解方式進行單元測試, 測試相關的方法都必須是 public .
@BeforeClass -- 用來修飾static方法, 在測試類加載時執行.
@AfterClass -- 用來修飾static方法, 在測試類結束時執行.
@Before -- 用來修飾實例方法, 在每個測試方法之前執行, 比如用來初始化一個 MockMvc 對象.
@After -- 用來修飾實例方法, 在每個測試方法之后執行.
@Test -- 用來修飾實例方法, 是測試用例方法.
@Transactional --和@Test一起搭配使用, 作用是自動回滾事務, 防止單元測試污染測試數據庫.
@Rollback(false) --和@Transactional/@Test一起搭配使用, 用來提交事務.
=====================
JUnit 類級別注解
=====================
1. @RunWith -- 對於SpringBoot程序, 使用 @RunWith(SpringRunner.class)
2. @SuiteClasses -- 在IDE中一次只能執行一個測試類, 對於一個大型項目, 肯定不止一個測試類, 如何一次執行多個測試類呢? 答案就是使用@SuiteClasses. 該注解能將多個測試class歸並到一個新的測試class, 新的測試類往往是一個空類, 需要加上 @RunWith(Suite.class), 代碼如下:
@RunWith(Suite.class) @SuiteClasses({Test1.class, Test2.class}) public class TestSuiteMain{ // 空類, 但它會執行 TestSuite1 和 TestSuite2 的所有測試方法 }
=====================
JUnit 斷言
=====================
assertEquals() --是否相等
assertSame() --判斷是否是同一個對象
assertTrue()
assertFalse()
assertNotNull()
assertArrayEquals()
assertThat() -- JUnit4.4 新增一個全能的斷言
=====================
Spring test 提供的類級別注解
=====================
Spring 提供了一系列的測試方式注解, 我們可以按需選擇它們, @SpringBootTest 主要用於集成測試, 其他注解用於單元測試.
(1). @SpringBootTest, 該注解負責掃描配置來構建測試用的Spring上下文環境. 將在啟動單元測試之前, 首先按照包名逐級向上找 SpringBoot 業務系統的入口, 並啟動該SpringBoot 程序. 所以啟動速度較慢, 用於集成測試.
如果我們需要要注入 WebApplicationContext 和 MockMvc bean, 需要在類上再加一個 @AutoConfigureMockMvc 注解. 完整示例見下面的 MyControllerTests 代碼.
(2). @WebMvcTest, 該注解僅僅掃描並初始化與 Controller 相關的bean, 但一般的@Component並不會被掃描, 另外也不啟動一個 http server, 所以能加快測試進程. @WebMvcTest 還可以傳入 Controller 類清單參數, 進一步縮小容器中bean的數量.
@WebMvcTest 還會自動實例化一個 WebApplicationContext 和 MockMvc bean, 供我們在測試用例中使用.
(3). @RestClientTest 和 @WebMvcTest 類似, 用來測試基於Rest API的Service 類, 該注解會自動實例化一個MockRestServiceServer bean用來模擬遠程的 Restful 服務.
@RunWith(SpringRunner.class) @RestClientTest(RemoteVehicleDetailsService.class) public class ExampleRestClientTest { @Autowired private RemoteVehicleDetailsService service; @Autowired private MockRestServiceServer server; @Test public void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() throws Exception { this.server.expect(requestTo("/greet/details")) .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); String greeting = this.service.callRestService(); assertThat(greeting).isEqualTo("hello"); } }
(4). 除了上面幾個類型, 還有 @MybatisTest 、 @JsonTest 、 @DataRedisTest 等等.
=====================
MockMvc 基本使用
=====================
MockMvc bean, 使用該對象的 perform() 可以發出模擬web請求, 並完成期望檢查, 當然期望檢查還可以使用JUnit 的 Assert. 雖然 MockMvc 是一個模擬的Http調用, 但它確實真實調用了視圖函數, 並以 http 協議封裝了結果, 所以可以用來做單元測試.
@RestController @RequestMapping("/") class MyController { @GetMapping("/") public String index() throws SQLException { return "index"; } }
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class MyControllerTests { @Autowired private MockMvc mvc; /** * 期望調用成功, 並打印完整結果. * * @throws Exception */ @Test public void indexPrint() throws Exception { //@formatter:off mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); //@formatter:on } /** * 期望調用成功, 並驗證該視圖函數的返回內容是否為字符串 index * * @throws Exception */ @Test public void index() throws Exception { String expected = "index"; //@formatter:off mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(expected)); //@formatter:on } }
=====================
Mockito基本使用
=====================
頂層class經常會依賴一些底層class, 要對頂層class做單元測試, 就需要使用 mock 技術來代替底層class.
1. @MockBean 注解, 該 Mockito 注解可注入頂層對象, 它和 @Autowired 用法含義差不多, 但並不會注入真實的底層實現類.
2. 使用 Mockito.when() 來模擬底層類的行為:
Mockito.when(methodCall).thenReturn(expected) 使用窮舉法來模擬, 這個方式簡單但最常用, 我們寫測試用例基本上也是按照case by case 設計有限的幾個測試用例.
Mockito.when(methodCall).thenAnswer(匿名對象) 通過when()傳入的參數動態模擬, 該方式很強大, 但並不常用, 因為我們在測試用例中, 沒有必要再和被模擬對象一樣實現一套業務邏輯.
下面是被測試的代碼, MyServiceConsumer 依賴一個 MyService 接口.
interface MyService { public String appendA(String source); } @Service class DefaultMyService implements MyService { @Override public String appendA(String source) { System.out.println("MyServiceConsumer.appendA() called"); return source + "B"; } } @Component class MyServiceConsumer { @Autowired MyService myService; public String getServiceName(String source) { return myService.appendA(source); } }
下面是MyServiceConsumer 的測試代碼, 我們使用mockito 模擬了一個依賴 MyService 對象.
@RunWith(SpringRunner.class) @SpringBootTest public class MyServiceConsumerTests { @MockBean MyService myService; @Autowired MyServiceConsumer myServiceConsumer; /* * 使用 MockBean 來模擬 MyService 的 appendA() 行為 */ @Before public void Init() { // 方式1: 使用窮舉法模擬, 具體是通過 thenReturn()方法 String source = "MyService1"; Mockito.when(myService.appendA(source)) .thenReturn(source + "A"); // 方式2:使用動態參數形式模擬, 具體是通過 thenAnswer()方法 Mockito.when(myService.appendA(Mockito.anyString())) .thenAnswer(new Answer<String>() { @Override public String answer(InvocationOnMock invocation) throws Throwable { String arg = (String) invocation.getArgument(0); return arg + "A"; } }); } @Test public void getServiceName1() { String source = "MyService1"; String expected = "MyService1A"; String actual = myServiceConsumer.getServiceName(source); Assert.assertEquals("錯誤: getServiceName() 不符合預期", expected, actual); } @Test public void getServiceName2() { String source = "Other"; String expected = "OtherA"; String actual = myServiceConsumer.getServiceName(source); Assert.assertEquals("錯誤: getServiceName() 不符合預期", expected, actual); } }
=====================
參考
=====================
使用@SpringBootTest注解進行單元測試
https://www.cnblogs.com/ywjy/p/9997412.html
Testing in Spring Boot
https://www.baeldung.com/spring-boot-testing
第三十五章:SpringBoot與單元測試的小秘密
https://segmentfault.com/a/1190000011420910
http://tengj.top/2017/12/28/springboot12/
springboot(十二):springboot如何測試打包部署
http://www.ityouknow.com/springboot/2017/05/09/springboot-deploy.html
