Spring Boot 2 實(shí)戰(zhàn):mock測(cè)試你的web應(yīng)用

1. 概要

軟件測(cè)試是一個(gè)應(yīng)用軟件質(zhì)量的保證。java開(kāi)發(fā)者開(kāi)發(fā)接口往往忽視接口單元測(cè)試庐船。作為java開(kāi)發(fā)如果會(huì)Mock單元測(cè)試银酬,那么你的bug量將會(huì)大大降低。spring提供test測(cè)試模塊筐钟,所以現(xiàn)在小胖哥帶你來(lái)玩下springboot下的Mock單元測(cè)試揩瞪,我們將對(duì)controller,service 的單元測(cè)試進(jìn)行實(shí)戰(zhàn)操作篓冲。

2. 依賴引入

??

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

按照上面引入依賴而且scope為test李破。該依賴提供了一下類庫(kù)

  • JUnit 4: 目前最強(qiáng)大的java應(yīng)用單元測(cè)試框架
  • Spring Test & Spring Boot Test: Spring Boot 集成測(cè)試支持.
  • AssertJ: 一個(gè)java斷言庫(kù),提供測(cè)試斷言支持.
  • Hamcrest: 對(duì)象匹配斷言和約束組件.
  • Mockito: 知名 Java mock 模擬框架.
  • JSONassert: JSON斷言庫(kù).
  • JsonPath: JSON XPath 操作類庫(kù).

以上都是在單元測(cè)試中經(jīng)常接觸的類庫(kù)壹将。有時(shí)間你最好研究一下嗤攻。

3. 配置測(cè)試環(huán)境

一個(gè)Spring Boot 應(yīng)用程序是一個(gè)Spring ApplicationContext ,一般測(cè)試不會(huì)超出這個(gè)范圍。
測(cè)試框架提供一個(gè)@SpringBootTest注解來(lái)提供SpringBoot單元測(cè)試環(huán)境支持诽俯。你使用的JUnit版本如果是JUnit 4不要忘記在測(cè)試類上添加@RunWith(SpringRunner.class)妇菱,JUnit 5就不需要了。默認(rèn)情況下暴区,@SpringBootTest不會(huì)啟動(dòng)服務(wù)器闯团。您可以使用其 webEnvironment 屬性進(jìn)一步優(yōu)化測(cè)試的運(yùn)行方式,webEnvironment 相關(guān)講解:

  • MOCK(默認(rèn)):加載Web ApplicationContext并提供模擬Web環(huán)境仙粱。該選擇下不會(huì)啟動(dòng)嵌入式服務(wù)器房交。如果類路徑上沒(méi)有Web環(huán)境,將創(chuàng)建常規(guī)非Web的 ApplicationContext伐割。你可以配合@AutoConfigureMockMvc@AutoConfigureWebTestClient模擬的Web應(yīng)用程序涌萤。
  • RANDOM_PORT:加載 WebServerApplicationContext 并提供真實(shí)的Web環(huán)境淹遵,啟用的是隨機(jī)web容器端口。
  • DEFINED_PORT:加載 WebServerApplicationContext 并提供真實(shí)的Web環(huán)境 和 RANDOM_PORT 不同的是啟用你激活的SpringBoot應(yīng)用端口负溪,通常都聲明在application.yml配置文件中透揣。
  • NONE:通過(guò)SpringApplication加載一個(gè)ApplicationContext。但不提供 任何 Web環(huán)境(無(wú)論是Mock或其他)川抡。

注意事項(xiàng):如果你的測(cè)試帶有@Transactional注解時(shí)辐真,默認(rèn)情況下每個(gè)測(cè)試方法執(zhí)行完就會(huì)回滾事務(wù)。但是當(dāng)你的 webEnvironment 設(shè)置為RANDOM_PORT或者 DEFINED_PORT崖堤,也就是隱式地提供了一個(gè)真實(shí)的servlet web環(huán)境時(shí)侍咱,是不會(huì)回滾的。這一點(diǎn)特別重要密幔,請(qǐng)確保不會(huì)在生產(chǎn)發(fā)布測(cè)試中寫入臟數(shù)據(jù)楔脯。

4. 編寫測(cè)試類測(cè)試你的api

言歸正傳,首先我們編寫了一個(gè) 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);

}

其實(shí)現(xiàn)類如下胯甩,為了簡(jiǎn)單明了沒(méi)有測(cè)試持久層昧廷,如果持久層需要測(cè)試注意增刪改需要Spring事務(wù)注解@Transactional支持以達(dá)到測(cè)試后回滾的目的。

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);
    }

}

我們?cè)赟pring Boot maven項(xiàng)目的單元測(cè)試包 test下對(duì)應(yīng)的類路徑 編寫自己的測(cè)試類

??

 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 開(kāi)始模擬
         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);
     }
  }

測(cè)試類前兩個(gè)注解不用說(shuō)偎箫,第三個(gè)注解@AutoConfigureMockMvc可能你們很陌生木柬。這個(gè)是用來(lái)開(kāi)啟Mock Mvc測(cè)試的自動(dòng)化配置的。

然后我們編寫一個(gè)測(cè)試方法bookApiTest()來(lái)測(cè)試BookApi#getBook(String title)接口淹办。
??

