37-代码测试(下):Go语言其他测试类型及IAM测试介绍

 。

Go中的两类测试:单元测试和性能测试。 

 我就来介绍下Go 语言中的其他测试类型:示例测试、TestMain函数、Mock测试、Fake测试等, 

示例测试

示例测试以Example开头,没有输入和返回参数,通常保存在example_test.go文件中。示例测试可能包含以Output:或者Unordered output:开头的注释,这些注释放在函数的结尾部分。Unordered output:开头的注释会忽略输出行的顺序。

执行go test命令时,会执行这些示例测试,并且go test会将示例测试输出到标准输出的内容,跟注释作对比(比较时将忽略行前后的空格)。如果相等,则示例测试通过测试;如果不相等,则示例测试不通过测试。下面是一个示例测试(位于example_test.go文件中):

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

执行go test命令,测试ExampleMax示例测试:

$ go test -v -run='Example.*'
=== RUN   ExampleMax
--- PASS: ExampleMax (0.00s)
PASS
ok      github.com/marmotedu/gopractise-demo/31/test    0.004s

可以看到ExampleMax测试通过。这里测试通过是因为fmt.Println(Max(1, 2))向标准输出输出了2,跟// Output:后面的2一致。

当示例测试不包含Output:或者Unordered output:注释时,执行go test只会编译这些函数,但不会执行这些函数。

示例测试命名规范

示例测试需要遵循一些命名规范 Godoc才能将示例测试和包级别的标识符进行关联。例如,有以下示例测试(位于example_test.go文件中):

package stringutil_testimport ("fmt""github.com/golang/example/stringutil"
)func ExampleReverse() {fmt.Println(stringutil.Reverse("hello"))// Output: olleh
}

Godoc将在Reverse函数的文档旁边提供此示例,如下图所示:

图片

示例测试名以Example开头,后面可以不跟任何字符串,也可以跟函数名、类型名或者类型_方法名,中间用下划线_连接,例如:

func Example() { ... } // 代表了整个包的示例
func ExampleF() { ... } // 函数F的示例
func ExampleT() { ... } // 类型T的示例
func ExampleT_M() { ... } // 方法T_M的示例

当某个函数/类型/方法有多个示例测试时,可以通过后缀来区分,后缀必须以小写字母开头,例如:

func ExampleReverse()
func ExampleReverse_second()
func ExampleReverse_third()

大型示例

 我们可以编写一个整文件的示例(whole file example),它有这几个特点:文件名以_test.go结尾;只包含一个示例测试,文件中没有单元测试函数和性能测试函数;至少包含一个包级别的声明;当展示这类示例测试时,godoc会直接展示整个文件。例如:

package sort_testimport ("fmt""sort"
)type Person struct {Name stringAge  int
}func (p Person) String() string {return fmt.Sprintf("%s: %d", p.Name, p.Age)
}// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Personfunc (a ByAge) Len() int           { return len(a) }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }func Example() {people := []Person{{"Bob", 31},{"John", 42},{"Michael", 17},{"Jenny", 26},}fmt.Println(people)sort.Sort(ByAge(people))fmt.Println(people)// Output:// [Bob: 31 John: 42 Michael: 17 Jenny: 26]// [Michael: 17 Jenny: 26 Bob: 31 John: 42]
}

一个包可以包含多个whole file example,一个示例一个文件,例如example_interface_test.goexample_keys_test.goexample_search_test.go等。

TestMain函数

 例如创建数据库连接等;在测试之后做些清理工作,例如关闭数据库连接、清理测试文件等。这时,我们可以在_test.go文件中添加TestMain函数,其入参为*testing.M

TestMain是一个特殊的函数(相当于main函数),测试用例在执行时,会先执行TestMain函数,然后可以在TestMain中调用m.Run()函数执行普通的测试函数。在m.Run()函数前面我们可以编写准备逻辑,在m.Run()后面我们可以编写清理逻辑。

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

执行go test,输出如下:

$ go test -v
do some setup
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
...
=== RUN   ExampleMax
--- PASS: ExampleMax (0.00s)
PASS
do some cleanup
ok  	github.com/marmotedu/gopractise-demo/31/test	0.006s

在执行测试用例之前,打印了do some setup,在测试用例运行完成之后,打印了do some cleanup

IAM项目的测试用例中,使用TestMain函数在执行测试用例前连接了一个fake数据库,代码如下(位于internal/apiserver/service/v1/user_test.go文件中):

func TestMain(m *testing.M) {fakeStore, _ := fake.NewFakeStore()store.SetClient(fakeStore)os.Exit(m.Run())
}

