golang-单元测试和mock框架的介绍和推荐


背景介绍:探索golang 的单元测试框架,看一下哪种框架是结合业务体验更好的。
推荐 和 不推荐 使用的框架,我都会在标题中 标注出来,没有标注的表示体验一般,但也没有特别的缺点,观望态度

一、单元测试框架介绍

1、原生testing

1.1 示例

func TestModifyArr(t *testing.T) {
	arr := [3]int{0, 1, 2}
	modifyArr(arr)
	if 112233 == arr[0] {
		t.Logf("[TestModifyArr] 测试修改数组元素成功!")
	} else if 0 == arr[0] {
		t.Errorf("[TestModifyArr] 测试修改数组元素失败!元素未修改")
	} else {
		t.Errorf("[TestModifyArr] 测试修改数组元素失败!未知元素: %d", arr[0])
	}
}

注意:使用 t.Errorf 的同时,单测也会被置为失败(但是测试不会马上停止,用 FailedNow 或者 Fatalf 才会)

1.2 扩展:Table-Driven 设计思想

其实就是将多个测试用例封装到数组中,依次执行相同的测试逻辑

即使是用其他测试框架,这个设计思想也是挺有用的,用例多的时候可以简化代码量

示例:

var (
	powTests = []struct {
		base     float64
		power    float64
		expected float64
	}{
		{1, 5, 1},
		{2, 4, 16},
		{3, 3, 27},
		{5, 0, 1},
	}
)

// 测试一些math 包的计算方法
func TestMathPkgMethodByTesting(t *testing.T) {
	for index, currentTest := range powTests {
		if currentTest.expected != math.Pow(currentTest.base, currentTest.power) {
			t.Errorf("[TestMathPkgMethod] %d th test: %.2f the power of %.2f is not expected: %.2f",
				index, currentTest.base, currentTest.power, currentTest.expected)
		}
	}
	t.Logf("[TestMathPkgMethod] All test passed!")
}

1.3 并行测试

使用方式:在测试代码中执行:t.Parallel(),该测试方法就可以和其他测试用例一起并行执行。
场景:一般在 多个用例需要同时执行,比如测试生产和消费的时候才需要用到。
但是个人不建议这么做,因为这有点违背“单测”的概念:一个单测就测试一个功能。类似的场景也可以通过 单测中设置 channel 多协程来实现。

2、goconvey

2.1 示例

引入方式:
go get github.com/smartystreets/goconvey/convey

import 方式:
import (
	. "github.com/smartystreets/goconvey/convey"
)

// 提醒:诸如 goconvey、gomonkey 这些工具类 最好都用这种import方式,减少使用其内部方法的代码长度,让代码更加简洁
func TestMathPkgMethodByConvey(t *testing.T) {
	Convey("Convey test pow", t, func() {
		for _, currentTest := range powTests {
			So(math.Pow(currentTest.base, currentTest.power), ShouldEqual, currentTest.expected)
		}
	})
}

So 这个方法结构对一开始接触 GoConvey 的同学可能有点不太好理解,这里结合源码简单说明一下:

// source code: github.com\smartystreets\goconvey@v1.6.4\convey\context.go
type assertion func(actual interface{}, expected ...interface{}) string
......
func (ctx *context) So(actual interface{}, assert assertion, expected ...interface{}) {
	if result := assert(actual, expected...); result == assertionSuccess {
		ctx.assertionReport(reporting.NewSuccessReport())
	} else {
		ctx.assertionReport(reporting.NewFailureReport(result))
	}
}

关键是对So 参数的理解,总共有三个参数:
actual: 输入
assert:断言
expected:期望值

assert 断言看定义,其实也是一个方法,但其实Convey 包已经帮我们定义了大部分的基础断言了:

// source code: github.com\smartystreets\goconvey@v1.6.4\convey\assertions.go
var (
	ShouldEqual          = assertions.ShouldEqual
	ShouldNotEqual       = assertions.ShouldNotEqual
	ShouldAlmostEqual    = assertions.ShouldAlmostEqual
	ShouldNotAlmostEqual = assertions.ShouldNotAlmostEqual
	ShouldResemble       = assertions.ShouldResemble
	ShouldNotResemble    = assertions.ShouldNotResemble
	.....

诸如 判断相等、大于小于 这些判断方法都是可以直接拿来用的。

2.2 双层嵌套

func TestMathPkgMethodByConvey(t *testing.T) {
	// 双层嵌套
	Convey("Convey test multiple test", t, FailureHalts, func() {
		Convey("Failed test", func() {
			So(math.Pow(5, 2), ShouldEqual, 26)
			log.Printf("[test] 5^3 = 125? to execute!")
			So(math.Pow(5, 3), ShouldEqual, 125)
		})

		Convey("Success test", func() {
			log.Printf("[test] 5^2 = 25? to execute!")
			So(math.Pow(5, 2), ShouldEqual, 25)
		})
	})
}

注意:内层的Convey 不再需要加上 testing 对象
注意:子Convey 的执行策略是并行的,因此前面的子Convey 执行失败,不会影响后面的Convey 执行。但是一个Convey 下的子 So,执行是串行的。

2.3 跳过测试

如果有的测试在本次提交 还没有测试完全,可以先用 TODO + 跳过测试的方式,先备注好,下次commit 的时候再完善

SkipConvey:跳过当前Convey 下的所有测试
SkipSo:跳过当前断言

2.4 设置失败后的执行策略

默认 一个Convey 下的多个 So 断言,是失败后就终止的策略。如果想要调整,在Convey 参数中加上 失败策略即可,比如设置 失败后继续,就用 FailureContinues

// source code: github.com\smartystreets\goconvey@v1.6.4\convey\doc.go
const (
    ......
	FailureContinues FailureMode = "continue"

	......
	FailureHalts FailureMode = "halt"

	......
	FailureInherits FailureMode = "inherits"
)

但是要注意:这里的失败后策略是针对 一个Convey 下的多个So 断言来说的,而不是一个Convey 下的多个子Convey。所以接下来会讲到Convey 的执行机制:是并行的。

2.5 子 Convey 并发执行的原理简述

GoConvey 底层是借助了 jtolds/gls 这个库实现了 goroutine 的管理,也实现了 多个子Convey 的并发执行。

// source code: github.com\smartystreets\goconvey@v1.6.4\convey\context.go
func (ctx *context) Convey(items ...interface{}) {
	......

	if inner_ctx.shouldVisit() {
		ctxMgr.SetValues(gls.Values{nodeKey: inner_ctx}, func() {
			// entry.Func 就是实际的测试方法
			inner_ctx.conveyInner(entry.Situation, entry.Func)
		})
	}
}

// source code: github.com\jtolds\gls@v4.20.0+incompatible\context.go
func (m *ContextManager) SetValues(new_values Values, context_call func()) {
	......

	// 该方法会判断 是否满足并发执行的条件
	EnsureGoroutineId(func(gid uint) {
		...... // 解析传入的 context 参数

		context_call()
	})
}

了解有限,这里不会展开讲 gls 库的原理,借助一些文档,了解到gls 实际就是通过 go 底层的api 对 GPM 模型进行管理,在满足一定条件的时候,会将子Convey 提交到子协程中执行(默认)

对gls 库感兴趣,想了解其 底层 是怎么管理协程的话,可以参考:
gls 官方github 地址

gls godoc

3、testify(推荐)

其实Testify的用法 和 原生的testing 的用法差不多,都是比较清晰的断言定义。

它提供 assert 和 require 两种用法,分别对应失败后的执行策略,前者失败后继续执行,后者失败后立刻停止。 但是它们都是单次断言失败,当前Test 就失败。

func TestGetStudentById(t *testing.T) {
	currentMock := gomonkey.ApplyFunc(dbresource.NewDBController, dbresource.NewDBMockController)
	defer currentMock.Reset()
	schoolService := schoolservice.NewSchoolService()
	student := schoolService.GetStudentById("1")
	
	assert.NotEqual(t, "", student.Name)
	require.Equal(t, studentsql.TEST_STUDENT_NAME, student.Name)
}

4、测试框架总结

这里简单总结一下几个测试框架:个人觉得 GoConvey 的语法 对业务代码侵入有点严重,而且理解它本身也需要一些时间成本,比如 testify 逻辑清晰。单元测试逻辑本身就要求比较简单,综上,还是更推荐用testify

二、mock框架介绍

1、gostub(不推荐)

1.1 基本使用

go get github.com/prashantv/gostub
func TestGetLocalIp(t *testing.T) {
	// 给变量打桩
	varStub := Stub(&testGlobalInt, 100)
	defer varStub.Reset()
	log.Printf("[test mock] mock var: %d", testGlobalInt)

	// 给方法打桩
	var getIpFunc = system.GetOutboundIP
	funcStub := StubFunc(&getIpFunc, "1.2.3.4")
	defer funcStub.Reset()
}

1.2 和 GoConvey 结合示例

在这里插入图片描述

1.3 不推荐使用的原因

主要是局限性太多:
gostub 由于方法的mock 还必须声明出 variable 才能进行mock,即使是 interface method 也需要这么来定义,不是很方便

另外,如果需要mock 的方法,入参和返回的 数量都是长度不固定的数组类型,可能就没法定义mock 了

最后,同一个方法,如果需要mock 多种入参出参场景,gostub 也无法实现。这就非常麻烦,mock 不同的参数场景应该算是mock 的基本功能了

2、gomock

官方维护的 mock 框架,只要是对象 + 接口的数据结构,基本都能通过gomock 来直接编写 不同场景的mock。
之前写过一篇关于 gomock 如何使用的基本介绍,总体来说,是比较适用于框架场景的,比如 通过 protobuf 定义并生成的对外对象和接口,如果能自动生成 gomock 代码,对开发就比较方便了。但是对业务代码 并不是特别适合,因为业务内部往往还要定义非常多的对象,每个对象都要生成mock 还是有点麻烦的。

参考博客-Golang 单元测试详尽指引

3、gomonkey(推荐)

参考博客-gomonkey调研文档和学习

import "github.com/agiledragon/gomonkey/v2"

3.1 给方法打桩

func TestGetAbsolutePath(t *testing.T) {
	// 打桩方法
	funcStub := ApplyFunc(config.GetAbsolutePath, testGetAbsolutePath)
	defer funcStub.Reset()
	log.Printf("config path: %s", config.GetAbsolutePath())
}

总体来说,和 gostub 的使用方法非常类似,也是要通过变量单独指定方法,并设置mock。执行 ApplyFunc 方法
不同的地方在于 StubFunc 直接定义方法的出参(行为结果),但是 ApplyFunc 还需要定义 方法具体的动作(行为本身

3.2 给方法打序列桩

func TestGetAbsolutePath(t *testing.T) {
	// 方法序列打桩
	retArr := []OutputCell{
		{Values: Params{"./testpath1"}},
		{Values: Params{"./testpath2"}},
		{Values: Params{"./testpath3"}, Times: 2},
	}
	ApplyFuncSeq(config.GetAbsolutePath, retArr)

	log.Printf("config path: %s", config.GetAbsolutePath())
	log.Printf("config path: %s", config.GetAbsolutePath())
	log.Printf("config path: %s", config.GetAbsolutePath())
	log.Printf("config path: %s", config.GetAbsolutePath())
}

3.3 给全局变量打桩

在这里插入图片描述
用法和gostub 的Stub 方法类似,不多赘述了。

另外还有什么 ApplyMethod (为对象的指定方法打桩)、ApplyMethodSeq 等,用法依然是和ApplyFunc 很类似了。详细可以看参考博客,或者直接看源码中的测试例子。

四、总结和展望

这里介绍了单测、mock 的几个通用框架的使用,并总结出 testify + gomonkey 是比较直观好用的框架。
我会在下一篇博客中 介绍这两个测试框架 如何更好地结合实际项目,编写完整的、含mock 的单元测试。

单元测试是一种软件测试方法,用于测试应用程序的各个部分是否按照预期进行工作。在 Golang 中,我们可以使用 Mock 对象来编写单元测试Mock 对象可以模拟应用程序的行为,以便进行测试。 以下是使用 Golang 编写 Mock 单元测试的步骤: 1. 安装 Mock 库 在 Golang 中,我们可以使用各种 Mock 库,例如 GoMockMockery 等。在本例中,我们将使用 GoMock 库。请使用以下命令安装 GoMock: ``` go get github.com/golang/mock/gomock ``` 2. 创建 Mock 对象 在 Golang 中,我们可以使用 Mock 对象来模拟应用程序的行为。我们可以使用 GoMock 库来创建 Mock 对象。以下是一个示例: ``` // mock_test.go package main import ( "testing" "github.com/golang/mock/gomock" ) type MockDatabase struct { mock.Mock } func (m *MockDatabase) GetUser(id int) (string, error) { args := m.Called(id) return args.String(0), args.Error(1) } ``` 在上面的示例中,我们创建了一个名为 `MockDatabase` 的结构体,该结构体包含一个 `mock.Mock` 字段。我们还定义了一个 `GetUser` 方法,该方法接受一个 `id` 参数,并返回一个字符串和一个错误。在该方法中,我们使用 `m.Called` 方法来模拟一个函数调用,并返回模拟的结果。 3. 编写测试用例 编写测试用例时,我们可以使用 `gomock.NewController` 方法来创建一个 Mock 控制器,并使用 `controller.Finish` 方法来释放资源。以下是一个示例: ``` // main_test.go package main import ( "testing" "github.com/golang/mock/gomock" ) func TestGetUser(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockDatabase := NewMockDatabase(ctrl) mockDatabase.EXPECT().GetUser(1).Return("Alice", nil) app := App{ Database: mockDatabase, } user, err := app.GetUser(1) if err != nil { t.Errorf("Unexpected error: %v", err) } if user != "Alice" { t.Errorf("Expected user to be Alice, but got %v", user) } } ``` 在上面的示例中,我们创建了一个名为 `TestGetUser` 的测试用例。在该测试用例中,我们使用 `gomock.NewController` 方法创建了一个 Mock 控制器,并使用 `ctrl.Finish` 方法释放资源。我们还创建了一个名为 `mockDatabase` 的 Mock 对象,并使用 `mockDatabase.EXPECT().GetUser(1).Return("Alice", nil)` 方法来模拟 `GetUser` 方法的行为。最后,我们使用模拟的 `Database` 对象创建了一个 `App` 对象,并调用 `GetUser` 方法进行测试。 总结 在 Golang 中,我们可以使用 Mock 对象来编写单元测试。使用 Mock 对象可以模拟应用程序的行为,以便进行测试。在本文中,我们介绍了使用 GoMock 库编写 Mock 单元测试的步骤,并提供了示例代码。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值