javascript
基于链路思想的SpringBoot单元测试快速写法
簡(jiǎn)介:本文更偏向?qū)嵺`而非方法論,所提及的SpringBoot單元測(cè)試寫(xiě)法亦并非官方解,僅僅是筆者自身覺(jué)得比較方便、效率較高的一種寫(xiě)法。每個(gè)團(tuán)隊(duì)甚至團(tuán)隊(duì)內(nèi)的每位開(kāi)發(fā)可能都有自己的寫(xiě)法習(xí)慣和風(fēng)格,只要能實(shí)現(xiàn)單元測(cè)試的效果,就沒(méi)必要糾結(jié)于寫(xiě)法的簡(jiǎn)單抑或復(fù)雜。這里也歡迎各位大佬們發(fā)表看法或分享自己的單測(cè)心得,幫助像筆者這樣的新人快速成長(zhǎng)。
作者 | 桃符
來(lái)源 | 阿里技術(shù)公眾號(hào)
引言:
本文更偏向?qū)嵺`而非方法論,所提及的SpringBoot單元測(cè)試寫(xiě)法亦并非官方解,僅僅是筆者自身覺(jué)得比較方便、效率較高的一種寫(xiě)法。每個(gè)團(tuán)隊(duì)甚至團(tuán)隊(duì)內(nèi)的每位開(kāi)發(fā)可能都有自己的寫(xiě)法習(xí)慣和風(fēng)格,只要能實(shí)現(xiàn)單元測(cè)試的效果,就沒(méi)必要糾結(jié)于寫(xiě)法的簡(jiǎn)單抑或復(fù)雜。這里也歡迎各位大佬們發(fā)表看法或分享自己的單測(cè)心得,幫助像筆者這樣的新人快速成長(zhǎng)。
一 為什么要寫(xiě)單元測(cè)試?
測(cè)試是Devops上極重要的一環(huán),但大多數(shù)開(kāi)發(fā)的眼光都停留在集成測(cè)試這一環(huán)——只要能聯(lián)調(diào)成功,那么我這次準(zhǔn)備上線的特性一定是沒(méi)問(wèn)題的。
老實(shí)承認(rèn),我曾經(jīng)是這樣的可能現(xiàn)在也還是這樣。作為非科班出身的筆者,研究生畢業(yè)后就立即進(jìn)入了同在杭州的xx廠,先后參與了內(nèi)部Devops平臺(tái)建設(shè)和xx云Paas項(xiàng)目開(kāi)荒,在這兩個(gè)項(xiàng)目中,開(kāi)發(fā) > 測(cè)試是很正常的場(chǎng)景,甚至部分測(cè)試也是原開(kāi)發(fā)友情客串的:由于缺少專業(yè)的測(cè)試人員,開(kāi)發(fā)往往需要兼顧集成測(cè)試甚至是線上測(cè)試的活兒。為了提高效率,我將一部分常用的測(cè)試用例維護(hù)在了內(nèi)部的自動(dòng)化測(cè)試平臺(tái)上。即便如此,我仍能清晰地感覺(jué)到,測(cè)試所能覆蓋的場(chǎng)景屈指可數(shù),以至于每次自信地上線大特性后,都會(huì)因一些奇怪的問(wèn)題而定位到大半夜。幸虧后面遇到了一位資深大佬,在code review時(shí),他直接點(diǎn)出我不寫(xiě)單元測(cè)試的壞習(xí)慣,并用自身慘痛的線上教訓(xùn)反復(fù)強(qiáng)調(diào)單測(cè)的重要性。
當(dāng)然上述只是我的親身經(jīng)歷,勉強(qiáng)作為日常閑聊的談資。如果想要深入理解單元測(cè)試的重要性,推薦Google上搜索the importance of unit test關(guān)鍵字,可以感受下不同國(guó)家、不同領(lǐng)域的程序員對(duì)單元測(cè)試的不同理解,想必能有更大的收獲。
二 為什么推薦鏈路思想?
深入接觸單元測(cè)試,開(kāi)發(fā)難免會(huì)遇到以下場(chǎng)景:
剛開(kāi)始學(xué)習(xí)寫(xiě)單元測(cè)試,我也曾參考并嘗試過(guò)網(wǎng)上五花八門(mén)的寫(xiě)法。這些寫(xiě)法可能用到了不同的單測(cè)框架,也可能側(cè)重了不同的代碼環(huán)節(jié)(例如特定的某個(gè)service方法)。一開(kāi)始我為自己能夠熟練使用多種單測(cè)框架而沾沾自喜,但隨著工作的推進(jìn),我逐漸意識(shí)到,單元測(cè)試中重要的并不是框架選型,而是如何設(shè)計(jì)一套優(yōu)秀的用例。之所以用"一套"而不是"一個(gè)",是因?yàn)樵谖覀兊臉I(yè)務(wù)代碼中,邏輯往往并非"一帆風(fēng)順",有許多if-else會(huì)妝點(diǎn)我們的業(yè)務(wù)代碼。顯然對(duì)于這類業(yè)務(wù)代碼,"一個(gè)"測(cè)試用例無(wú)法完全滿足所有可能出現(xiàn)的場(chǎng)景。如果為了偷懶,嘗試僅僅用"一個(gè)"用例去覆蓋主流程,無(wú)異于給自己埋了個(gè)雷——線上場(chǎng)景可沒(méi)"一個(gè)"用例這么簡(jiǎn)單!
我開(kāi)始專注于測(cè)試用例的設(shè)計(jì),從輸入輸出開(kāi)始,重新審視曾經(jīng)開(kāi)發(fā)過(guò)的代碼。我發(fā)現(xiàn),如果將某個(gè)controller方法作為入口,那這一套業(yè)務(wù)流程可以當(dāng)做一條鏈路,而上下文中所關(guān)聯(lián)的service層、dao層、api層的各方法都可以作為鏈路上的各環(huán)節(jié)。通過(guò)繪制鏈路圖,將各環(huán)節(jié)根據(jù)是否關(guān)聯(lián)外部系統(tǒng)大致分成黑、白兩類,整套業(yè)務(wù)流程和各環(huán)節(jié)的潛在分支便會(huì)變得清晰,測(cè)試用例便從"一個(gè)"自然而然地變成了"一套"。此處多提一嘴,鏈路思想設(shè)計(jì)用例的基礎(chǔ)是結(jié)構(gòu)清晰、圈復(fù)雜度可控制的代碼風(fēng)格,如果開(kāi)發(fā)的時(shí)候依然尊崇"論文式"、"一刀流",在單個(gè)方法內(nèi)"長(zhǎng)篇大論",那鏈路式將是一個(gè)巨大的負(fù)擔(dān)。
編寫(xiě)測(cè)試用例其實(shí)不是一件費(fèi)勁的事,對(duì)于深耕業(yè)務(wù)代碼的開(kāi)發(fā)而言,編寫(xiě)測(cè)試用例便像是做一盤(pán)小菜,舉手可為。于我而言,如今寫(xiě)測(cè)試用例所花費(fèi)的時(shí)間甚至沒(méi)有設(shè)計(jì)測(cè)試用例的時(shí)間長(zhǎng)(凸顯用例設(shè)計(jì)的重要性但也有可能是我對(duì)測(cè)試用例的設(shè)計(jì)還不夠熟練)。在測(cè)試框架選型上,我更習(xí)慣于Junit+Mockito的組合,原因僅僅是熟悉與簡(jiǎn)單,且參考文檔比比皆是。如果各位已經(jīng)有自己習(xí)慣的框架和寫(xiě)法,也不必照搬本文所提及的東西,畢竟單測(cè)是為了better code,而不是自找麻煩。
但無(wú)論測(cè)試用例如何設(shè)計(jì)或是如何編寫(xiě),我始終認(rèn)為,在不考慮測(cè)試代碼的風(fēng)格和規(guī)范的前提下,衡量測(cè)試用例質(zhì)量的核心指標(biāo)是分支覆蓋率。這也是我推薦鏈路思想的一大原因——從入口出發(fā),遍歷鏈路上各個(gè)環(huán)節(jié)的各個(gè)分支,遇到阻礙就Mock;相比于分別單測(cè)各個(gè)獨(dú)立方法,單測(cè)鏈路所需要的入?yún)⒑统鰠⒏忧逦?#xff0c;更是大大節(jié)省了編寫(xiě)測(cè)試代碼所需的時(shí)間成本!計(jì)算分支覆蓋率的工具有很多,例如本地的JaCoCo或是各類云化測(cè)試工具。試想,每當(dāng)看到單測(cè)完美地覆蓋了自己所提交的特性代碼時(shí),心里是不是放心了許多?
三 如何用鏈路思想設(shè)計(jì)/構(gòu)造單測(cè)?
作為程序員,大家更為熟悉的鏈路概念應(yīng)該是全鏈路壓測(cè)。
全鏈路壓測(cè)簡(jiǎn)單來(lái)說(shuō),就是基于實(shí)際的生產(chǎn)業(yè)務(wù)場(chǎng)景、系統(tǒng)環(huán)境,模擬海量的用戶請(qǐng)求和數(shù)據(jù)對(duì)整個(gè)業(yè)務(wù)鏈進(jìn)行壓力測(cè)試,并持續(xù)調(diào)優(yōu)的過(guò)程,本質(zhì)上也是性能測(cè)試的一種手段。... 通過(guò)這種方法,在生產(chǎn)環(huán)境上落地常態(tài)化穩(wěn)定壓測(cè)體系,實(shí)現(xiàn)IT系統(tǒng)的長(zhǎng)期性能穩(wěn)定治理。
如果將完整的業(yè)務(wù)流程視作全鏈路,那作為業(yè)務(wù)鏈上的一環(huán),即某個(gè)后端服務(wù),它其實(shí)也是一個(gè)微鏈路。這里以自上而下的開(kāi)發(fā)流程為例,對(duì)于新增的功能接口,我們會(huì)習(xí)慣性地由controller開(kāi)始設(shè)計(jì),然后構(gòu)建service層、dao層、api層,最后再錦上添花地加些aop。如果以鏈路思想,將復(fù)雜的流程拆成各個(gè)鏈路的各個(gè)環(huán)節(jié),那這樣的代碼功能清晰,維護(hù)起來(lái)也相當(dāng)方便。我非常認(rèn)同 限制單個(gè)方法行數(shù)<=50 的代碼門(mén)禁,對(duì)于長(zhǎng)篇大論的代碼“論文”,想必沒(méi)有哪位接手的同學(xué)臉上能露出笑容的;針對(duì)這類代碼,我認(rèn)為clean code的優(yōu)先級(jí)比補(bǔ)充單測(cè)用例更高,連邏輯都無(wú)法理清,即便硬著頭皮寫(xiě)出單測(cè)用例,后續(xù)的調(diào)試和維護(hù)工作量也是不可預(yù)料的(試想,假如后面有位A同學(xué)接手了這塊代碼,他在“論文”中加了xx行導(dǎo)致ut失敗了,他該如何去定位問(wèn)題)。
簡(jiǎn)單畫(huà)個(gè)圖來(lái)強(qiáng)調(diào)一下我的觀點(diǎn)。這是一張"用戶買豬"的功能邏輯圖。以鏈路思想,開(kāi)發(fā)人員將整套流程拆分為相應(yīng)的鏈路環(huán)節(jié),涵蓋了controller、service、dao、api各層;整條鏈路清晰明了,只要搭配完善的上下文日志,定位線上問(wèn)題亦是輕而易舉。
當(dāng)然,基于鏈路思想的開(kāi)發(fā)還遠(yuǎn)遠(yuǎn)不夠,在補(bǔ)充單測(cè)用例時(shí),我們同樣也能用鏈路思想來(lái)構(gòu)造測(cè)試用例。測(cè)試用例的要求很簡(jiǎn)單,需要覆蓋controller、service等自主編寫(xiě)的代碼(多分支場(chǎng)景也需要完全覆蓋),對(duì)于周邊關(guān)聯(lián)的系統(tǒng)可以采用Mock進(jìn)行屏蔽,對(duì)于Dao層的SQL可以視需求決定是否Mock。秉承這個(gè)思路,我們可以對(duì)“用戶買豬”圖進(jìn)行改造,將允許Mock的環(huán)節(jié)涂灰,從而變成我們?cè)诰帉?xiě)單元測(cè)試用例時(shí)所需要的“虛擬用戶買豬”圖。
四 快速寫(xiě)法實(shí)踐案例
1 快速寫(xiě)法的核心步驟有哪些?
快速寫(xiě)法的入口是controller層方法,這樣對(duì)于controller層存在的少量邏輯代碼也能做到覆蓋。
設(shè)計(jì)測(cè)試用例的輸入與預(yù)期輸出
設(shè)計(jì)測(cè)試用例的目的不僅僅是跑通主流程,而是要跑通全部可能的流程,即所謂的分支全覆蓋,因此設(shè)計(jì)用例的輸入與輸出尤為重要。即便是新增分支的增量修改(例如加了一行if-else),也需要補(bǔ)充相應(yīng)的輸入與預(yù)期輸出。非常不建議根據(jù)單測(cè)運(yùn)行結(jié)果修改預(yù)期結(jié)果,這說(shuō)明原先的代碼設(shè)計(jì)有問(wèn)題。
確定鏈路上的全部Mock點(diǎn)
Mock點(diǎn)的判斷依據(jù)是鏈路上該環(huán)節(jié)是否依賴第三方服務(wù)。強(qiáng)烈建議在設(shè)計(jì)前畫(huà)出大概的功能流程圖(如”用戶買豬“圖),這可以大大提高確定Mock點(diǎn)的速度和準(zhǔn)確性。
收集Mock點(diǎn)的模擬返回?cái)?shù)據(jù)
確定Mock點(diǎn)后,我們就需要構(gòu)造相應(yīng)的模擬返回?cái)?shù)據(jù)。Mock數(shù)據(jù)需要考慮多個(gè)因素:
a. 是否與api層對(duì)應(yīng)方法的期望返回值匹配: 不能把從豬廠返回的Mock數(shù)據(jù)用牛肉替代
b. 是否與模擬輸入數(shù)據(jù)匹配:用戶需要1斤豬肉,不能返回5斤豬肉的數(shù)據(jù)
c. 是否與api層的所有分支匹配:部分api層會(huì)對(duì)返回值進(jìn)行響應(yīng)碼(2xx || 3xx || 4xx)校驗(yàn),這類場(chǎng)景便需要構(gòu)造不同響應(yīng)碼的Mock數(shù)據(jù)
2【開(kāi)發(fā)篇】真實(shí)用戶買豬
該項(xiàng)目基于PandoraBoot構(gòu)建,手動(dòng)升級(jí)SpringBoot版本至2.5.1,使用Mybatis-plus組件簡(jiǎn)化Dao層開(kāi)發(fā)過(guò)程。下面選取了上文圖中所涉及的重要方法進(jìn)行展示,僅實(shí)現(xiàn)了簡(jiǎn)單的業(yè)務(wù)流程,系統(tǒng)框架和工程結(jié)構(gòu)可以參考代碼倉(cāng)。
業(yè)務(wù)對(duì)象
PorkStorage.java - 豬肉庫(kù)存的數(shù)據(jù)庫(kù)實(shí)體類 /*** 豬肉庫(kù)存的數(shù)據(jù)庫(kù)實(shí)體類*/ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName(value = "pork_storage", autoResultMap = true) public class PorkStorage {@TableId(value = "id", type = IdType.AUTO)private Long id;private Long cnt; }PorkInst.java - 豬肉實(shí)例,由倉(cāng)庫(kù)打包后生成
/*** 豬肉實(shí)例,由倉(cāng)庫(kù)打包后生成**/ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class PorkInst {/*** 重量*/private Long weight;/*** 附件參數(shù),例如包裝類型,寄送地址等信息*/private Map< String, Object> paramsMap; }業(yè)務(wù)代碼
PorkController.java @RestController @Slf4j @RequestMapping("/pork") public class PorkController {@Autowiredprivate PorkService porkService;@PostMapping("/buy")public ResponseEntity< PorkInst> buyPork(@RequestParam("weight") Long weight,@RequestBody Map< String,Object> params) {if (weight == null) {throw new BaseBusinessException("invalid input: weight", ExceptionTypeEnum.INVALID_REQUEST_PARAM_ERROR);}return ResponseEntity.ok(porkService.getPork(weight, params));} }PorkService.java
public interface PorkService {/*** 獲取豬肉打包實(shí)例** @param weight 重量* @param params 額外信息* @return {@link PorkInst} - 指定數(shù)量的豬肉實(shí)例* @throws BaseBusinessException 如果豬肉庫(kù)存不足,返回異常,同時(shí)后臺(tái)告知工廠*/PorkInst getPork(Long weight, Map< String, Object> params); }PorkStorageDao.java
@Mapper public interface PorkStorageDao extends BaseMapper< PorkStorage> {PorkStorage queryStore(); }PorkStorageDao.xml
< ?xml version="1.0" encoding="UTF-8"?> < !DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> < mapper namespace="com.alibaba.ut.demo.dao.PorkStorageDao">< sql id="columns">id, cnt< /sql>< sql id="table_name">pork_storage< /sql>< select id="queryStore" resultType="com.alibaba.ut.demo.entity.PorkStorage">select< include refid="columns"/>from< include refid="table_name"/>where id = 1< /select> < /mapper>FactoryApi.java
public interface FactoryApi {void supplyPork(Long weight); }FactoryApiImpl.java
@Service @Slf4j public class FactoryApiImpl implements FactoryApi {@Overridepublic void supplyPork(Long weight) {log.info("call real factory to supply pork, weight: {}", weight);} }WareHouseApi.java
public interface WareHouseApi {PorkInst packagePork(Long weight, Map< String, Object> params); }WareHouseApiImpl.java
@Service @Slf4j public class WareHouseApiImpl implements WareHouseApi {@Overridepublic PorkInst packagePork(Long weight, Map< String, Object> params) {log.info("call real warehouse to package, weight: {}", weight);return PorkInst.builder().weight(weight).paramsMap(params).build();} }3【單測(cè)篇】虛擬用戶買豬
單測(cè)依賴
對(duì)于PandoraBoot工程,可參考下文的Maven配置引入相關(guān)依賴。
對(duì)于非PandoraBoot工程,僅需引入Junit和Mockito兩個(gè)包即可。
注本章所提到的單測(cè)寫(xiě)法默認(rèn)Mock Dao層且無(wú)需啟動(dòng)容器應(yīng)用。如果不想Mock Dao層,建議在依賴中引入H2這類內(nèi)存型數(shù)據(jù)庫(kù),同時(shí)支持本地啟動(dòng)容器應(yīng)用。
寫(xiě)法思路
在閱讀下面的內(nèi)容前,強(qiáng)烈建議先學(xué)習(xí)Junit和Mockito的基本用法和運(yùn)行原理,包括但不限于下文寫(xiě)法中可能涉及的注解:Junit原生流Method注解:@Before 、@Test、@After
Mockito原生Field注解:@Mock、@InjectMocks、@Spy
在已知待單測(cè)業(yè)務(wù)鏈路的前提下,寫(xiě)法可以簡(jiǎn)要?dú)w納為以下幾步:
- 非Mock點(diǎn)方法:對(duì)于鏈路中非入口的環(huán)節(jié)(通常將controller作為入口,其他方法即為非入口),需要標(biāo)注@Spy以聲明該對(duì)象在單測(cè)鏈路中為監(jiān)聽(tīng)狀態(tài),即需要正常走完流程。此處根據(jù)方法內(nèi)是否引用Mock點(diǎn)方法進(jìn)一步分成兩類。
- 該方法內(nèi)引用了其他Mock點(diǎn)方法,需要在@Spy的基礎(chǔ)上額外標(biāo)注@InjectMocks,聲明該對(duì)象在單測(cè)鏈路中需要被注入其他Mock對(duì)象。
- 該方法內(nèi)未引用其他Mock點(diǎn)方法,無(wú)需進(jìn)行其他操作。
- Mock點(diǎn)方法:標(biāo)注@Mock以聲明該對(duì)象在單測(cè)鏈路中需要被Mock,可以通過(guò)org.mockito.Mockito類內(nèi)的一系列static方法手動(dòng)注入Mock值(ep. when(A()).thenReturn(B))。
這里仍以"用戶買豬"的場(chǎng)景為例,依照鏈路思想,當(dāng)服務(wù)端收到用戶購(gòu)買豬肉的請(qǐng)求時(shí),我們可以構(gòu)造出如下分支場(chǎng)景:
單測(cè)代碼
package com.alibaba.ut.demo.controller;import com.alibaba.ut.demo.PorkController; import com.alibaba.ut.demo.api.FactoryApi; import com.alibaba.ut.demo.api.WareHouseApi; import com.alibaba.ut.demo.dao.PorkStorageDao; import com.alibaba.ut.demo.entity.PorkInst; import com.alibaba.ut.demo.entity.PorkStorage; import com.alibaba.ut.demo.exception.BaseBusinessException; import com.alibaba.ut.demo.service.impl.PorkServiceImpl; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.mockito.stubbing.Answer; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity;import java.util.HashMap; import java.util.Map; import java.util.Optional;import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when;/*** @Author Taofu.lj* @Version 1.0.0* @Date 2021年12月02日 14:15*/ @Slf4j public class PorkControllerTest {/*** controller入口,由于是鏈路入口,無(wú)需用@Spy監(jiān)聽(tīng)*/@InjectMocksprivate PorkController porkController;/*** 接口類型的鏈路環(huán)節(jié)用實(shí)現(xiàn)類初始化代替, @Spy需要手動(dòng)初始化避免initMocks時(shí)失敗* 注:鏈路上每一環(huán)都必須聲明,即使測(cè)試用例中并沒(méi)有被顯性調(diào)用*/@InjectMocks@Spyprivate PorkServiceImpl porkService = new PorkServiceImpl();/*** 待Mock的鏈路環(huán)節(jié),下同*/@Mockprivate PorkStorageDao porkStorageDao;@Mockprivate FactoryApi factoryApi;@Mockprivate WareHouseApi wareHouseApi;/*** 預(yù)置數(shù)據(jù)可直接作為類變量聲明*/private final Map< String, Object> mockParams = new HashMap< String, Object>() {{put("user", "system_user");}};@Beforepublic void setup() {// 必要: 初始化該類中所聲明的Mock和InjectMock對(duì)象MockitoAnnotations.initMocks(this);// Mock預(yù)置數(shù)據(jù)并綁定相關(guān)方法(適用于有返回值的方法)PorkStorage mockStorage = PorkStorage.builder().id(1L).cnt(10L).build();// 常見(jiàn)Mock寫(xiě)法一:僅試圖Mock返回值when(porkStorageDao.queryStore()).thenReturn(mockStorage);// 常見(jiàn)Mock寫(xiě)法二:不僅試圖Mock返回值,還想額外打些日志方便定位when(wareHouseApi.packagePork(any(), any())).thenAnswer(ans -> {log.info("mock log can be written here");return PorkInst.builder().weight(ans.getArgumentAt(0, Long.class)).paramsMap(ans.getArgumentAt(1, Map.class)).build();});// Mock動(dòng)作并綁定相關(guān)方法(適用于無(wú)返回值方法)doAnswer((Answer< Void>) invocationOnMock -> {log.info("mock factory api success!");return null;}).when(factoryApi).supplyPork(any());}@Afterpublic void teardown() {// TODO: 可以加入Mock數(shù)據(jù)清理或資源釋放}/*** 當(dāng)傳入?yún)?shù)為null時(shí),拋出業(yè)務(wù)異常** @throws BaseBusinessException*/@Test(expected = BaseBusinessException.class)public void testBuyPorkIfWeightIsNull() {porkController.buyPork(null, mockParams);}/*** 當(dāng)后臺(tái)庫(kù)存不滿足需求時(shí),拋出業(yè)務(wù)異常** @throws BaseBusinessException*/@Test(expected = BaseBusinessException.class)public void testBuyPorkIfStorageIsShortage() {porkController.buyPork(20L, mockParams);}/*** 正常購(gòu)買時(shí)返回業(yè)務(wù)結(jié)果*/@Testpublic void testBuyPorkIfResultIsOk() {Long expectWeight = 5L;ResponseEntity< PorkInst> res = porkController.buyPork(expectWeight, mockParams);// 此處第一次校驗(yàn)接口返回狀態(tài)是否符合預(yù)期Assert.assertEquals(HttpStatus.OK, res.getStatusCode());Long actualWeight = Optional.of(res).map(HttpEntity::getBody).map(PorkInst::getWeight).orElse(-99L);// 此處第二次校驗(yàn)接口返回值是否符合預(yù)期Assert.assertEquals(expectWeight, actualWeight);} }原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?
總結(jié)
以上是生活随笔為你收集整理的基于链路思想的SpringBoot单元测试快速写法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一文说清linux system loa
- 下一篇: Spring官方RSocket Brok