单元测试、性能测试、示例测试、TestMain函数是go test支持的测试类型。此外,为了测试在函数内使用了Go Interface的函数,我们还延伸出了Mock测试和Fake测试两种测试类型。

Mock测试

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

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

安装GoMock

要使用GoMock,首先需要安装GoMock包和mockgen工具,安装方法如下:

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

下面,我通过一个获取当前Golang最新版本的例子,来给你演示下如何使用GoMock。示例代码目录结构如下(目录下的代码见gomock):

tree .
.
├── go_version.go
├── main.go
└── spider└── spider.go

spider.go文件中定义了一个Spider接口,spider.go代码如下:

package spidertype Spider interface {GetBody() string
}

Spider接口中的GetBody方法可以抓取https://golang.org首页的Build version字段,来获取Golang的最新版本。

我们在go_version.go文件中,调用Spider接口的GetBody方法,go_version.go代码如下:

package gomockimport ("github.com/marmotedu/gopractise-demo/gomock/spider"
)func GetGoVersion(s spider.Spider) string {body := s.GetBody()return body
}

GetGoVersion函数直接返回表示版本的字符串。 

func TestGetGoVersion(t *testing.T) {v := GetGoVersion(spider.CreateGoVersionSpider())if v != "go1.8.3" {t.Error("Get wrong version %s", v)}
}

上面的测试代码,依赖spider.CreateGoVersionSpider()返回一个实现了Spider接口的实例(爬虫)。但很多时候,spider.CreateGoVersionSpider()爬虫可能还没有实现,或者在单元测试环境下不能运行(比如,在单元测试环境中连接数据库),这时候TestGetGoVersion测试用例就无法执行。

那么,如何才能在这种情况下运行TestGetGoVersion测试用例呢?这时候,我们就可以通过Mock工具,Mock一个爬虫实例。 

首先,用 GoMock 提供的mockgen工具,生成要 Mock 的接口的实现,我们在gomock目录下执行以下命令:

$ mockgen -destination spider/mock/mock_spider.go -package spider github.com/marmotedu/gopractise-demo/gomock/spider Spider

上面的命令会在spider/mock目录下生成mock_spider.go文件:

$ tree .
.
├── go_version.go
├── go_version_test.go
├── go_version_test_traditional_method.go~
└── spider├── mock│   └── mock_spider.go└── spider.go

mock_spider.go文件中,定义了一些函数/方法,可以支持我们编写TestGetGoVersion测试函数。这时候,我们的单元测试代码如下(见go_version_test.go文件):

package gomockimport ("testing""github.com/golang/mock/gomock"spider "github.com/marmotedu/gopractise-demo/gomock/spider/mock"
)func TestGetGoVersion(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()mockSpider := spider.NewMockSpider(ctrl)mockSpider.EXPECT().GetBody().Return("go1.8.3")goVer := GetGoVersion(mockSpider)if goVer != "go1.8.3" {t.Errorf("Get wrong version %s", goVer)}
}

这一版本的TestGetGoVersion通过GoMock, Mock了一个Spider接口,而不用去实现一个Spider接口。 

通过上面的测试用例,我们可以看到,GoMock 和上一讲介绍的testing单元测试框架可以紧密地结合起来工作。

mockgen工具介绍

上面,我介绍了如何使用 GoMock 编写单元测试用例。其中,我们使用到了mockgen工具来生成 Mock代码,mockgen工具提供了很多有用的功能, 

mockgen工具是 GoMock 提供的,用来Mock一个Go接口。它可以根据给定的接口,来自动生成Mock代码。这里,有两种模式可以生成Mock代码,分别是源码模式和反射模式。

  1. 源码模式

如果有接口文件,则可以通过以下命令来生成Mock代码:

$ mockgen -destination spider/mock/mock_spider.go -package spider -source spider/spider.go

上面的命令,Mock了spider/spider.go文件中定义的Spider接口,并将Mock代码保存在spider/mock/mock_spider.go文件中,文件的包名为spider

mockgen工具的参数说明见下表:

  1. 反射模式

此外,mockgen工具还支持通过使用反射程序来生成 Mock 代码。它通过传递两个非标志参数,即导入路径和逗号分隔的接口列表来启用,其他参数和源码模式共用,例如:

$ mockgen -destination spider/mock/mock_spider.go -package spider github.com/marmotedu/gopractise-demo/gomock/spider Spider

通过注释使用mockgen

如果有多个文件,并且分散在不同的位置,那么我们要生成Mock文件的时候,需要对每个文件执行多次mockgen命令 这种操作还是比较繁琐的,mockgen还提供了一种通过注释生成Mock文件的方式,此时需要借助go generate工具。

