生三从境界:昨夜西风凋碧树,独上高楼,望尽天涯路。 衣带渐宽终不悔,为伊消得人憔悴。 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。人

随笔 - 151  文章 - 0  评论 - 117  阅读 - 108万 

 


1 原则

  • 单元测试文件名必须以 xxx_test.go 命名
  • 方法必须是 TestXxx 开头,建议风格保持一致:驼峰,XXX标识需要测试的函数名
  • 方法参数必须 t *testing.T
  • 测试文件和被测试文件必须在一个包中
  • 优先核心函数热点工具类函数
  • 写明每个单测的注释,单测作用,比如:

测试用例 1:输入 4,输出 2。

测试用例 2:输入-1,输出 0。

2 框架使用

  • GoConvey 和其他框架的兼容性较好,可直接在终端窗口和浏览器上使用,自带大量的标准断言函数,可以管理和运行测试用例
  • goMonkey 在运行时通过汇编语句重写可执行文件,将待打桩函数或方法的实现跳转到桩实现,原理和热补丁类似。

通过 Monkey,我们可以解决函数或方法的打桩问题,但 Monkey 不是线程安全的,不要将 Monkey 用于并发的测试中

可以为全局变量、函数、过程、方法打桩,同时避免了gostub对代码的侵入

特性列表:

支持为一个函数打一个桩

支持为一个成员方法打一个桩

支持为一个接口打一个桩

支持为一个全局变量打一个桩

支持为一个函数变量打一个桩

支持为一个函数打一个特定的桩序列

支持为一个成员方法打一个特定的桩序列

支持为一个函数变量打一个特定的桩序列

支持为一个接口打一个特定的桩序列

缺陷:

对inline函数打桩无效

不支持多次调用桩函数(方法)而呈现不同行为的复杂情况

  • GoMock 是由 Golang 官方开发维护的测试框架,实现了较为完整的基于 interface 的 Mock 功能,能够与 Golang 内置的 testing 包良好集成,也能用于其它的测试环境中。

GoMock 测试框架包含了 GoMock 包和 mockgen 工具两部分,其中 GoMock 包完成对桩对象生命周期的管理,mockgen 工具用来生成 interface 对应的 Mock 类源文件

缺陷:

只有以接口定义的方法才能mock,需要用mockgen生成源文件,然后用gomock去实现自己想要的数据,用法稍重。

  • gostub 可以为全局变量、函数、过程打桩,比gomock轻量,不需要依赖接口

缺陷:

对项目源代码有侵入性,即被打桩方法必须赋值给一个变量,只有以这种形式定义的方法才能别打桩,gostub 由于方法的mock 还必须声明出 variable 才能进行mock,即使是 interface method 也需要这么来定义,不是很方便

  • 外层框架——goconvey

项目代码很多逻辑比较复杂,需要编写不同情况下的测试用例,用goconvey组织的测试代码逻辑层次比较清晰,有着较好的可读性和可维护性。断言方面感觉convey和testify功能差不多。不过convey没有testify社区活跃度高,后续使用convey时碰到一些问题,都不太容易找到解决办法

  • 函数mock——gomonkey。项目代码基本都不是基于interface实现的,所以不太方便使用gomock,项目目前运行稳定,所以也不想因为单元测试重构原来的代码,所以也不太方便gostub,基本符合我们对函数打桩的需求。

 

  • 持久层mock——sqlmock。我们持久层的框架是gorm。当时考虑2种方法进行mock,一种是使用gomonkey对gorm的函数进行mock,另一种则是选用sqlmock。如果使用gomonkey的话需要对连续调用的gorm函数都进行mock,过于繁杂。而用sqlmock的话只需匹配对应的sql语句即可

 

3. 安装

go get github.com/smartystreets/goconvey
go install github.com/smartystreets/goconvey

运行:

./goconvey.exe

页面访问: http://127.0.0.1:8080

 

4. 样例

原程序

复制代码
package goconvey

import (
  "errors"
)

func Add(a, b int) int {
  return a + b
}

func Subtract(a, b int) int {
  return a - b
}

func Multiply(a, b int) int {
  return a * b
}

func Division(a, b int) (int, error) {
  if b == 0 {
    return 0, errors.New("被除数不能为 0")
  }
  return a / b, nil
}
复制代码

 

 test文件

复制代码
package goconvey

