Go項目實戰-代碼里有API調用時單元測試怎么做?
與數據庫的CURD操作類似,當我們對包含API接口調用的代碼進行單元測試時,肯定也是希望即不用對接口發起真正的網絡請求調用,也能驗證我們的API對接程序是否符合預期。那么今天我們就聚焦于怎么為與API對接程序做單元測試,本節大綱如下:
圖片
在開發項目的過程中總會遇到要調用依賴方接口的情況,如果依賴方的API接口還沒有開發好,通常我們會先約定好API接口的請求參數、響應結構和各類錯誤對應的響應碼。這時雙方再按照這個約定同步進行開發。
除了上面說的情況外,還有一種就是當你開發的功能需要與微信支付類的API進行對接時,因為各種訂單、簽名、證書等的限制你在開發階段也不能直接去調用支付的API來驗證自己開發的程序是否能成功完成對接,這種時候我們該怎么辦呢?很多人會說發到測試環境讓QA造單子測,很多公司里的項目也確實是這么干的。
針對上面說的這些情況,我們有沒有什么辦法在開發階段就能通過單元測試來驗證我們寫的程序符不符合預期呢?這就需要我們掌握對API調用進行Mock的技巧了。
API 調用Mock 基礎
gock 是 Go 生態下一個提供無侵入 HTTP Mock 的工具,用來在單元測試中Mock API 的調用,即不對要請求的API發起真正的網絡調用,而是由gock攔截到請求后返回我們指定的Mock響應。
它支持用請求參數、請求頭、請求體等方式設置攔截請求的匹配條件,一旦匹配成功就會攔截測試程序中對API的調用,返回我們提前預設好的響應。
gock 的安裝方法如下
go get -u github.com/h2non/gock
關于 gock 的基本使用方法,可以參考我寫的這篇文章:用gock 攔截HTTP請求,Mock API調用 。我們接下來直接進入API Mock的實戰環節。
API Mock 測試實戰
我們項目的API對接都放在了API對接層 library 中,實戰環節中我挑選了兩個API對接邏輯演示如何對他們進行Mock單元測試,它們正好能覆蓋了GET、POST兩種請求方式下按照請求參數匹配攔截API請求和JSON請求體匹配攔截API請求。
單元測試入口TestMain的設置
我們項目里的對外API對接都放在library層中,按照上節課我們為項目做的的單元測試目錄規劃,它的單元測試_test.go 文件都應該放在test/library 目錄中。
.
|---test
| |---controller # controller 的測試用例
| |---dao # dao 的測試用例
| |---domainservice # 邏輯層領域服務的測試用例
| |---library # 外部API對接的測試用例
在開始寫單元測試前我們還是需要在TestMain方法中做一些 library 包中單元測試的初始化基礎工作。
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())
}
因為我們項目中的API調用都是httptool來發起的,所以我們需要把 httptool持有的全局httpClient 替換成由 gock 做了攔截的httpClient,只有這樣才能為項目中library層中封裝的各個API對接程序做攔截和Mock。
實戰案例一:IP地址查詢的Mock測試
實戰環節先來一個簡單點的案例,在library中我們曾經演示過一個用 whois API 查詢本機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
}
里面的邏輯很簡單,只有一個簡單的對whois API 的GET方式的請求調用,我們對 WhoisLib 的GetHostIpDetail 方法做單測時,可以對whois的API做Mock,讓API返回我們指定的IP地址,然后讓測試程序驗證 GetHostIpDetail 方法返回的是不是這個指定的IP地址。
具體的單元測試方法如下:
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)
}
你可能會說這個例子也太簡單了,別著急,接下來我們來個難的。
實戰案例二:微信支付的Mock測試
當在開發的功能需要與微信支付類的API進行對接時,因為各種訂單、簽名、證書等的限制,在開發階段不能直接去調用支付的API來驗證自己開發的程序是否能成功完成對接,在這種情況下如果能掌握API Mock技巧,能讓我們提前做好自己開發程序的邏輯驗證。
我們拿項目 WxPayLib 中的 CreateOrderPay 方法來給大家舉例子,這個方法會根據訂單數據向微信支付的JSAPI發起支付預下單,拿到預下單ID后再生成前端喚起微信進行支付所需要的信息返給前端。
CreateOrderPay 方法的實現如下:
func (wpl *WxPayLib) CreateOrderPay(order *do.Order, userOpenId string) (payInvokeInfo *WxPayInvokeInfo, err error) {
// 創建預支付單
// 微信支付文檔:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml
payDescription := fmt.Sprintf("GOMALL 商場購買%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
}
// 生成前端調起支付需要的參數
payInvokeInfo, err = wpl.genPayInvokeInfo(prepayReply.PrePayId)
if err != nil {
err = errcode.Wrap("WxPayLibCreatePrePayError", err)
}
return payInvokeInfo, nil
}
觀察 CreateOrderPay 中的代碼我們發現,方法中除了對微信支付API的請求外 getToken、genPayInvokeInfo 這兩個 WxPayLib 中定義的私有方法分別做了拿微信支付請求Token 和生成前端喚起微信客戶端進行支付的參數的工作。
那么想要對 CreateOrderPay 進行單元測試除了Mock方法中對微信支付預下單接口的API請求外,還需要Mock 依賴的getToken和genPayInvokeInfo兩個方法的返回,而且因為它們兩個是私有方法,在test目錄Mock 它們就必須使用支持 Mock 私有方法的工具,好在Go的生態夠全,這里我使用的是gomonkey這個庫。
完成這個測試程序中主要分三步
- 造Order訂單數據。
- 為WxPayLib的genToken方法打樁,指定我們期望的返回。
- 使用 gock Mock 對 https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi 的調用。
- 為WxPayLib的genPayInvokeInfo方法打樁,指定我們期望的返回。
- 用 assert 斷言各種結果,決定單元測試是否成功。