在接口文件的代码中,添加以下注释(具体代码见spider.go文件):

//go:generate mockgen -destination mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider

这时候,我们只需要在gomock目录下,执行以下命令,就可以自动生成Mock代码:

$ go generate ./...

使用Mock代码编写单元测试用例

生成了Mock代码之后,我们就可以使用它们了。这里我们结合testing来编写一个使用了Mock代码的单元测试用例。

首先,需要在单元测试代码里创建一个Mock控制器:

ctrl := gomock.NewController(t)

*testing.T传递给GoMock ,生成一个Controller对象,该对象控制了整个Mock的过程。在操作完后,还需要进行回收,所以一般会在NewController后面defer一个Finish,代码如下:

defer ctrl.Finish()

然后,就可以调用Mock的对象了:

mockSpider := spider.NewMockSpider(ctrl)

这里的spider是mockgen命令里面传递的包名,后面是NewMockXxxx格式的对象创建函数,Xxx是接口名。这里,我们需要传递控制器对象进去,返回一个Mock实例。

接着,有了Mock实例,我们就可以调用其断言方法EXPECT()了。

gomock采用了链式调用法,通过.连接函数调用,可以像链条一样连接下去。例如:

mockSpider.EXPECT().GetBody().Return("go1.8.3")

Mock一个接口的方法,我们需要Mock该方法的入参和返回值。我们可以通过参数匹配来Mock入参,通过Mock实例的 Return 方法来Mock返回值。 

先来看如何指定入参。如果函数有参数,我们可以使用参数匹配来指代函数的参数,例如:

mockSpider.EXPECT().GetBody(gomock.Any(), gomock.Eq("admin")).Return("go1.8.3")

gomock支持以下参数匹配:

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

接下来,我们看如何指定返回值。

EXPECT()得到Mock的实例,然后调用Mock实例的方法,该方法返回第一个Call对象,然后可以对其进行条件约束,比如使用Mock实例的 Return 方法约束其返回值。Call对象还提供了以下方法来约束Mock实例:

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

上面列出了多个 Call 对象提供的约束方法,接下来我会介绍3个常用的约束方法:指定返回值、指定执行次数和指定执行顺序。

  1. 指定返回值

我们可以提供调用CallReturn函数,来指定接口的返回值,例如:

mockSpider.EXPECT().GetBody().Return("go1.8.3")
  1. 指定执行次数

有时候,我们需要指定函数执行多少次,例如:对于接受网络请求的函数,计算其执行了多少次。我们可以通过CallTimes函数来指定执行次数:

mockSpider.EXPECT().Recv().Return(nil).Times(3)

上述代码,执行了三次Recv函数,这里gomock还支持其他的执行次数限制:

  • AnyTimes(),表示执行0到多次。
  • MaxTimes(n int),表示如果没有设置,最多执行n次。
  • MinTimes(n int),表示如果没有设置,最少执行n次。
  1. 指定执行顺序

initCall := mockSpider.EXPECT().Init()
mockSpider.EXPECT().Recv().After(initCall)

最后,我们可以使用go test来测试使用了Mock代码的单元测试代码:

$ go test -v
=== RUN   TestGetGoVersion
--- PASS: TestGetGoVersion (0.00s)
PASS
ok  	github.com/marmotedu/gopractise-demo/gomock	0.002s

Fake测试

在Go项目开发中,对于比较复杂的接口,我们还可以Fake一个接口实现,来进行测试。所谓Fake测试,其实就是针对接口实现一个假(fake)的实例。至于如何实现Fake实例,需要你根据业务自行实现。例如:IAM项目中iam-apiserver组件就实现了一个fake store,代码见fake目录。因为这一讲后面的IAM项目测试实战部分有介绍,所以这里不再展开讲解。

何时编写和执行单元测试用例?

上面,我介绍了Go代码测试的基础知识,这里我再来分享下在做测试时一个比较重要的知识点:何时编写和执行单元测试用例。

编码前:TDD

图片

Test-Driven Development,也就是测试驱动开发,是敏捷开发的⼀项核心实践和技术,也是⼀种设计方法论。简单来说,TDD原理就是:开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。这样做的好处在于,通过测试的执行代码肯定满足需求,而且有助于面向接口编程,降低代码耦合,也极大降低了bug的出现几率。