import (
    "testing"
    "github.com/smartystreets/goconvey/convey"
)

func TestAdd(t *testing.T) {
    convey.Convey("将两数相加", t, func() {
        convey.So(Add(1, 2), convey.ShouldEqual, 3)
    })
}

func TestSubtract(t *testing.T) {
    convey.Convey("将两数相减", t, func() {
        convey.So(Subtract(1, 2), convey.ShouldEqual, -1)
    })
}

func TestMultiply(t *testing.T) {
    convey.Convey("将两数相乘", t, func() {
        convey.So(Multiply(3, 2), convey.ShouldEqual, 6)
    })
}

func TestDivision(t *testing.T) {
    convey.Convey("将两数相除", t, func() {
        convey.Convey("除以非 0 数", func() {
            num, err := Division(10, 2)
            convey.So(err, convey.ShouldBeNil)
            convey.So(num, convey.ShouldEqual, 5)
        })

        convey.Convey("除以 0", func() {
            _, err := Division(10, 0)
            convey.So(err, convey.ShouldNotBeNil)
        })
    })
}
复制代码

 

5 .断言函数

General Equality //通用比较

So(thing1, ShouldEqual, thing2) //相等

So(thing1, ShouldNotEqual, thing2) //不等

So(thing1, ShouldResemble, thing2) // a deep equals for arrays, slices, maps, and structs

So(thing1, ShouldNotResemble, thing2) //深度比较不相等

So(thing1, ShouldPointTo, thing2) //地址指向

So(thing1, ShouldNotPointTo, thing2) //地址不是指向

So(thing1, ShouldBeNil) //等于 nil

So(thing1, ShouldNotBeNil) //不等于 nil

So(thing1, ShouldBeTrue) //等于true

So(thing1, ShouldBeFalse) //等于false

So(thing1, ShouldBeZeroValue) //等于0值

 

Numeric Quantity comparison //数值比较

So(1, ShouldBeGreaterThan, 0) //大于

So(1, ShouldBeGreaterThanOrEqualTo, 0) //大于等于

So(1, ShouldBeLessThan, 2) //小于

So(1, ShouldBeLessThanOrEqualTo, 2) //小于等于

So(1.1, ShouldBeBetween, .8, 1.2) //区间内

So(1.1, ShouldNotBeBetween, 2, 3) //不在区间内

So(1.1, ShouldBeBetweenOrEqual, .9, 1.1) //区间取上下线

So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000) //不再区间

So(1.0, ShouldAlmostEqual, 0.99999999, .0001) // 容差比较,允许多的误差 tolerance is optional; default 0.0000000001

So(1.0, ShouldNotAlmostEqual, 0.9, .0001) //容差比较,不允许多少的误差

 

Collections //内建类型比较

So([]int{2, 4, 6}, ShouldContain, 4) //包含

So([]int{2, 4, 6}, ShouldNotContain, 5) //不包含

So(4, ShouldBeIn, ...[]int{2, 4, 6}) //在列表内

So(4, ShouldNotBeIn, ...[]int{1, 3, 5}) //不在列表内

So([]int{}, ShouldBeEmpty) //空列表

So([]int{1}, ShouldNotBeEmpty) //非空列表

So(map[string]string{"a": "b"}, ShouldContainKey, "a") //map 包含key

So(map[string]string{"a": "b"}, ShouldNotContainKey, "b") //map不包含key

So(map[string]string{"a": "b"}, ShouldNotBeEmpty) //非空map

So(map[string]string{}, ShouldBeEmpty) //空列表

So(map[string]string{"a": "b"}, ShouldHaveLength, 1) //长度 supports map, slice, chan, and string

 

Strings //字符串比较

So("asdf", ShouldStartWith, "as") //以某字符开头

So("asdf", ShouldNotStartWith, "df") //不是以某字符串开头

So("asdf", ShouldEndWith, "df") //以某字符串结尾

So("asdf", ShouldNotEndWith, "df") //不是以某字符串结尾

So("asdf", ShouldContainSubstring, "sd") //包含子串

So("asdf", ShouldNotContainSubstring, "er") //不包含子串

So("adsf", ShouldBeBlank) //空字符

So("asdf", ShouldNotBeBlank) //非空字符

 

panic //panic断言

So(func(), ShouldPanic) //发送panic

So(func(), ShouldNotPanic) //没有发生panic

