上一节课我给大家展示了项目Dao层里的CURD操作我们该怎么做单元测试,尤其是Insert、Update这些需要对数据库进行更改的操作怎么用sqlMock的方式让我们既不用真正对数据库发起操作也能验证这些操作是否符合预期。
与数据库的CURD操作类似,当我们对包含API接口调用的代码进行单元测试时,肯定也是希望即不用对接口发起真正的网络请求调用,也能验证我们的API对接程序是否符合预期。那么今天我们就聚焦于怎么为与API对接程序做单元测试,本节大纲如下:
除了上面说的情况外,还有一种就是当你开发的功能需要与微信支付类的API进行对接时,因为各种订单、签名、证书等的限制你在开发阶段也不能直接去调用支付的API来验证自己开发的程序是否能成功完成对接,这种时候我们该怎么办呢?很多人会说发到测试环境让QA造单子测,很多公司里的项目也确实是这么干的。
针对上面说的这些情况,我们有没有什么办法在开发阶段就能通过单元测试来验证我们写的程序符不符合预期呢?这就需要我们掌握对API调用进行Mock的技巧了。
本文节选自我的专栏《Go项目搭建和整洁开发实战》欢迎扫码订阅解锁更多内容。

订阅后除了加入实战项目外,还可加入专属读者群一起学习。
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这个库
gomonkey 的使用方法请参考:Go代码测试时怎么打桩?给大家写了几个常用案例
完成这个测试程序中主要分三步
造Order订单数据。
为WxPayLib的genToken方法打桩,指定我们期望的返回。
使用 gock Mock 对 https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi 的调用。
为WxPayLib的genPayInvokeInfo方法打桩,指定我们期望的返回。
用 assert 断言各种结果,决定单元测试是否成功。
本文实战项目和更全面的教程请扫下方二维码订阅专栏后联系我进项目

加入专栏后除了能获得完整版教程,更有配套的实战项目和专属读者群,我在其中用git tag版本编排好了每节教程对应的代码更新,让你能更容易地追踪项目开发过程中的每一处代码变更。
《Go项目搭建和整洁开发实战》专栏分为五大部分,重点章节如下

第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项。
扫描上方二维码或者访问 https://xiaobot.net/p/golang 即刻订阅
此外想更详细地了解专栏内容,咨询专栏优惠,都可以添加下面我的微信