然而,TDD的坏处也显而易见:由于测试用例是在进行代码设计之前写的,很有可能限制开发者对代码的整体设计;并且,由于TDD对开发⼈员要求非常高,体现的思想跟传统开发思维也不⼀样,因此实施起来比较困难;此外,因为要先编写测试用例,TDD也可能会影响项目的研发进度。所以,在客观情况不满足的情况下,不应该盲目追求对业务代码使用TDD的开发模式。

与编码同步进行:增量

及时为增量代码写单测是一种良好的习惯。一方面是因为,此时我们对需求有一定的理解,能够更好地写出单元测试来验证正确性。并且,在单测阶段就发现问题,而不是等到联调测试中才发现,修复的成本也是最小的。

另一方面,在写单测的过程中,我们也能够反思业务代码的正确性、合理性,推动我们在实现的过程中更好地反思代码的设计,并及时调整。

编码后:存量

在完成业务需求后,我们可能会遇到这种情况:因为上线时间比较紧张、没有单测相关规划,开发阶段只手动测试了代码是否符合功能。

如果这部分存量代码出现较大的新需求,或者维护已经成为问题,需要大规模重构,这正是推动补全单测的好时机。为存量代码补充上单测,一方面能够推进重构者进一步理解原先的逻辑,另一方面也能够增强重构者重构代码后的信心,降低风险。

但是,补充存量单测可能需要再次回忆理解需求和逻辑设计等细节,而有时写单测的人并不是原编码的设计者,所以编码后编写和执行单元测试用例也有一定的不足。

测试覆盖率

我们写单元测试的时候应该想得很全面,能够覆盖到所有的测试用例,但有时也会漏过一些 case,Go提供了cover工具来统计测试覆盖率。具体可以分为两大步骤。

第一步,生成测试覆盖率数据:

$ go test -coverprofile=coverage.out
do some setup
PASS
coverage: 40.0% of statements
do some cleanup
ok  	github.com/marmotedu/gopractise-demo/test	0.003s

上面的命令在当前目录下生成了coverage.out覆盖率数据文件。

图片

第二步,分析覆盖率文件:

$ go tool cover -func=coverage.out
do some setup
PASS
coverage: 40.0% of statements
do some cleanup
ok  	github.com/marmotedu/gopractise-demo/test	0.003s
[colin@dev test]$ go tool cover -func=coverage.out
github.com/marmotedu/gopractise-demo/test/math.go:9:	Abs		100.0%
github.com/marmotedu/gopractise-demo/test/math.go:14:	Max		100.0%
github.com/marmotedu/gopractise-demo/test/math.go:19:	Min		0.0%
github.com/marmotedu/gopractise-demo/test/math.go:24:	RandInt		0.0%
github.com/marmotedu/gopractise-demo/test/math.go:29:	Floor		0.0%
total:							(statements)	40.0%

在上述命令的输出中,我们可以查看到哪些函数没有测试,哪些函数内部的分支没有测试完全。cover工具会根据被执行代码的行数与总行数的比例计算出覆盖率。可以看到,Abs和Max函数的测试覆盖率为100%,Min和RandInt的测试覆盖率为0。

我们还可以使用go tool cover -html生成HTML格式的分析文件,可以更加清晰地展示代码的测试情况:

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

上述命令会在当前目录下生成一个coverage.html文件,用浏览器打开coverage.html文件,可以更加清晰地看到代码的测试情况,如下图所示:

通过上图,我们可以知道红色部分的代码没有被测试到,可以让我们接下来有针对性地添加测试用例,而不是一头雾水,不知道需要为哪些代码编写测试用例。

在Go项目开发中,我们往往会把测试覆盖率作为代码合并的一个强制要求,所以需要在进行代码测试时,同时生成代码覆盖率数据文件。在进行代码测试时,可以通过分析该文件,来判断我们的代码测试覆盖率是否满足要求,如果不满足则代码测试失败。

IAM项目测试实战

接下来,我来介绍下IAM项目是如何编写和运行测试用例的,你可以通过IAM项目的测试用例,加深对上面内容的理解。

IAM项目是如何运行测试用例的?

首先,我们来看下IAM项目是如何执行测试用例的。

在IAM项目的源码根目录下,可以通过运行make test执行测试用例,make test会执行iam/scripts/make-rules/golang.mk文件中的go.test伪目标,规则如下:

.PHONY: go.test
go.test: tools.verify.go-junit-report@echo "===========> Run unit test"@set -o pipefail;$(GO) test -race -cover -coverprofile=$(OUTPUT_DIR)/coverage.out \\-timeout=10m -short -v `go list ./...|\egrep -v $(subst $(SPACE),'|',$(sort $(EXCLUDE_TESTS)))` 2>&1 | \\tee >(go-junit-report --set-exit-code >$(OUTPUT_DIR)/report.xml)@sed -i '/mock_.*.go/d' $(OUTPUT_DIR)/coverage.out # remove mock_.*.go files from test coverage@$(GO) tool cover -html=$(OUTPUT_DIR)/coverage.out -o $(OUTPUT_DIR)/coverage.html

