分享
  1. 首页
  2. 文章

mock go 程序的新方法

spacewander · · 1565 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一直以来,我都认为在 go 里面 mock 是非常困难的。不像动态语言或者跑在 VM 上的语言,go 要求在开发的时候就给 mock 介入预留空间,不然测试的时候会不得其门而入。开发的时候需要头疼的事情可多了,还要求再考虑下可测试性,真有点强人所难。另外第三方库并不一定给 mock 预留空间,遇到这种情况只能干瞪眼绕路走。很多时候,无法 mock 掉某些带副作用的函数,就不能覆盖掉目标路径。既然测试不到关键的路径,那干脆就不写测试了。结果是,项目里很多 go 代码事实上一直都没有被测试覆盖掉。

但最近我发现了一个库:https://github.com/bouk/monkey
似乎可以跟开头的烦恼永别了?小范围地体验了下,感觉还是挺好用的。

长话短说,monkey 库通过修改内存地址的方式,替换目标函数的实际执行地址,实现(几乎)任意函数的 mock。你可以指定目标函数,然后定义一个匿名函数替换掉它。替换的记录会存在一个全局表里,不需要的时候可以通过它重新恢复原来的目标函数。由于采用的是修改内存地址的黑科技,作者建议千万不要用在测试环境以外的地方。目前仅支持x86架构上的 Linux 和 Mac,Windows 似乎没有测试过?不管怎样,支持 Linux 和 Mac 就足以覆盖开发机和 CI 环境了。

monkey 库用起来非常简单,直接边上示例代码,边解释好了:

package main
import (
 "fmt"
 "github.com/bouk/monkey"
 "os"
 "os/exec"
 "reflect"
 "testing"
)
// 假如我们要测试函数 call
func call(cmd string) (int, string) {
 bytes, err := exec.Command("sh", "-c", cmd).CombinedOutput()
 output := string(bytes)
 if err != nil {
 return 1, reportExecFailed(output)
 }
 return 0, output
}
// 上面的函数会调用它,这个函数一定要mock掉!
func reportExecFailed(msg string) string {
 os.Exit(1) // 讨人嫌的副作用
 return msg
}
func TestExecSussess(t *testing.T) {
 // 恢复 patch 修改
 // 实际使用中会把 UnpatchAll 放到 teardown 函数里
 // 不过在 go 自带的 testing 里就这么处理了
 defer monkey.UnpatchAll()
 // mock 掉 exec.Command 返回的 *exec.Cmd 的 CombinedOutput 方法
 monkey.PatchInstanceMethod(
 reflect.TypeOf((*exec.Cmd)(nil)),
 "CombinedOutput", func(_ *exec.Cmd) ([]byte, error) {
 return []byte("results"), nil
 },
 )
 // mock 掉 reportExecFailed 函数
 monkey.Patch(reportExecFailed, func(msg string) string {
 return msg
 })
 rc, output := call("any")
 if rc != 0 {
 t.Fail()
 }
 if output != "results" {
 t.Fail()
 }
}
func TestExecFailed(t *testing.T) {
 defer monkey.UnpatchAll()
 // 上次 mock 的是执行成功的情况,这一次轮到执行失败
 monkey.PatchInstanceMethod(
 reflect.TypeOf((*exec.Cmd)(nil)),
 "CombinedOutput", func(_ *exec.Cmd) ([]byte, error) {
 return []byte(""), fmt.Errorf("sth bad happened")
 },
 )
 monkey.Patch(reportExecFailed, func(msg string) string {
 return msg
 })
 rc, output := call("any")
 if rc != 1 {
 t.Fail()
 }
 if output != "" {
 t.Fail()
 }
}

执行 go test xx_test.go,可以运行上面的代码。

测试中常有的一个需求:在位置 A 需要 mock 掉函数,在位置 B 里需要调用原来的函数才能运行下去。这时候需要使用 monkey 库提供的 PatchGuard 结构体。官方文档中有个示例,这里稍微调整下:

package main
import (
 "fmt"
 "github.com/bouk/monkey"
 "strings"
)
func main() {
 var guard *monkey.PatchGuard
 guard = monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
 s := make([]interface{}, len(a))
 for i, v := range a {
 s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
 }
 // 以下代码等价于
 // guard.Unpatch()
 // defer guard.Restore()
 // return fmt.Println(s...)
 guard.Unpatch()
 n, err = fmt.Println(s...)
 guard.Restore()
 return
 })
 fmt.Println("what the hell?") // what the *bleep*?
 fmt.Println("what the hell?") // what the *bleep*?
}

上面的代码关键在于,调用原来的函数之前先调用一次 Unpatch,恢复到 mock 之前的情况;然后在调用了原函数之后,调用一次 Restore,重新打上 mock。剩下的,无非是根据输入参数来判断现在是运行到位置 A,还是位置 B。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:spacewander

查看原文:mock go 程序的新方法

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1565 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