代码测试

时间:2023-01-30
本文章向大家介绍代码测试,主要内容包括单元测试、单元测试模板、单元测试自动化生成、性能测试、示例测试、TestMain函数、mock测试、mockgen说明、mock代码编写单元测试、使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

go语言通过自带的testing框架,可以用来实现单元测试与性能测试,通过go test命令来执行单元测试或性能测试。

go test执行单元测试是以包为单位的,如果没有指定包,则默认使用执行命令时所在的包。遍历包下以*_test.go结尾的文件,执行以TestBenchmark, Example开头的测试函数。

单元测试

单元测试用例函数以Test开头,例如TestXxx或者Test_xxx。函数的参数必须是testing.T,可以调用testing.T的Error 、Errorf 、FailNow 、Fatal 、FatalIf 方法,来说明测试不通过;调用 Log 、Logf 方法来记录测试信息。

执行go test命令就会执行所在包下的所有测试函数。在测试过程中还有如下常用的参数:

  • -v:显示测试函数的运行细节。
  • -run <regexp>:指定要执行的测试函数
  • -count N:指定测试函数指定的次数

单元测试模板

测试一个函数,我们通过会测试多个输入与输出是否正确,测试函数的编写也有了较为通用的模板

func TestFunc(t *testing.T) {
	// 开始写单元测试逻辑
	type args struct {
		// 被测试函数的参数
	}
	tests := []struct {
		name    string
		args    args
		want TYPE //args作为参数的时候,被测试函数预期的输出结果
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		if got := Func(tt.args.x, tt.args.y); got != tt.want {
			t.Errorf("Func(args.x, args.y) = %f, want %v", got, tt.want)
		}
	}
}

具体的细节可能有所不同,但是一般较为规范的单元测试都符合上面的格式。

在上面的got != tt.want的比较,我们可以使用github.com/stretchr/testify/assert 包来使得比较更加方便和简单。

单元测试自动化生成

单元测试既然可以有通用的模板,那么当然就可以有工具来帮助我们生成这个模板。比如gotests这个工具。

执行如下命令安装gotests

$ go get -u github.com/cweill/gotests/...

gotests 命令执行格式为:gotests [options] [PATH] [FILE] ...。gotests 可以为PATH下的所有 Go 源码文件中的函数生成测试代码,也可以只为某个FILE中的函数生成测试代码。

例如$ gotests -all -w .命令就会为当前目录下的所有函数生成测试代码。然后只需要在模板的TODO位置出添加具体的case即可。

性能测试

性能测试函数必须以Benchmark开头,例如BenchmarkXxx或者Benchmark_xxx。并且其函数参数必须为b testing.B,函数内b.N作为循环测试,其中N会在运行时动态调整,直到性能测试函数能够运行足够长的时间,来进行可靠的计时。

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        RandInt()
    }
}

go test命令默认不会执行性能测试函数,需要通过参数-bench <regexp>来运行指定的测试函数,go test -bench=".*"表示执行所有的性能测试函数。

在性能测试中,如果被测试函数执行前需要进行一些耗时的准备操作,那么可以在准备工作完成后重置计时,或者先暂停计时准备工作完成后再启动计时。


func BenchmarkXxx(b *testing.B) {
	// 准备工作
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
		//...
    }
}

func BenchmarkXxx(b *testing.B) {
	b.StopTimer() // 调用该函数停止压力测试的时间计数  
	// 准备工作
	b.StartTimer() // 重新开始时间
    for i := 0; i < b.N; i++ {
		//...
    }
}

性能测试中我们还关注如下的参数

  • -benchmem:性能测试中关注的内存指标。如 每次执行分配的内存大小(越小,占用内存越少)以及每次执行内存的分配次数(越小,性能越好)。
  • -benchtime:指定测试时间和循环执行测试,格式为Nx,例如10s表示10秒,100x表示执行100次。
  • -cpu:指定 GOMAXPROCS。
  • -timeout:指定测试函数的超时时间

示例测试

示例测试函数以Example开头。例如ExampleXxx或者Example_xxx。示例测试函数没有输入参数和输出参数,但是在函数的结尾可能会有以Output:或者Unordered output:开头的注释,Unordered output:开头的注释会忽略输出行的顺序。例如

func ExampleMax() {
    fmt.Println(Max(1, 2))
    // Output:
    // 2
}

go test命令默认也会执行示例测试函数,并且将示例测试函数输出到标准输出的内容与注释的内容进行比较(比较时忽略前后空格),相等则测试通过,不相等则测试失败。

对于大型示例测试,可以一个测试函数一个文件,这样godoc展示这类示例测试的时候会直接展示整个文件。

TestMain函数

func TestMain(m *testing.M) {
    fmt.Println("do some setup")
    m.Run()
    fmt.Println("do some cleanup")
}

TestMain是一个特殊的函数,go test命令执行测试函数的时候,会先执行TestMain函数,TestMain中调用m.Run()来执行普通的测试函数。所以TestMain函数,我们通过在m.Run()之前做一些测试的准备工作,例如创建数据库连接;在m.Run()做一些测试的清理工作,例如关闭数据库连接,删除测试产生的临时文件。

mock测试