在上述规则中,我们执行go test时设置了超时时间、竞态检查,开启了代码覆盖率检查,覆盖率测试数据保存在了coverage.out文件中。在Go项目开发中,并不是所有的包都需要单元测试,所以上面的命令还过滤掉了一些不需要测试的包,这些包配置在EXCLUDE_TESTS变量中:

EXCLUDE_TESTS=github.com/marmotedu/iam/test github.com/marmotedu/iam/pkg/log github.com/marmotedu/iam/third_party github.com/marmotedu/iam/internal/pump/storage github.com/marmotedu/iam/internal/pump github.com/marmotedu/iam/internal/pkg/logger

同时,也调用了go-junit-report将go test的结果转化成了xml格式的报告文件,该报告文件会被一些CI系统,例如Jenkins拿来解析并展示结果。上述代码也同时生成了coverage.html文件,该文件可以存放在制品库中,供我们后期分析查看。

这里需要注意,Mock的代码是不需要编写测试用例的,为了避免影响项目的单元测试覆盖率,需要将Mock代码的单元测试覆盖率数据从coverage.out文件中删除掉,go.test规则通过以下命令删除这些无用的数据:

sed -i '/mock_.*.go/d' $(OUTPUT_DIR)/coverage.out # remove mock_.*.go files from test coverage

另外,还可以通过make cover来进行单元测试覆盖率测试,make cover会执行iam/scripts/make-rules/golang.mk文件中的go.test.cover伪目标,规则如下:

.PHONY: go.test.cover
go.test.cover: go.test@$(GO) tool cover -func=$(OUTPUT_DIR)/coverage.out | \\awk -v target=$(COVERAGE) -f $(ROOT_DIR)/scripts/coverage.awk

上述目标依赖go.test,也就是说执行单元测试覆盖率目标之前,会先进行单元测试,然后使用单元测试产生的覆盖率数据coverage.out计算出总的单元测试覆盖率,这里是通过coverage.awk脚本来计算的。

如果单元测试覆盖率不达标,Makefile会报错并退出。可以通过Makefile的COVERAGE变量来设置单元测试覆盖率阈值。

COVERAGE的默认值为60,我们也可以在命令行手动指定,例如:

$ make cover COVERAGE=80

为了确保项目的单元测试覆盖率达标,需要设置单元测试覆盖率质量红线。一般来说,这些红线很难靠开发者的自觉性去保障,所以好的方法是将质量红线加入到CICD流程中。

所以,在Makefile文件中,我将cover放在all目标的依赖中,并且位于build之前,也就是all: gen add-copyright format lint cover build。这样每次当我们执行make时,会自动进行代码测试,并计算单元测试覆盖率,如果覆盖率不达标,则停止构建;如果达标,继续进入下一步的构建流程。

IAM项目测试案例分享

接下来,我会给你展示一些IAM项目的测试案例,因为这些测试案例的实现方法,我在36讲 和这一讲的前半部分已有详细介绍,所以这里,我只列出具体的实现代码,不会再介绍这些代码的实现方法。

  1. 单元测试案例

我们可以手动编写单元测试代码,也可以使用gotests工具生成单元测试代码。

先来看手动编写测试代码的案例。这里单元测试代码见Test_Option,代码如下:

func Test_Option(t *testing.T) {fs := pflag.NewFlagSet("test", pflag.ExitOnError)opt := log.NewOptions()opt.AddFlags(fs)args := []string{"--log.level=debug"}err := fs.Parse(args)assert.Nil(t, err)assert.Equal(t, "debug", opt.Level)
}

上述代码中,使用了github.com/stretchr/testify/assert包来对比结果。

再来看使用gotests工具生成单元测试代码的案例(Table-Driven 的测试模式)。出于效率上的考虑,IAM项目的单元测试用例,基本都是使用gotests工具生成测试用例模板代码,并基于这些模板代码填充测试Case的。代码见service_test.go文件。

  1. 性能测试案例

IAM项目的性能测试用例,见BenchmarkListUser测试函数。代码如下:

func BenchmarkListUser(b *testing.B) {opts := metav1.ListOptions{Offset: pointer.ToInt64(0),Limit:  pointer.ToInt64(50),}storeIns, _ := fake.GetFakeFactoryOr()u := &userService{store: storeIns,}for i := 0; i < b.N; i++ {_, _ = u.List(context.TODO(), opts)}
}
  1. 示例测试案例

