Spring Boot 2 實戰:mock測試你的web應用

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

測試場景描述如下:

  1. 指定打樁對象的返回值
  2. 判斷某個打樁對象的某個方法被調用及調用的次數
  3. 指定打樁對象拋出某個特定異常

一般有以下幾種組合:

  • 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獲取更多資訊

個人博客:https://felord.cn

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章