Spring Boot項目中使用Mockito
- 2019 年 10 月 27 日
- 筆記
本文首發於個人網站:Spring Boot項目中使用Mockito
Spring Boot可以和大部分流行的測試框架協同工作:通過Spring JUnit創建單元測試;生成測試數據初始化數據庫用於測試;Spring Boot可以跟BDD(Behavier Driven Development)工具、Cucumber和Spock協同工作,對應用程序進行測試。
進行軟件開發的時候,我們會寫很多代碼,不過,再過六個月(甚至一年以上)你知道自己的代碼怎麼運作么?通過測試(單元測試、集成測試、接口測試)可以保證系統的可維護性,當我們修改了某些代碼時,通過回歸測試可以檢查是否引入了新的bug。總得來說,測試讓系統不再是一個黑盒子,讓開發人員確認系統可用。
在web應用程序中,對Controller層的測試一般有兩種方法:(1)發送http請求;(2)模擬http請求對象。第一種方法需要配置回歸環境,通過修改代碼統計的策略來計算覆蓋率;第二種方法是比較正規的思路,但是在我目前經歷過的項目中用得不多,今天總結下如何用Mock對象測試Controller層的代碼。
在之前的幾篇文章中,我們都使用bookpub這個應用程序作為例子,今天也不例外,準備測試它提供的RESTful接口是否能返回正確的響應數據。這種測試不同於單元測試,需要為之初始化完整的應用程序上下文、所有的spring bean都織入以及數據庫中需要有測試數據,一般來說這種測試稱之為集成測試或者接口測試。
實戰
通過spirng.io新建的Spring Boot項目提供了一個空的測試文件——BookPubApplicationTest.java,內容是:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = BookPubApplication.class) public class BookPubApplicationTests { @Test public void contextLoads() { } }
- 在pom文件中增加spring-boot-starter-test依賴,添加jsonPath依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> </dependency>
- 在BookPubApplicationTest中添加測試用例
package com.test.bookpub; import com.test.bookpub.domain.Book; import com.test.bookpub.repository.BookRepository; import org.junit.Before;import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; import org.springframework.boot.test.WebIntegrationTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.WebApplicationContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = BookPubApplication.class) @WebIntegrationTest("server.port:0") public class BookPubApplicationTests { @Autowired private WebApplicationContext context; @Autowired private BookRepository bookRepository; @Value("${local.server.port}") private int port; private MockMvc mockMvc; private RestTemplate restTemplate = new TestRestTemplate(); @Before public void setupMockMvc() { mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void contextLoads() { assertEquals(1, bookRepository.count()); } @Test public void webappBookIsbnApi() { Book book = restTemplate.getForObject("http://localhost:" + port +"/books/9876-5432-1111", Book.class); assertNotNull(book); assertEquals("中文測試", book.getPublisher().getName()); } @Test public void webappPublisherApi() throws Exception { //MockHttpServletRequestBuilder.accept方法是設置客戶端可識別的內容類型 //MockHttpServletRequestBuilder.contentType,設置請求頭中的Content-Type字段,表示請求體的內容類型 mockMvc.perform(get("/publishers/1") .accept(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(content().string(containsString("中文測試"))) .andExpect(jsonPath("$.name").value("中文測試")); } }
- spring boot項目的代碼覆蓋率
使用cobertura,參考項目的github地址:spring boot template
# To create test coverage reports (in target/site/cobertura) mvn clean cobertura:cobertura test
分析
首先分析在BookPubApplicationTests類中用到的註解:
- @RunWith(SpringJUnit4ClassRunner.class),這是JUnit的註解,通過這個註解讓SpringJUnit4ClassRunner這個類提供Spring測試上下文。
- @SpringApplicationConfiguration(classes = BookPubApplication.class),這是Spring Boot註解,為了進行集成測試,需要通過這個註解加載和配置Spring應用上下文。這是一個元註解(meta-annoation),它包含了@ContextConfiguration( loader = SpringApplicationContextLoader.class)這個註解,測試框架通過這個註解使用Spring Boot框架的SpringApplicationContextLoader加載器創建應用上下文。
- @WebIntegrationTest("server.port:0"),這個註解表示當前的測試是集成測試(integration test),因此需要初始化完整的上下文並啟動應用程序。這個註解一般和@SpringApplicationConfiguration一起出現。server.port:0指的是讓Spring Boot在隨機端口上啟動Tomcat服務,隨後在測試中程序通過@Value("${local.server.port}")獲得這個端口號,並賦值給port變量。當在Jenkins或其他持續集成服務器上運行測試程序時,這種隨機獲取端口的能力可以提供測試程序的並行性。
了解完測試類的註解,再看看測試類的內部。由於這是Spring Boot的測試,因此我們可通過@Autowired註解織入任何由Spring管理的對象,或者是通過@Value設置指定的環境變量的值。在現在這個測試類中,我們定義了WebApplicationContext和BookRepository對象。
每個測試用例用@Test註解修飾。在第一個測試用例——contextLoads()方法中,我僅僅需要確認BookRepository連接已經建立,並且數據庫中已經包含了對應的測試數據。
第二個測試用例用來測試我們提供的RESTful URL——通過ISBN查詢一本書,即「/books/{isbn}」。在這個測試用例中我們使用TestRestTemplate對象發起RESTful請求。
第三個測試用例中展示了如何通過MockMvc對象實現跟第二個測試類似的功能。Spring測試框架提供MockMvc對象,可以在不需要客戶端-服務端請求的情況下進行MVC測試,完全在服務端這邊就可以執行Controller的請求,跟啟動了測試服務器一樣。
測試開始之前需要建立測試環境,setup方法被@Before修飾。通過MockMvcBuilders工具,使用WebApplicationContext對象作為參數,創建一個MockMvc對象。
MockMvc對象提供一組工具函數用來執行assert判斷,都是針對web請求的判斷。這組工具的使用方式是函數的鏈式調用,允許程序員將多個測試用例鏈接在一起,並進行多個判斷。在這個例子中我們用到下面的一些工具函數:
- perform(get(…))建立web請求。在我們的第三個用例中,通過MockMvcRequestBuilder執行GET請求。
- andExpect(…)可以在perform(…)函數調用後多次調用,表示對多個條件的判斷,這個函數的參數類型是ResultMatcher接口,在MockMvcResultMatchers這這個類中提供了很多返回ResultMatcher接口的工具函數。這個函數使得可以檢測同一個web請求的多個方面,包括HTTP響應狀態碼(response status),響應的內容類型(content type),會話中存放的值,檢驗重定向、model或者header的內容等等。這裡需要通過第三方庫json-path檢測JSON格式的響應數據:檢查json數據包含正確的元素類型和對應的值,例如jsonPath("$.name").value("中文測試")用於檢查在根目錄下有一個名為name的節點,並且該節點對應的值是「中文測試」。
一個字符亂碼問題
-
問題描述:通過spring-boot-starter-data-rest建立的repository,取出的漢字是亂碼。
-
分析:使用postman和httpie驗證都沒問題,說明是Mockmvc的測試用例寫得不對,應該主動設置客戶端如何解析HTTP響應,用get.accept方法設置客戶端可識別的內容類型,修改後的測試用例如下:
@Test public void webappPublisherApi() throws Exception { //MockHttpServletRequestBuilder.accept方法是設置客戶端可識別的內容類型 //MockHttpServletRequestBuilder.contentType,設置請求頭中的Content-Type字段,表示請求體的內容類型 mockMvc.perform(get("/publishers/1") .accept(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(content().string(containsString("中文測試"))) .andExpect(jsonPath("$.name").value("中文測試")); }
參考資料
- 基於Spring-WS的Restful API的集成測試
- J2EE要懂的小事—圖解HTTP協議
- Integration Testing a Spring Boot Application
- spring boot project template
Spring Boot 1.x系列
- Spring Boot的自動配置、Command-line-Runner
- 了解Spring Boot的自動配置
- Spring Boot的@PropertySource註解在整合Redis中的使用
- Spring Boot項目中如何定製HTTP消息轉換器
- Spring Boot整合Mongodb提供Restful接口
- Spring中bean的scope
- Spring Boot項目中使用事件派發器模式
- Spring Boot提供RESTful接口時的錯誤處理實踐
- Spring Boot實戰之定製自己的starter
- Spring Boot項目如何同時支持HTTP和HTTPS協議
- 自定義的Spring Boot starter如何設置自動配置註解
本號專註於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。