IAM项目的示例测试用例见example_test.go文件。example_test.go中的一个示例测试代码如下:

func ExampleNew() {err := New("whoops")fmt.Println(err)// Output: whoops
}
  1. TestMain测试案例

IAM项目的TestMain测试案例,见user_test.go文件中的TestMain函数:

func TestMain(m *testing.M) {_, _ = fake.GetFakeFactoryOr()os.Exit(m.Run())
}

TestMain函数初始化了fake Factory,然后调用m.Run执行测试用例。

  1. Mock测试案例

Mock代码见internal/apiserver/service/v1/mock_service.go,使用Mock的测试用例见internal/apiserver/controller/v1/user/create_test.go文件。因为代码比较多,这里建议你打开链接,查看测试用例的具体实现。

我们可以在IAM项目的根目录下执行以下命令,来自动生成所有的Mock文件:

$ go generate ./...
  1. Fake测试案例

fake store代码实现位于internal/apiserver/store/fake目录下。fake store的使用方式,见user_test.go文件:

func TestMain(m *testing.M) {_, _ = fake.GetFakeFactoryOr()os.Exit(m.Run())
}func BenchmarkListUser(b *testing.B) {opts := metav1.ListOptions{Offset: pointer.ToInt64(0),Limit:  pointer.ToInt64(50),}storeIns, _ := fake.GetFakeFactoryOr()u := &userService{store: storeIns,}for i := 0; i < b.N; i++ {_, _ = u.List(context.TODO(), opts)}
}

上述代码通过TestMain初始化fake实例(store.Factory接口类型):

func GetFakeFactoryOr() (store.Factory, error) {once.Do(func() {fakeFactory = &datastore{users:    FakeUsers(ResourceCount),secrets:  FakeSecrets(ResourceCount),policies: FakePolicies(ResourceCount),}})if fakeFactory == nil {return nil, fmt.Errorf("failed to get mysql store fatory, mysqlFactory: %+v", fakeFactory)}return fakeFactory, nil
}

GetFakeFactoryOr函数,创建了一些fake users、secrets、policies,并保存在了fakeFactory变量中,供后面的测试用例使用,例如BenchmarkListUser、Test_newUsers等。

其他测试工具/包

最后,我再来分享下Go项目测试中常用的工具/包,因为内容较多,我就不详细介绍了,如果感兴趣你可以点进链接自行学习。我将这些测试工具/包分为了两类,分别是测试框架和Mock工具。

测试框架

  • Testify框架:Testify是Go test的预判工具,它能让你的测试代码变得更优雅和高效,测试结果也变得更详细。
  • GoConvey框架:GoConvey是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性。

Mock工具

这一讲里,我介绍了Go官方提供的Mock框架GoMock,不过还有一些其他的优秀Mock工具可供我们使用。这些Mock工具分别用在不同的Mock场景中,我在 10讲中已经介绍过。不过,为了使我们这一讲的测试知识体系更加完整,这里我还是再提一次,你可以复习一遍。

  • sqlmock:可以用来模拟数据库连接。数据库是项目中比较常见的依赖,在遇到数据库依赖时都可以用它。
  • httpmock:可以用来Mock HTTP请求。
  • bouk/monkey:猴子补丁,能够通过替换函数指针的方式来修改任意函数的实现。如果golang/mock、sqlmock和httpmock这几种方法都不能满足我们的需求,我们可以尝试用猴子补丁的方式来Mock依赖。可以这么说,猴子补丁提供了单元测试 Mock 依赖的最终解决方案。

总结

除了示例测试和TestMain函数,我还详细介绍了Mock测试,也就是如何使用GoMock来测试一些在单元测试环境下不好实现的接口。绝大部分情况下,可以使用GoMock来Mock接口,但是对于一些业务逻辑比较复杂的接口,我们可以通过Fake一个接口实现,来对代码进行测试,这也称为Fake测试。

此外,我还介绍了何时编写和执行测试用例。我们可以根据需要,选择在编写代码前、编写代码中、编写代码后编写测试用例。

为了保证单元测试覆盖率,我们还应该为整个项目设置单元测试覆盖率质量红线,并将该质量红线加入到CICD流程中。我们可以通过 go test -coverprofile=coverage.out 命令来生成测试覆盖率数据,通过go tool cover -func=coverage.out 命令来分析覆盖率文件。

