go 单元测试进阶篇

作者:熊训德

本文档说明 go 语言自带的测试框架未提供或者未方便地提供的测试方案,主要是用于解决写单元测试中比较头痛的依赖问题。也就是伪造模式,经典的伪造模式有桩对象( stub ),模拟对象( mock )和伪对象( fake )。比较幸运的是,社区有丰富的第三方测试框架支持支持。下面就对笔者亲身试用并实践到项目中的几个框架做介绍:

1.gomock

文档地址:package gomock

gomock 模拟对象的方式是让用户声明一个接口,然后使用 gomock 提供的 mockgen 工具生成 mock 对象代码。要模拟( mock )被测试代码的依赖对象时候,即可使用 mock 出来的对象来模拟和记录依赖对象的各种行为:比如最常用的返回值,调用次数等等。文字叙述有点抽象,直接上代码:

dick.go 中 DickFunc 依赖外部对象 OutterObj,本示例就是说明如何使用 gomock 框架控制所依赖的对象。

func DickFunc( outterObj MockInterface,para int)(result int){
    fmt.Println("This init DickFunc")
    fmt.Println("call outter.func:")

    return outterObj.OutterFunc(para)
}

mockgen工具命令是:

mockgen -source {source_file}.go -destination {dest_file}.go

比如,本示例即是:

mockgen -source src_mock.go -destination dst_mock.go

执行完后,可在同目录下找到生成的 dst_mock.go 文件,可以看到 mockgen 工具也实现了接口:

接下来就可以使用 mockgen 工具生成的 NewMockInterFace 来生产 mock 对象,使用这个 mock 对象。 OutterFunc() 这个函数,gomock在控制mock类时支持链式编程的方式,其原理和其他链式编程类似一直维持了一个Call对象,把需要控制的方法名,入参,出参,调用次数以及前置和后置动作等,最后使用反射来调用方法,所以这个Call对象是mock对象的代理。jmockit的早期版本也是jdk自带的java.reflect.Proxy动态代理实现的(最近的版本是动态Instrumentation配合代理模式)。

在本示例中只简单的更改了返回值,抛砖引玉:

func TestDickFunc(t *testing.T ){
   mockCtrl := gomock.NewController(t)
//defer mockCtrl.Finish()

   mockObj := dick.NewMockMockInterface(mockCtrl)
   mockObj.EXPECT().OutterFunc(3).Return(10)

   result :=dick.DickFunc(mockObj,3)
   t.Log("resutl:",result)

}

使用go test命令执行这个单测:

从结果看:本来应该输出3,最后输出就是10,和其他语言mock框架相似,生产出来的Mock对象不用自己去重定义这么麻烦。

更多示例可以查看官网一个囊括gomock几乎所有功能的例子:package user

2.httpexcept

由于go在网络架构上的优秀封装,使得go在很多网络场景被广泛使用,而http协议是其中重要部分,在面对http请求的时候,可以对http的client进行测试,算是mock的特殊应用场景。

看一个简单的示例就轻松的看懂了:

func TestHttp(t *testing.T) {

    handler := FruitServer()

    server := httptest.NewServer(handler)
    defer server.Close()

    e := httpexpect.New(t, server.URL)

    e.GET("/fruits").
        Expect().
        Status(http.StatusOK).JSON().Array().Empty()
}

其中还支持对不同方法(包括Header,Post等)的构造以及返回值Json的自定义,更多细节查看其官网

3.testify

还有一个testify使用起来可以说兼容了《一》中的gocheck和gomock,但是其mock使用稍微有点烦杂,使用继承tetify.Mock(匿名组合)重新实现需要Mock的接口,在这个接口里使用者自己使用Called(反射实现)被Mock的接口。

《单元测试的艺术》中认为stub和mock最大的区别就依赖对象是否和被测对象有交互,而从结果看就是桩对象不会使测试失败,它只是为被测对象提供依赖的对象,并不改变测试结果,而mock则会根据不同的交互测试要求,很可能会更改测试的结果。说了这么多理论,但其实这两种方法都不是割裂的,所以gomock框架除了像其名字一样可以模拟对象以外,还提供了桩对象的功能(stub)。以其实现来说,更像是一个桩对象的注入。但是因为兼容了多个有用的功能,所以其在社区最为火爆。

具体用法可参考其github主页

4.go-sqlmock

还有一种比较常见的场景就是和数据库的交互场景,go-sqlmock是sql模拟(Mock)驱动器,主要用于测试数据库的交互,go-sqlmock提供了完整的事务的执行测试框架,最新的版本(16.11.02)还支持prepare参数化提交和执行的Mock方案。

比如有这样的被测函数:

func recordStats(db *sql.DB, userID, productID int64) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return
    }

    defer func() {
        switch err {
        case nil:
            err = tx.Commit()
        default:
            tx.Rollback()
        }
    }()

    if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
        return
    }
    if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
        return
    }
    return
}

func main() {

    db, err := sql.Open("mysql", "root@/root")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    if err = recordStats(db, 1 , 5 ); err != nil {
        panic(err)
    }
}

单测时:

func TestShouldUpdateStats(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("mock error: '%s' ", err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers")
          .WithArgs(2, 3)
          .WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    if err = recordStats(db, 2, 3); err != nil {
        t.Errorf("exe error: %s", err)
    }

    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("not implements: %s", err)
    }
}

//测试回滚
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("mock error: '%s'", err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers")
           .WithArgs(2, 3)
           .WillReturnError(fmt.Errorf("some error"))
    mock.ExpectRollback()

    // 执行被测方法,有错
    if err = recordStats(db, 2, 3); err == nil {
        t.Errorf("not error")
    }

    // 执行被测方法,mock对象
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("not implements: %s", err)
    }
}

更多例子和详情,请查看官网:DATA-DOG/go-sqlmock

介绍了这么多框架,最后需要说明的也可能最重要的是写代码时就应该考虑代码是可被测试的。要使得单元测试容易写,或者说代码容易被测,其实很重要的一个部分就是被测代码本身是容易被测的,也就是说在设计和编写代码的时候就应该先想到相好如何单元测试,甚至有人提出可以先写单元测试,再写具体被测代码。因为一个接口(或者称为单元)在被设计好后,它实现就确定了,实际效果也确定了。这种方式被称作测试驱动开发(Test-Driven Development, TDD)。而对于已经写好的代码,很大程度上不好测试,有一种方式是测试性重构,就是为了更好的测试而进行重构。这些一定程度上来说并了解这些框架更重要,有意向可以,可以查阅有关两本书《单元测试的艺术(第2版)》《xUnit测试模式》

参考

《单元测试的艺术 ( 第2版 ) 》 《 xUnit 测试模式 》 如何测试 Go 代码 - 单元测试 Go Testing Toolbox

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小白安全

绕过软WAF攻略

现在软waf较为多,就在今年夏天苦逼挖洞的日子里经常遇到360主机卫士,安全狗,云锁之类的软waf进行拦截,经常碰到如下拦截提示: ? ? ? 看到以上...

4355
来自专栏玄魂工作室

看代码学PHP渗透(3) - 实例化任意对象漏洞

大家好,我们是红日安全-代码审计小组。最近我们小组正在做一个PHP代码审计的项目,供大家学习交流,我们给这个项目起了一个名字叫 PHP-Audit-Labs 。...

2021
来自专栏信安之路

使用 Wave 文件绕过 CSP 策略

CSP 全称 Content Security Policy,即内容安全策略。CSP 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括 XSS 和注入...

950
来自专栏Seebug漏洞平台

Mirai源码分析

1. 背景概述 最近的德国断网事件让Mirai恶意程序再次跃入公众的视线,相对而言,目前的IoT领域对于恶意程序还是一片蓝海,因此吸引了越来越多的人开始涉足这趟...

4137
来自专栏编程思想之路

WiFiAp探究实录--功能实现与源码分析

Android虐我千百遍,我待Android如初恋。 ——————编辑于2017-08-02——————— wifi热点说的是wifiAp相...

9358
来自专栏信安之路

HCTF2017的三个WriteUp

解决方法就是先 undefine 掉函数,再右键选择 Code,最后 Create function 就可以正常反编译了。

900
来自专栏信安之路

PHP代码审计

代码审计顾名思义就是检查源代码中的缺点和错误信息,分析并找到这些问题引发的安全漏洞,并提供代码修订措施和建议。

1120
来自专栏漏斗社区

送你们几个字!对!就是MACCMS注入!

文末答题赢福利 0x00 相关信息 源码信息:maccms8_mfb(苹果CMS视频分享程序 8.0 | 2017.09.27 发布) 源码下载:http:...

41110
来自专栏安恒网络空间安全讲武堂

用css绕过同源策略跨域窃取数据

用css绕过同源策略跨域窃取数据 序言 如果你和我一样无聊,你可能遇到过这种潜在的攻击 -》https://www.w3.org/TR/CSP2/#securi...

2129
来自专栏安恒网络空间安全讲武堂

技术分享 | 深入分析APPCMS<=2.0.101 sql注入漏洞

目录 0x00 前言 0x01 漏洞分析--代码审计 0x02 漏洞利用 1.sql注入出后台账号、密码、安全码 2.二次漏洞利用:sql注入+csrf...

2368

扫码关注云+社区