邏輯是 MockMvc 執(zhí)行一個(gè)模擬的get請(qǐng)求然后期望結(jié)果是expect Json字符串并且將相應(yīng)對(duì)象打印了出來(lái)(下圖1標(biāo)識(shí))眉枕。一旦請(qǐng)求不通過(guò)將拋出java.lang.AssertionError錯(cuò)誤, 會(huì)把期望值(Expected)跟實(shí)際值打印出來(lái)(下圖2標(biāo)識(shí))。如果跟預(yù)期相同只會(huì)出現(xiàn)下圖1怜森。


??

5. 測(cè)試打樁

有個(gè)很常見(jiàn)的情形速挑,在開(kāi)發(fā)中有可能你調(diào)用的其他服務(wù)沒(méi)有開(kāi)發(fā)完,比如你有個(gè)短信發(fā)送接口還在辦理短信接口手續(xù)副硅,但是你還需要短信接口來(lái)進(jìn)行測(cè)試姥宝。你可以通過(guò)@MockBean 構(gòu)建一個(gè)抽象接口的實(shí)現(xiàn)。拿上面的BookService來(lái)說(shuō)想许,假如其實(shí)現(xiàn)類邏輯還沒(méi)有確定伶授,我們可以通過(guò)規(guī)定其入?yún)⒁约皩?duì)應(yīng)的返回值來(lái)模擬這個(gè)bean的邏輯断序,或者根據(jù)某個(gè)情形下進(jìn)行某個(gè)路由操作的選擇(如果入?yún)⑹茿則結(jié)果為B流纹,如果為C則D)。這種模擬也被成為測(cè)試打樁违诗。 這里我們會(huì)用到Mockito

測(cè)試場(chǎng)景描述如下:

  1. 指定打樁對(duì)象的返回值
  2. 判斷某個(gè)打樁對(duì)象的某個(gè)方法被調(diào)用及調(diào)用的次數(shù)
  3. 指定打樁對(duì)象拋出某個(gè)特定異常

一般有以下幾種組合:

  • do/when:包括doThrow(…).when(…) / doReturn(…).when(…) / doAnswer(…).when(…)
  • given/will:包括given(…).willReturn(…) / given(…).willAnswer(…)
  • when/then: 包括when(…).thenReturn(…) / when(…).thenAnswer(…)

其他都好理解漱凝,著重介紹一下Answer , Answer 正是為了解決如果入?yún)⑹茿則結(jié)果為B,如果為C則D這種路由操作的诸迟。接下來(lái)我們實(shí)操一下 ,跟最開(kāi)始基本一樣茸炒,只是更換成@MockBean

??

然后利用Mockito編寫打樁方法void bookServiceMockBean(String title)愕乎,模擬上面BookServiceImpl 實(shí)現(xiàn)類。不過(guò)模擬的bean每次測(cè)試完都會(huì)自動(dòng)重置壁公。而且不能用于模擬在應(yīng)用程序上下文刷新期間運(yùn)行的bean的行為感论。

??

然后把這個(gè)方法注入controller 測(cè)試方法就可以測(cè)試了。

??

6. 其他

內(nèi)置的assertj也是常用的斷言紊册,api非常友好比肄,這里也通過(guò)bookServiceTest()簡(jiǎn)單演示了一下

??

7. 總結(jié)

本文中實(shí)現(xiàn)了一些簡(jiǎn)單的Spring Boot啟用集成測(cè)試。 對(duì)測(cè)試環(huán)境的搭建囊陡,測(cè)試代碼的編寫進(jìn)行了實(shí)戰(zhàn)操作芳绩,基本能滿足日常開(kāi)發(fā)測(cè)試需要,相信你能從本文學(xué)到不少東西撞反。

相關(guān)的講解代碼可以從gitee獲取妥色。

也可通過(guò)我 個(gè)人博客 及時(shí)獲取更多的干貨分享。

關(guān)注公眾號(hào):碼農(nóng)小胖哥遏片,獲取更多資訊

個(gè)人博客:https://felord.cn

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嘹害,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子丁稀,更是在濱河造成了極大的恐慌吼拥,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件线衫,死亡現(xiàn)場(chǎng)離奇詭異凿可,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)授账,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門枯跑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人白热,你說(shuō)我怎么就攤上這事敛助。” “怎么了屋确?”我有些...
    開(kāi)封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵纳击,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我攻臀,道長(zhǎng)焕数,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任刨啸,我火速辦了婚禮堡赔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘设联。我一直安慰自己善已,他們只是感情好灼捂,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著换团,像睡著了一般悉稠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艘包,一...
    開(kāi)封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天偎球,我揣著相機(jī)與錄音,去河邊找鬼辑甜。 笑死衰絮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的磷醋。 我是一名探鬼主播猫牡,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼邓线!你這毒婦竟也來(lái)了淌友?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤骇陈,失蹤者是張志新(化名)和其女友劉穎震庭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體你雌,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡器联,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了婿崭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拨拓。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氓栈,靈堂內(nèi)的尸體忽然破棺而出渣磷,到底是詐尸還是另有隱情,我是刑警寧澤授瘦,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布醋界,位于F島的核電站,受9級(jí)特大地震影響提完,放射性物質(zhì)發(fā)生泄漏形纺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一氯葬、第九天 我趴在偏房一處隱蔽的房頂上張望挡篓。 院中可真熱鬧婉陷,春花似錦帚称、人聲如沸官研。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戏羽。三九已至,卻和暖如春楼吃,著一層夾襖步出監(jiān)牢的瞬間始花,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工孩锡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酷宵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓躬窜,卻偏偏與公主長(zhǎng)得像浇垦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子荣挨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容