IAM项目中使用了大量的测试方法和技巧来测试代码,为了加深你对测试知识的理解,我也列举了一些测试案例,供你参考、学习和验证。具体的测试案例,你可以返回前面查看下。

除此之外,我们还可以使用其他一些测试框架,例如Testify框架和GoConvey框架。在Go代码测试中,我们最常使用的是Go官方提供的Mock框架GoMock,但仍然有其他优秀的Mock工具,可供我们在不同场景下使用,例如sqlmock、httpmock、bouk/monkey等。

课后习题

  1. 请使用 sqlmock 来Mock一个GORM数据库实例,并完成GORM的CURD单元测试用例编写。
  2. 思考下,在Go项目开发中,还有哪些优秀的测试框架、测试工具、Mock工具以及测试技巧? 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/309935.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT-4 Turbo 今天开放啦!附如何查询GPT-4 是否为 Turbo

2024年4月12日&#xff0c;OpenAI在X上宣布GPT-4 Turbo开放了&#xff01;提高了写作、数学、逻辑推理和编码方面的能力。另外最重要的是&#xff0c;响应速度更快了&#xff01;&#xff01; ChatGPT4 Turbo 如何升级&#xff1f;解决国内无法升级GPT4 Turbo的问题&#xff0…

云端漫步:如何免费享受亚马逊云服务器的12个月奇妙旅程

前言&#xff1a; 废话不多说&#xff0c;开头就直接上体验链接 亚马逊科技 (免费试用产品专属链接)包括灵活的Amazon EC2云服务器、稳定的Amazon RDS数据库服务、可扩展的Amazon S3云存储空间等等常见云服务产品。福利很大&#xff0c;有需要的朋友赶紧冲冲冲&#xff01; 想…

电商技术揭秘十三:云计算在电商中的应用场景

相关系列文章 电商技术揭秘一&#xff1a;电商架构设计与核心技术 电商技术揭秘二&#xff1a;电商平台推荐系统的实现与优化 电商技术揭秘三&#xff1a;电商平台的支付与结算系统 电商技术揭秘四&#xff1a;电商平台的物流管理系统 电商技术揭秘五&#xff1a;电商平台…

vscode只修改几行,git却显示整个文件都被修改

原因&#xff1a;不同的操作系统默认的回车换行符是不一样的&#xff0c;有些编辑器会自动修改回车换行&#xff0c;然后就整个文件都变化了。 Unix/Linux/Mac使用的是LF&#xff0c;但Windows一直使用CRLF【回车(CR, ASCII 13, r) 换行(LF, ASCII 10, n)】作为换行符。 解决&a…

深度学习pytorch实战第P3周--实现天气识别

>- **&#x1f368; 本文为[&#x1f517;365天深度学习训练营](https://mp.weixin.qq.com/s/0dvHCaOoFnW8SCp3JpzKxg) 中的学习记录博客** >- **&#x1f356; 原作者&#xff1a;[K同学啊](https://mtyjkh.blog.csdn.net/)** 引言 1.复习上周 深度学习pytorch实战-第…

Upload-labs(Pass-14 - Pass-16)

Pass-14 &#xff08;图片马&#xff0c;判断文件类型&#xff09; 图片的格式在防护中通常是不会使用后缀进行判断的依据&#xff0c;文件头是文件开头的一段二进制码&#xff0c;不同类型的图片也就会有不同的二进制头。   JPEG (jpg)&#xff0c;文件头&#xff1a;FF D…

qemu源码解析一

基于qemu9.0.0 简介 QEMU是一个开源的虚拟化软件&#xff0c;它能够模拟各种硬件设备&#xff0c;支持多种虚拟化技术&#xff0c;如TCG、Xen、KVM等 TCG 是 QEMU 中的一个组件&#xff0c;它可以将高级语言编写的代码&#xff08;例如 C 代码&#xff09;转换为可在虚拟机中…

WIFI详解及周边拓展

一、WiFi协议简介 WiFi协议&#xff0c;也称为无线保真技术&#xff0c;是一种允许电子设备通过无线方式在局域网&#xff08;WLAN&#xff09;和互联网上进行通信的技术标准。WiFi协议是基于IEEE 802标准的子系列标准协议&#xff0c;由电气和电子工程师协会制定。随着移动设备…

在一台恢复测试机器上验证oracle备份有效性

一 目的 定期将生产环境oracle数据库恢复到一台测试环境数据库服务器上&#xff0c;以验证备份是否有效&#xff0c;是否能正常恢复。 二 环境 这里以恢复orcl1库为例&#xff0c;计划在orcl这个实例上进行恢复测试。 三 实验步骤 3.1 在目标端创建和源端一样的备份目录 ①…