So(func(), ShouldPanicWith, "") //以什么报错发什么 panic or errors.New("something")

So(func(), ShouldNotPanicWith, "") //不是以某错发生panic or errors.New("something")

 

Type checking //类型判断

So(1, ShouldHaveSameTypeAs, 0) //是否类型相同

So(1, ShouldNotHaveSameTypeAs, "asdf") //是否类型不相同

 

time.Time (and time.Duration) //时间判断

So(time.Now(), ShouldHappenBefore, time.Now()) //发生前

So(time.Now(), ShouldHappenOnOrBefore, time.Now()) //发生前或者当前时间

So(time.Now(), ShouldHappenAfter, time.Now()) //发生后

So(time.Now(), ShouldHappenOnOrAfter, time.Now()) //发生在之后或者当前时间

So(time.Now(), ShouldHappenBetween, time.Now(), time.Now()) //在某个时间区间

So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now()) //在区间内,并且取边界

So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now()) //不相等或者不再区间内

So(time.Now(), ShouldHappenWithin, duration, time.Now()) //以某个时间间隔固定发生

So(time.Now(), ShouldNotHappenWithin, duration, time.Now()) //不是以某时间间隔发生

6 .Mock 方法

复制代码
// ApplyFunc mock常规函数
patches := ApplyFunc(GetCmdbInsts, func(dims *models.DimsInfo) ([]Endpoint, error) {
    return endpointList, nil
})
defer patches.Reset()

// ApplyMethod mock方法函数
var test *ConsistentHashRing
patches.ApplyMethod(reflect.TypeOf(test), "GetNode", func(_ *ConsistentHashRing, pk string) (string, error) {
    return "", errors.New("get judge node fail")
})
defer patches.Reset()

// ApplyGlobalVar mock全局变量
patches := ApplyGlobalVar(&num, 150)
defer patches.Reset()

// ApplyFuncSeq mock 函数序列桩
patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
defer patches.Reset()

output, err := fake.ReadLeaf("")
if err != nil {
    log.Fatalf("Error reading leaf: %v", err)
}
So(output, ShouldEqual, info1)

output, err = fake.ReadLeaf("")
if err != nil {
    log.Fatalf("Error reading leaf: %v", err)
}
So(output, ShouldEqual, info2)

// ApplyFuncVar mock 函数变量
patches := ApplyFuncVar(&fake.Marshal, func(_ interface{}) ([]byte, error) {
    return []byte(str), nil
}) // fake.Marshal是函数变量
defer patches.Reset()

// ApplyFuncVarSeq 函数变量序列
patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
defer patches.Reset()

bytes, err := fake.Marshal("")
if err != nil {
    log.Fatalf("Error marshaling: %v", err)
}
So(string(bytes), ShouldEqual, info1)

bytes, err = fake.Marshal("")
if err != nil {
    log.Fatalf("Error marshaling: %v", err)
}
So(string(bytes), ShouldEqual, info2)

// ApplyMethodSeq mock 成员方法打序列桩
patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
defer patches.Reset()

output, err := e.Retrieve("")
if err != nil {
    log.Fatalf("Error retrieving: %v", err)
}
So(output, ShouldEqual, info1)

// mock 接口打桩,同接口打桩
e := &fake.Etcd{}
info := "hello interface"
patches.ApplyMethod(reflect.TypeOf(e), "Retrieve", func(_ *fake.Etcd, _ string) (string, error) {
    return info, nil
})

output, err := db.Retrieve("")
if err != nil {
    log.Fatalf("Error retrieving from db: %v", err)
}
So(output, ShouldEqual, info)
复制代码

 

7 .参考链接

- https://mp.weixin.qq.com/s/eAptnygPQcQ5Ex8-6l0byA

- https://www.cnblogs.com/youhui/articles/11265947.html

- https://knapsackpro.com/testing_frameworks/difference_between/goconvey/vs/go-testify

- https://github.com/smartystreets/goconvey

- https://github.com/stretchr/testify/

- https://studygolang.com/topics/2992

- https://geektutu.com/post/quick-gomock.html gomock 的使用

- https://blog.marvel6.cn/2020/01/test-and-mock-db-by-xorm-with-the-help-of-convey-and-sqlmock/ 参考测试XORM

- https://github.com/dche423/dbtest/blob/master/pg/repository_test.go 参考测试gorm

 

 

posted on   测试开发喵  阅读(609)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示