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

1. 概要

軟件測試是一個應用軟件質量的保證。java開發者開發接口每每忽視接口單元測試。做爲java開發若是會Mock單元測試,那麼你的bug量將會大大下降。spring提供test測試模塊,因此如今小胖哥帶你來玩下springboot下的Mock單元測試,咱們將對controller,service 的單元測試進行實戰操做。java

2. 依賴引入

​​

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

按照上面引入依賴並且scope爲test。該依賴提供了一下類庫git

  • JUnit 4: 目前最強大的java應用單元測試框架
  • Spring Test & Spring Boot Test: Spring Boot 集成測試支持.
  • AssertJ: 一個java斷言庫,提供測試斷言支持.
  • Hamcrest: 對象匹配斷言和約束組件.
  • Mockito: 知名 Java mock 模擬框架.
  • JSONassert: JSON斷言庫.
  • JsonPath: JSON XPath 操做類庫.

以上都是在單元測試中常常接觸的類庫。有時間你最好研究一下。web

3. 配置測試環境

一個Spring Boot 應用程序是一個Spring ApplicationContext ,通常測試不會超出這個範圍。
測試框架提供一個@SpringBootTest註解來提供SpringBoot單元測試環境支持。你使用的JUnit版本若是是JUnit 4不要忘記在測試類上添加@RunWith(SpringRunner.class)JUnit 5就不須要了。默認狀況下,@SpringBootTest不會啓動服務器。您可使用其 webEnvironment 屬性進一步優化測試的運行方式,webEnvironment 相關講解:spring

  • 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環境時,是不會回滾的。這一點特別重要,請確保不會在生產發佈測試中寫入髒數據。json

4. 編寫測試類測試你的api

言歸正傳,首先咱們編寫了一個 BookService 做爲Service 層
​​api

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支持以達到測試後回滾的目的。springboot

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下對應的類路徑 編寫本身的測試類app

​​

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

相關文章
相關標籤/搜索