如何在浏览器Web前端在线编辑PPT幻灯片?

有时候在项目中我们会遇到需要在网页在线打开并编辑PPT文档保存到本地或者服务器指定位置&#xff0c;猿大师办公助手可以很方便的调用本机Office实现在网页上编辑PPT幻灯片&#xff0c;效果与本机Office打开PPT完全一样。 猿大师办公助手支持完整嵌入模式&#xff0c;也就是本…

nginx的启动,systemctl管理

service unit file文件通常由三部分组成&#xff1a; [Unit]&#xff1a;定义与Unit类型无关的通用选项&#xff1b;用于提供unit的描述信息、unit行为及依赖关系等 [Service]&#xff1a;与特定类型相关的专用选项&#xff1b;此处为Service类型 [Install]&#xff1a;定义…

【SCI绘图】【热力图系列1 R】多特征相关性分析热力图R语言实现

SCI&#xff0c;CCF&#xff0c;EI及核心期刊绘图宝典&#xff0c;爆款持续更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【热力图系列1 R】多特征相关性分析热力图R语言实现 1.环境准备 library(gplots) library(RColorBrewer) 2.数据示例 ###…

UE5学习日记——制作多语言版本游戏,同时初步学习UI制作、多语言化、控制器配置、独立进程测试、打包配置和快速批量翻译等

所有的文本类&#xff0c;无论变量还是控件等都能实现本地化&#xff0c;以此实现不同语言版本。 在这里先将重点注意标注一下&#xff1a; 所有文本类的变量、控件等都可以多语言&#xff1b;本地化控制板中收集、编译时&#xff0c;别忘了编译这一步&#xff1b;支持批量复制…

第十五届蓝桥杯c++b组赛后复盘和真题展示

题目变成八道了&#xff0c;分数一百分可能&#xff0c;感觉拿奖难度还是很高 第一题是一个简单的握手问题 答案算出来1204&#xff0c;纯手写 第二题是 物理题 纯蒙&#xff0c;随便猜了个轨迹&#xff0c;答案具体忘了&#xff0c;最后是 .45 第三题暴力 第四题 我是傻逼…

Zookeeper中的节点类型和实现持久化的两种方式

进入zookeeper的bin目录&#xff0c;执行./zkServer.sh start ../conf/zoo.cfg启动&#xff01; Zookeeper内部的数据模型 类似于数据结构中的树&#xff0c;同时也很像文件系统的目录&#xff0c; 节点的类型 持久节点&#xff1a;create /znode 创建出的节点&#xff0c…

永磁同步电机无感FOC(扩展卡尔曼滤波EKF位置观测控制)

文章目录 1、前言2、扩展卡尔曼滤波器原理2.1 预测阶段&#xff08;时间更新阶段&#xff09;2.2 校正阶段&#xff08;状态更新阶段&#xff09; 3、永磁同步电机EKF的模型4、永磁同步电机EKF的无位置状态观测仿真4.1 核心模块&#xff08;在滑膜、龙伯格、磁链等观测器基础上…

从零自制docker-8-【构建实现run命令的容器】

文章目录 log "github.com/sirupsen/logrus"args...go moduleimport第三方包失败package和 go import的导入go build . 和go runcli库log.SetFormatter(&log.JSONFormatter{})error和nil的关系cmd.Wait()和cmd.Start()arg……context.Args().Get(0)syscall.Exec和…

GRE/MGRE详解

GRE GRE&#xff1a;通用路由封装&#xff0c;是标准的三层隧道技术&#xff0c;是一种点对点的隧道技术&#xff1b; 该技术可以实现不同的网络之间安全的访问&#xff1b; 如上&#xff1a;可以使用该技术搭建一条专线&#xff0c;实现公司A与分公司A1之间相互通信&#xf…

(处理流)转换流与对象流

1.字符编码与解码. (1). 字符编码 : 将字符&#xff0c;字符串&#xff0c;字符数组------&#xff1e; 字节&#xff0c;字节数组. (2). 字节&#xff0c;字节数组------&#xff1e;字符&#xff0c;字符串&#xff0c;字符数组. 如果希望程序在读取文件时(也就是解码)不…

Linux多进程开发2 - 进程间通信

1、进程间通信的概念 进程是一个独立的资源分配单元&#xff0c;不同进程之间的资源是独立的&#xff0c;没有关联&#xff0c;不能在一个进程中直接访问另一个进程的资源。但是&#xff0c;进程不是孤立的&#xff0c;不同的进程需要进行信息的交换和状态的传递等&…