1. 程式人生 > 程式設計 >Spring Boot專案中使用Mockito

Spring Boot專案中使用Mockito

本文首發於個人網站: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複製程式碼

cobertura統計程式碼覆蓋率單擊某個類進去,可以看到詳細資訊

分析

首先分析在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設定指定的環境變數的值。在現在這個測試類中,我們定義了WebApplicationContextBookRepository物件。

每個測試用例用@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,表示請求體的內容型別
    mockMvc.perform(get("/publishers/1")
            .accept(MediaType.APPLICATION_JSON_UTF8))

            .andExpect(status().isOk())
            .andExpect(content().string(containsString("中文測試")))
            .andExpect(jsonPath("$.name").value("中文測試"));
}複製程式碼

參考資料

  1. 基於Spring-WS的Restful API的整合測試
  2. J2EE要懂的小事—圖解HTTP協議
  3. Integration Testing a Spring Boot Application
  4. spring boot project template

Spring Boot 1.x系列

  1. Spring Boot的自動配置、Command-line-Runner
  2. 瞭解Spring Boot的自動配置
  3. Spring Boot的@PropertySource註解在整合Redis中的使用
  4. Spring Boot專案中如何定製HTTP訊息轉換器
  5. Spring Boot整合Mongodb提供Restful介面
  6. Spring中bean的scope
  7. Spring Boot專案中使用事件派發器模式
  8. Spring Boot提供RESTful介面時的錯誤處理實踐
  9. Spring Boot實戰之定製自己的starter
  10. Spring Boot專案如何同時支援HTTP和HTTPS協議
  11. 自定義的Spring Boot starter如何設定自動配置註解

***本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。javaadu