1. 程式人生 > >補習系列-springboot 單元測試之道

補習系列-springboot 單元測試之道

try 精彩 一次 run rest spec ner hat ltm

目錄

  • 目標
  • 一、About 單元測試
  • 二、About Junit
  • 三、SpringBoot-單元測試
    • 項目依賴
    • 測試樣例
  • 四、Mock測試
  • 五、最後

目標

  1. 了解 單元測試的背景
  2. 了解如何 利用 springboot 實現接口的測試
  3. 了解如何 利用 mokito 做代碼的 mock

一、About 單元測試

單元測試其實是一種廉價的技術,是由開發者創建運行測試代碼,用於對程序模塊(軟件設計的最小單位)進行正確性檢驗的一種做法。
而所謂的最小單元,就是指應用的最小可測試部件。 在面向對象領域,最小單元對應於類的某個成員方法。

通常意義的單元測試會用於驗證某場景、某條件下某方法的行為結果,舉個例子:

我想驗證

    Equals 方法,在兩個對象類型不一致時應該返回 false

單元測試的初衷,是對各個相互獨立,互不影響的基本單元基線測試,以此來保證核心代碼的質量。

每一段單元測試代碼,都一定會包含幾個部分

  • Arrange
    用於初始化一些被測試方法需要的參數或依賴的對象。

  • Act方法
    用於調用被測方法進行測試。

  • Assert
    用於驗證測試方法是否按期望執行或者結果是否符合期望值

See !並不是很復雜,可是大多數開發者並不喜歡做單元測試。
而且,有一個現象很有意思,水平越高的程序員,越不喜歡寫測試代碼,why?

“ 因為單元測試,主要是用來防低級程序員挖坑的啊 ”

這句話不是我說的,但卻代表了相當一部分程序員的心聲..

那麽,單元測試到底要不要做,並不是本文要討論的問題。
建議大家閱讀下 《單元測試之道-Java版本》 (程序員修煉三部曲系列)這邊書,看完後再做出自己的理解。

為了測試一座橋梁,不應該只在晴朗的天氣,開一輛汽車從橋中間穿過,就認為已經完成了對橋梁的測試

二、About Junit

接下來,要說一說 Junit框架,這個是最流行的Java 單元測試框架。
Junit 創建者是 Kent Beck和Erich Gamma,自其出現以來,Junit 生態圈已經非常龐大。
大量的應用程序、開發框架都以 Junit 作為標準的的基礎測試組件,這當然也包括 Spring系列的框架。

一個典型的Junit單元測試類:


class StandardTests {

    @BeforeClass
    static void initAll() {
    }

    @Before
    void init() {
    }

    @Test
    void justTest() {
    ...
    assertTrue(...)
    }

    
    @After
    void tearDown() {
    }

    @AfterClass
    static void tearDownAll() {
    }

}

說明

要點 說明
@BeforeClass 在當前類測試之前執行
@Before 在每個測試方法之前執行
@Test 聲明測試方法
@After 在每個測試方法之後執行
@AfterClass 在當前類測試之後執行

上面的註解還是比較容易理解的,需要註意的只是 @BeforeClass 和 @Before,前者是一個靜態方法,
會在整個測試用例類開始前執行,僅一次; 而後者則是在方法測試之前觸發,可能會執行多次。

當前最新的版本是Junit 5 ,有興趣的可以看看 https://junit.org/junit5 官網的介紹

為了更清晰的理解Junit 是怎麽運作,下面展示一個源碼片段:


    public void runBare() throws Throwable {
        Throwable exception = null;
        setUp();
        try {
            runTest();
        } catch (Throwable running) {
            exception = running;
        } finally {
            try {
                tearDown();
            } catch (Throwable tearingDown) {
                if (exception == null) exception = tearingDown;
            }
        }
        if (exception != null) throw exception;
    }

這是早期版本的TestCase類其中的一段實現,與我們所說的思路是基本一致的。
然而,基於註解的實現是由 Junit4提供的,在有興趣的話可以深入看看源碼。

關鍵詞
TestCase、JUnit4TestAdapter、BlockJUnit4ClassRunner

三、SpringBoot-單元測試

SpringBoot 提供了 spring-boot-starter-test 用於實現單元測試。

項目依賴

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <version>${spring-boot.version}</version>
</dependency>

測試樣例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoBoot.class)
public class RestApiTest {

    private MockMvc mockMvc;

    private ObjectMapper mapper = new ObjectMapper();

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private RestDataManager dataManager;

    private static final String CUSTOMER = "LiLei";
    private Pet polly;
    private Pet badboy;

    @Before
    public void setupMockMvc() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
        initData();
    }

    private void initData() {
        // 清除原有寵物信息
        dataManager.clearPets(CUSTOMER);

        // 添加新的寵物信息
        polly = new Pet();
        polly.setType("Bird");
        polly.setName("Polly");
        polly.setDescription("the rapid speaker");

        dataManager.addPet(CUSTOMER, polly);

        badboy = new Pet();
        badboy.setType("Dog");
        badboy.setName("BadBoy");
        polly.setDescription("the monster");

        dataManager.addPet(CUSTOMER, badboy);
    }

    @Test
    public void testGet() throws Exception {

        mockMvc.perform(MockMvcRequestBuilders.get("/rest/pets/{customer}/{petId}", 
                CUSTOMER, polly.getPetId()))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content()
                       .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(MockMvcResultMatchers.content()
                       .json(mapper.writeValueAsString(polly)))
                .andDo(MockMvcResultHandlers.print());

    }
}

