Go項(xiàng)目實(shí)戰(zhàn)-代碼里有API調(diào)用時(shí)單元測(cè)試怎么做?
與數(shù)據(jù)庫(kù)的CURD操作類(lèi)似,當(dāng)我們對(duì)包含API接口調(diào)用的代碼進(jìn)行單元測(cè)試時(shí),肯定也是希望即不用對(duì)接口發(fā)起真正的網(wǎng)絡(luò)請(qǐng)求調(diào)用,也能驗(yàn)證我們的API對(duì)接程序是否符合預(yù)期。那么今天我們就聚焦于怎么為與API對(duì)接程序做單元測(cè)試,本節(jié)大綱如下:
圖片
在開(kāi)發(fā)項(xiàng)目的過(guò)程中總會(huì)遇到要調(diào)用依賴(lài)方接口的情況,如果依賴(lài)方的API接口還沒(méi)有開(kāi)發(fā)好,通常我們會(huì)先約定好API接口的請(qǐng)求參數(shù)、響應(yīng)結(jié)構(gòu)和各類(lèi)錯(cuò)誤對(duì)應(yīng)的響應(yīng)碼。這時(shí)雙方再按照這個(gè)約定同步進(jìn)行開(kāi)發(fā)。
除了上面說(shuō)的情況外,還有一種就是當(dāng)你開(kāi)發(fā)的功能需要與微信支付類(lèi)的API進(jìn)行對(duì)接時(shí),因?yàn)楦鞣N訂單、簽名、證書(shū)等的限制你在開(kāi)發(fā)階段也不能直接去調(diào)用支付的API來(lái)驗(yàn)證自己開(kāi)發(fā)的程序是否能成功完成對(duì)接,這種時(shí)候我們?cè)撛趺崔k呢?很多人會(huì)說(shuō)發(fā)到測(cè)試環(huán)境讓QA造單子測(cè),很多公司里的項(xiàng)目也確實(shí)是這么干的。
針對(duì)上面說(shuō)的這些情況,我們有沒(méi)有什么辦法在開(kāi)發(fā)階段就能通過(guò)單元測(cè)試來(lái)驗(yàn)證我們寫(xiě)的程序符不符合預(yù)期呢?這就需要我們掌握對(duì)API調(diào)用進(jìn)行Mock的技巧了。
API 調(diào)用Mock 基礎(chǔ)
gock 是 Go 生態(tài)下一個(gè)提供無(wú)侵入 HTTP Mock 的工具,用來(lái)在單元測(cè)試中Mock API 的調(diào)用,即不對(duì)要請(qǐng)求的API發(fā)起真正的網(wǎng)絡(luò)調(diào)用,而是由gock攔截到請(qǐng)求后返回我們指定的Mock響應(yīng)。
它支持用請(qǐng)求參數(shù)、請(qǐng)求頭、請(qǐng)求體等方式設(shè)置攔截請(qǐng)求的匹配條件,一旦匹配成功就會(huì)攔截測(cè)試程序中對(duì)API的調(diào)用,返回我們提前預(yù)設(shè)好的響應(yīng)。
gock 的安裝方法如下
go get -u github.com/h2non/gock
關(guān)于 gock 的基本使用方法,可以參考我寫(xiě)的這篇文章:用gock 攔截HTTP請(qǐng)求,Mock API調(diào)用 。我們接下來(lái)直接進(jìn)入API Mock的實(shí)戰(zhàn)環(huán)節(jié)。
API Mock 測(cè)試實(shí)戰(zhàn)
我們項(xiàng)目的API對(duì)接都放在了API對(duì)接層 library 中,實(shí)戰(zhàn)環(huán)節(jié)中我挑選了兩個(gè)API對(duì)接邏輯演示如何對(duì)他們進(jìn)行Mock單元測(cè)試,它們正好能覆蓋了GET、POST兩種請(qǐng)求方式下按照請(qǐng)求參數(shù)匹配攔截API請(qǐng)求和JSON請(qǐng)求體匹配攔截API請(qǐng)求。
單元測(cè)試入口TestMain的設(shè)置
我們項(xiàng)目里的對(duì)外API對(duì)接都放在library層中,按照上節(jié)課我們?yōu)轫?xiàng)目做的的單元測(cè)試目錄規(guī)劃,它的單元測(cè)試_test.go 文件都應(yīng)該放在test/library 目錄中。
.
|---test
| |---controller # controller 的測(cè)試用例
| |---dao # dao 的測(cè)試用例
| |---domainservice # 邏輯層領(lǐng)域服務(wù)的測(cè)試用例
| |---library # 外部API對(duì)接的測(cè)試用例
在開(kāi)始寫(xiě)單元測(cè)試前我們還是需要在TestMain方法中做一些 library 包中單元測(cè)試的初始化基礎(chǔ)工作。
func TestMain(m *testing.M) {
client := &http.Client{Transport: &http.Transport{}}
gock.InterceptClient(client)
// 把框架的httptool使用的http client 換成gock攔截的client
httptool.SetUTHttpClient(client)
os.Exit(m.Run())
}
因?yàn)槲覀冺?xiàng)目中的API調(diào)用都是httptool來(lái)發(fā)起的,所以我們需要把 httptool持有的全局httpClient 替換成由 gock 做了攔截的httpClient,只有這樣才能為項(xiàng)目中l(wèi)ibrary層中封裝的各個(gè)API對(duì)接程序做攔截和Mock。
實(shí)戰(zhàn)案例一:IP地址查詢(xún)的Mock測(cè)試
實(shí)戰(zhàn)環(huán)節(jié)先來(lái)一個(gè)簡(jiǎn)單點(diǎn)的案例,在library中我們?cè)?jīng)演示過(guò)一個(gè)用 whois API 查詢(xún)本機(jī)IP詳情的程序,具體程序如下:
func (whois *WhoisLib) GetHostIpDetail() (*WhoisIpDetail, error) {
log := logger.New(whois.ctx)
httpStatusCode, respBody, err := httptool.Get(
whois.ctx, "https://ipwho.is",
httptool.WithHeaders(map[string]string{
"User-Agent": "curl/7.77.0",
}),
)
if err != nil {
log.Error("whois request error", "err", err, "httpStatusCode", httpStatusCode)
returnnil, err
}
reply := new(WhoisIpDetail)
json.Unmarshal(respBody, reply)
return reply, nil
}
里面的邏輯很簡(jiǎn)單,只有一個(gè)簡(jiǎn)單的對(duì)whois API 的GET方式的請(qǐng)求調(diào)用,我們對(duì) WhoisLib 的GetHostIpDetail 方法做單測(cè)時(shí),可以對(duì)whois的API做Mock,讓API返回我們指定的IP地址,然后讓測(cè)試程序驗(yàn)證 GetHostIpDetail 方法返回的是不是這個(gè)指定的IP地址。
具體的單元測(cè)試方法如下:
func TestWhoisLib_GetHostIpDetail(t *testing.T) {
defer gock.Off()
gock.New("https://ipwho.is").
MatchHeader("User-Agent", "curl/7.77.0").Get("").
Reply(200).
BodyString("{\"ip\":\"127.126.113.220\",\"success\":true}")
ipDetail, err := library.NewWhoisLib(context.TODO()).GetHostIpDetail()
assert.Nil(t, err)
assert.Equal(t, "127.126.113.220", ipDetail.Ip)
}
你可能會(huì)說(shuō)這個(gè)例子也太簡(jiǎn)單了,別著急,接下來(lái)我們來(lái)個(gè)難的。
實(shí)戰(zhàn)案例二:微信支付的Mock測(cè)試
當(dāng)在開(kāi)發(fā)的功能需要與微信支付類(lèi)的API進(jìn)行對(duì)接時(shí),因?yàn)楦鞣N訂單、簽名、證書(shū)等的限制,在開(kāi)發(fā)階段不能直接去調(diào)用支付的API來(lái)驗(yàn)證自己開(kāi)發(fā)的程序是否能成功完成對(duì)接,在這種情況下如果能掌握API Mock技巧,能讓我們提前做好自己開(kāi)發(fā)程序的邏輯驗(yàn)證。
我們拿項(xiàng)目 WxPayLib 中的 CreateOrderPay 方法來(lái)給大家舉例子,這個(gè)方法會(huì)根據(jù)訂單數(shù)據(jù)向微信支付的JSAPI發(fā)起支付預(yù)下單,拿到預(yù)下單ID后再生成前端喚起微信進(jìn)行支付所需要的信息返給前端。
CreateOrderPay 方法的實(shí)現(xiàn)如下:
func (wpl *WxPayLib) CreateOrderPay(order *do.Order, userOpenId string) (payInvokeInfo *WxPayInvokeInfo, err error) {
// 創(chuàng)建預(yù)支付單
// 微信支付文檔:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml
payDescription := fmt.Sprintf("GOMALL 商場(chǎng)購(gòu)買(mǎi)%s 等商品", order.Items[0].CommodityName)
prePayPram := &PrePayParam{
AppId: wpl.payConfig.AppId,
MchId: wpl.payConfig.MchId,
Description: payDescription,
OutTradeNo: order.OrderNo,
NotifyUrl: wpl.payConfig.NotifyUrl,
}
prePayPram.Amount.Total = order.PayMoney
prePayPram.Amount.Currency = "CNY"
prePayPram.Payer.OpenId = userOpenId
reqBody, _ := json.Marshal(prePayPram)
token, err := wpl.getToken(http.MethodPost, string(reqBody), prePayApiUrl)
if err != nil {
err = errcode.Wrap("WxPayLibCreatePrePayError", err)
return
}
_, replyBody, err := httptool.Post(wpl.ctx, prePayApiUrl, reqBody, httptool.WithHeaders(map[string]string{
"Authorization": "WECHATPAY2-SHA256-RSA2048 " + token,
}))
if err != nil {
err = errcode.Wrap("WxPayLibCreatePrePayError", err)
return
}
prepayReply := struct {
PrePayId string`json:"prepay_id"`
}{}
if err = json.Unmarshal(replyBody, &prepayReply); err != nil {
err = errcode.Wrap("WxPayLibCreatePrePayError", err)
return
}
// 生成前端調(diào)起支付需要的參數(shù)
payInvokeInfo, err = wpl.genPayInvokeInfo(prepayReply.PrePayId)
if err != nil {
err = errcode.Wrap("WxPayLibCreatePrePayError", err)
}
return payInvokeInfo, nil
}
觀察 CreateOrderPay 中的代碼我們發(fā)現(xiàn),方法中除了對(duì)微信支付API的請(qǐng)求外 getToken、genPayInvokeInfo 這兩個(gè) WxPayLib 中定義的私有方法分別做了拿微信支付請(qǐng)求Token 和生成前端喚起微信客戶(hù)端進(jìn)行支付的參數(shù)的工作。
那么想要對(duì) CreateOrderPay 進(jìn)行單元測(cè)試除了Mock方法中對(duì)微信支付預(yù)下單接口的API請(qǐng)求外,還需要Mock 依賴(lài)的getToken和genPayInvokeInfo兩個(gè)方法的返回,而且因?yàn)樗鼈儍蓚€(gè)是私有方法,在test目錄Mock 它們就必須使用支持 Mock 私有方法的工具,好在Go的生態(tài)夠全,這里我使用的是gomonkey這個(gè)庫(kù)。
完成這個(gè)測(cè)試程序中主要分三步
- 造Order訂單數(shù)據(jù)。
- 為WxPayLib的genToken方法打樁,指定我們期望的返回。
- 使用 gock Mock 對(duì) https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi 的調(diào)用。
- 為WxPayLib的genPayInvokeInfo方法打樁,指定我們期望的返回。
- 用 assert 斷言各種結(jié)果,決定單元測(cè)試是否成功。