一半来说,数据库中是不允许有外部依赖的,例如数据库连接,这些外部依赖都需要被模拟,在go中,就是借用各种mock工具进行模拟的。

GoMock是golang官方开发的测试框架,实现较为完整的基于interface的Mock功能。接下来我们就了解一下使用GoMock进行mock测试。

GoMock测试框架分为两部分,一是GoMock包,用来管理对象生成周期,另一个就是mockgen工具,用来生成interface对应的mock源文件。

首先,安装gomock包和mockgen工具

$ go get github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen

GoMock是基于interface的,所以我们现在有如下的接口Store,其中只有一个方法GetCount用于获取记录条数。我们可以为不同的数据库来实现这个接口。

type Store interface {
	GetCount() int
}

然后有一个函数调用了这个接口实现的方法

func Count(store Store) int {
	return store.GetCount()
}

我们现在要对Count这个函数进行单元测试,但是现在Store接口既没有实现,单元测试环境中也不应该依赖外部数据库,所以我们要对Store进行mock来获得一个实例。

对接口进行mock,这就要用到mockgen工具了。当前demo的module名为gomock-demo,目录结构如下:

├── count.go
├── go.mod
├── go.sum
└── store
    └── store.go

执行下面的命令为Store接口生成mock实现,命令参数后续进行说明

$ mockgen -destination store/mock/store_mock.go -package store gomock-demo/store Store

执行结束后的目录结构如下:

├── count.go
├── go.mod
├── go.sum
└── store
    ├── mock
    │   └── store_mock.go
    └── store.go

我们接下来就可以为Count编写如下的单元测试了

func TestCount(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockStore := store.NewMockStore(ctrl)
	mockStore.EXPECT().GetCount().Return(10)

	got := Count(mockStore)
	if got != 10 {
		t.Error("Count wrong res: ", got)
	}
}

通过mock,很多无法测试的函数也可以进行测试了。

mockgen说明

mockgen工具用于为接口生成mock实现。有两种生成方式,一种是带-source的源码模式,例如

mockgen -destination store/mock/store_mock.go -package store -source store/store.go

还有一种就是利用反射程序的反射模式,通过传递两个非标志参数,即导入路径和逗号分隔的接口列表来启用,其他参数和源码模式共用。前面的示例中使用的就是反射模式

$ mockgen -destination store/mock/store_mock.go -package store gomock-demo/store Store

这里介绍再一下命令行的参数即可

  • -destination:生成的mock代码所在路径
  • -package;mock文件的包名
  • -source:需要mock的接口文件
  • -imports:依赖的包
  • -aux_files:接口不止一个文件时,附件文件
  • -build_flags:传递给build工具的参数
    通常前3个参数就已经够用了。

为了方便,可以在接口文件的代码添加注释

//go:generate mockgen -destination mock/store_mock.go -package store -source store.go

这样我们只需要执行下面的命令,就可以为这个接口文件生成mock代码了

go generate ./...

mock代码编写单元测试

通过mockgen工具生成mock代码之后,我们就可以利用生成的mock代码和gomock包编写单元测试了。

首先,创建Mock控制器来管理整个mock过程,并在完成之后进行回收

ctrl := gomock.NewController(t)
defer ctrl.Finish()

然后创建mock实例

mockStore := store.NewMockStore(ctrl)

获得mock实例之后,要想mock一个接口,就需要mock接口的入参和返回值。返回值通过Return来mock,就像前面的示例一样

mockStore.EXPECT().GetCount().Return(10)

这里的GetCount没有入参,但是对于有入参的方法,这里需要使用如下的参数匹配对入参进行约束

  • gomock.Any(),可以用来表示任意的入参。
  • gomock.Eq(value),用来表示与 value 等价的值。
  • gomock.Not(value),用来表示非 value 以外的值。
  • gomock.Nil(),用来表示 None 值。

mock实例进行EXPECT断言,然后调用方法就可以获得第一个Call对象,并对其进行约束

func (c *Call) After(preReq *Call) *Call // After声明调用在preReq完成后执行
func (c *Call) Times(n int) *Call // 设置调用次数为 n 次
func (c *Call) AnyTimes() *Call // 允许调用次数为 0 次或更多次
func (c *Call) MaxTimes(n int) *Call // 设置最大的调用次数为 n 次
func (c *Call) MinTimes(n int) *Call // 设置最小的调用次数为 n 次
func (c *Call) Do(f interface{}) *Call // 声明在匹配时要运行的操作
func (c *Call) Return(rets ...interface{}) *Call //  // 声明模拟函数调用返回的值
func (c *Call) SetArg(n int, value interface{}) *Call // 声明使用指针设置第 n 个参数的值

测试覆盖率

为了避免漏掉为某些函数编写测试用例,或者测试用例不够全面。Go提供了cover工具来统计测试覆盖率。

  • 生成测试覆盖率数据
$ go test -coverprofile=coverage.out

会在当前目录下生成coverage.out覆盖率数据文件。

  • 分析覆盖率文件
$ go tool cover -func=coverage.out

可以查看各个函数的测试覆盖率。可以据此来为函数编写或完善测试用例。

为了更清晰地展示,还可以生成html格式的分析文件

$ go tool cover -html=coverage.out -o coverage.html

原文地址:https://www.cnblogs.com/smarticen/p/17076767.html