說明

SpringRunner繼承於SpringJUnit4ClassRunner,這是Spring框架基於Junit實現的基礎類。
如果還記得前面提到的 BlockJUnit4ClassRunner,應該不難猜到,Spring 的實現類集成了該類。

那麽,SpringRunner 做了什麽? 什麽也沒有,只是一個名稱的修正而已(論命名的重要性)

@SpringBootTest的作用

其代碼註釋如下:

Annotation that can be specified on a test class that runs Spring Boot based tests. 
Provides the following features over and above the regular Spring TestContext Framework: 

1. Uses SpringBootContextLoader as the default ContextLoader when no specific @ContextConfiguration(loader=...) is defined. 
2. Automatically searches for a @SpringBootConfiguration when nested @Configuration is not used, and no explicit classes are specified. 
3. Allows custom Environment properties to be defined using the properties attribute. 
4. Provides support for different webEnvironment modes, including the ability to start a fully running container listening on a defined or random port. 
5. Registers a TestRestTemplate bean for use in web tests that are using a fully running container. 

要點

  1. 默認會使用SpringBootContextLoader類用於上下文加載,
    這個類將會使用所配置的SpringBootApplication實體類作為入口,加載配置並初始化Spring上下文環境;

  2. 可以支持自定義的配置,通過 Environment 屬性設置;
  3. 支持不同的 web 環境模式,可以是固定端口、隨機端口、無端口幾種模式。

關鍵詞
SpringRunner、SpringBootTest、SpringBootContextLoader

四、Mock測試

Mock 測試的使用場景在於,被測試模塊(方法)依賴於外部系統(web服務、中間件或是數據庫)時。
我們需要提供一種快速驗證本地實現邏輯的策略,那就是 Mock,也稱為打樁。

技術分享圖片

如上圖,A 模塊依賴於 B 模塊,在 B 模塊不可達的時候,我們對 依賴接口進行了 Mock。
這樣在執行測試時,不需要真實的 B 模塊便可完成測試。

下面我們要用到的 Mock 組件叫 Mockito
springboot-starter-test 自帶了對於 mockito 的依賴,下面看一段代碼:

    @Before
    public void setupMockMvc() throws Exception {

        // 啟用mock
       
    @Before
    public void setupMockMvc() throws Exception {

        // 啟用mock
        MockitoAnnotations.initMocks(this);

        polly = new Pet();
        polly.setType("Bird");
        polly.setName("Polly");
        polly.setDescription("the rapid speaker");

        lilei = new Customer();
        lilei.setName(CUSTOMER);

        // 設置mock接口
        Mockito.when(dataManager.getPets(Mockito.isA(String.class))).thenReturn(Arrays.asList(polly));
        Mockito.when(dataManager.getCustomer(Mockito.isA(String.class))).thenReturn(lilei);

        // 使用standaloneSetup,指定controller
        // 由於不通過webappliationContext初始化,許多配置不會自動完成,此外bean的初始化方法也不會執行
        mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setMessageConverters(new MappingJackson2HttpMessageConverter()).build();
    }

        polly = new Pet();
        polly.setType("Bird");
        polly.setName("Polly");
        polly.setDescription("the rapid speaker");

        lilei = new Customer();
        lilei.setName(CUSTOMER);

        // 設置mock接口
        Mockito.when(dataManager.getPets(Mockito.isA(String.class))).thenReturn(Arrays.asList(polly));
        Mockito.when(dataManager.getCustomer(Mockito.isA(String.class))).thenReturn(lilei);

        // 使用standaloneSetup,指定controller
        // 由於不通過webappliationContext初始化,許多配置不會自動完成,此外bean的初始化方法也不會執行
        mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setMessageConverters(new MappingJackson2HttpMessageConverter()).build();
    }

看到了嗎,利用 Mockito 可以實現你想要的 Mock效果,如下:

Mockito.when( somemethod ).thenReturn( some thing to return);

然而,在進行 mock 方法時,需要使用 standaloneSetup 的模式,
否則 mockito 無法工作。

mockMvc = MockMvcBuilders.standaloneSetup(controller)..

關鍵詞
Mockito、MockMvcBuilders

五、最後

細心的讀者會發現,前面講了單元測試的對象,是指軟件設計的最小單位(方法),可是為什麽到了 SpringBoot 的部分時
卻都是對於API(Controller層)的測試呢? 到底我們的單元測試應該針對內部實現的某個單元,比如 DAO/Service方法,還是針對接口(API Interface)?

筆者認為,這點並沒有絕對的好壞之分,關鍵在於取舍。
單元測試是軟件工程領域的概念,而軟件項目是分很多種類型的,比如在早期的軟件工程中,
就有不少的基於C/S架構的程序,這類程序的體積相對龐大,往往需要對大量模塊級的方法進行單元測試;

現如今的微服務體系架構中,對於各個子系統來說,API(作為契約)是必須進行測試的。
對於某服務的單元測試,選擇 Controller 還是 Service層,取決於你的成本效益考慮,

而目前來看,結合敏捷化的 TDD實踐、 通過單元測試進行 API測試 已經是一種主流做法。

歡迎繼續關註"美碼師的補習系列-springboot篇" ,期待更多精彩內容^-^

補習系列-springboot 單元測試之道