0


Go 单元测试完全指南(一)- 基本测试流程

为什么写单元测试?

关于测试,有一张很经典的图,如下:

在这里插入图片描述

说明:
测试类型成本速度频率E2E 测试高慢低集成测试中中中单元测试低快高
也就是说,单元测试是最快、最便宜的测试方式。这不难理解,单元测试往往用来验证代码的最小单元,比如一个函数、一个方法,这样的测试我们一个命令就能跑完整个项目的单元测试,而且速度还很快,所以单元测试是我们最常用的测试方式。
而 E2E 测试和集成测试,往往需要启动整个项目,然后需要真实用户进行手动操作,这样的测试成本高,速度慢,所以我们往往不会频繁地运行这样的测试。只有在项目的最后阶段,我们才会运行这样的测试。而单元测试,我们可以在开发的过程中,随时随地地运行,这样我们就能及时发现问题,及时解决问题。

一个基本的 Go 单元测试

Go 从一开始就支持单元测试,Go 的测试代码和普通代码一般是放在同一个包下的,只是测试代码的文件名是

  1. _test.go

结尾的。比如我们有一个

  1. add.go

文件,那么我们的测试文件就是

  1. add_test.go

  1. // add.gopackage main
  2. funcAdd(a int, b int)int{return a + b
  3. }
  1. // add_test.gopackage main
  2. import"testing"funcTestAdd(t *testing.T){ifAdd(1,2)!=3{
  3. t.Error("1 + 2 did not equal 3")}}

我们可以通过

  1. go test

命令来运行测试:

  1. go test

输出:

  1. PASS
  2. ok go-test 0.004s

注意:

  1. 测试函数的命名必须以 Test 开头,后面的名字必须以大写字母开头,比如 TestAdd
  2. 测试函数的参数是 *testing.T 类型。
  3. go test 加上 -v 参数可以输出详细的测试信息,加上 -cover 参数可以输出测试覆盖率。

go test 命令的参数详解

基本参数

  • -v:输出详细的测试信息。比如输出每个测试用例的名称。
  • -run regexp:只运行匹配正则表达式的测试用例。如 -run TestAdd
  • -bench regexp:运行匹配正则表达式的基准测试用例。
  • -benchtime t:设置基准测试的时间,默认是 1s,也就是让基准测试运行 1s。也可以指定基准测试的执行次数,格式如 -benchtime 100x,表示基准测试执行 100 次。
  • -count n:运行每个测试函数的次数,默认是 1 次。如果指定了 -cpu 参数,那么每个测试函数会运行 n * GOMAXPROCS 次。但是示例测试只会运行一次,该参数对模糊测试无效。
  • -cover:输出测试覆盖率。
  • -covermode set,count,atomic:设置测试覆盖率的模式。默认是 set,也就是记录哪些语句被执行过。
  • -coverpkg pkg1,pkg2,pkg3:用于指定哪些包应该生成覆盖率信息。这个参数允许你指定一个或多个包的模式,以便在运行测试时生成这些包的覆盖率信息。
  • -cpu 1,2,4:设置并行测试的 CPU 数量。默认是 GOMAXPROCS。这个参数对模糊测试无效。
  • -failfast:一旦某个测试函数失败,就停止运行其他的测试函数了。默认情况下,一个测试函数失败了,其他的测试函数还会继续运行。
  • -fullpath:测试失败的时候,输出完整的文件路径。
  • -fuzz regexp:运行模糊测试。
  • -fuzztime t:设置模糊测试的时间,默认是 1s。又或者我们可以指定模糊测试的执行次数,格式如 -fuzztime 100x,表示模糊测试执行 100 次。
  • -fuzzminimizetime t:设置模糊测试的最小化时间,默认是 1s。又或者我们可以指定模糊测试的最小化执行次数,格式如 -fuzzminimizetime 100x,表示模糊测试最小化执行 100 次。在模糊测试中,当发现一个失败的案例后,系统会尝试最小化这个失败案例,以找到导致失败的最小输入。
  • -json:以 json 格式输出
  • -list regexp:列出所有匹配正则表达式的测试用例名称。
  • -parallel n:设置并行测试的数量。默认是 GOMAXPROCS。
  • -run regexp:只运行匹配正则表达式的测试用例。
  • -short:缩短长时间运行的测试的测试时间。默认关闭。
  • -shuffle off,on,N:打乱测试用例的执行顺序。默认是 off,也就是不打乱,这会由上到下执行测试函数。
  • -skip regexp:跳过匹配正则表达式的测试用例。
  • -timeout t:设置测试的超时时间,默认是 10m,也就是 10 分钟。如果测试函数在超时时间内没有执行完,那么测试会 panic
  • -vet list:设置 go vet 的检查列表。默认是 all,也就是检查所有的。

性能相关

  • -benchmem:输出基准测试的内存分配情况(也就是 go test -bench . 的时候可以显示每次基准测试分配的内存)。
  • -blockprofile block.out:输出阻塞事件的分析数据。
  • -blockprofilerate n:设置阻塞事件的采样频率。默认是 1(单位纳秒)。如果没有设置采样频率,那么就会记录所有的阻塞事件。
  • -coverprofile coverage.out:输出测试覆盖率到文件 coverage.out
  • -cpuprofile cpu.out:输出 CPU 性能分析信息到文件 cpu.out
  • -memprofile mem.out:输出内存分析信息到文件 mem.out
  • -memprofilerate n:设置内存分析的采样频率。
  • -mutexprofile mutex.out:输出互斥锁事件的分析数据。
  • -mutexprofilefraction n:设置互斥锁事件的采样频率。
  • -outputdir directory:设置输出文件的目录。
  • -trace trace.out:输出跟踪信息到文件 trace.out

子测试

使用场景:当我们有多个测试用例的时候,我们可以使用子测试来组织测试代码,使得测试代码更具组织性和可读性。

  1. package main
  2. import("testing")funcTestAdd2(t *testing.T){
  3. cases :=[]struct{
  4. name string
  5. a, b, sum int}{{"case1",1,2,3},{"case2",2,3,5},{"case3",3,4,7},}for_, c :=range cases {
  6. t.Run(c.name,func(t *testing.T){
  7. sum :=Add(c.a, c.b)if sum != c.sum {
  8. t.Errorf("Sum was incorrect, got: %d, want: %d.", sum, c.sum)}})}}

输出:

  1. go-test go test
  2. --- FAIL: TestAdd2 (0.00s)
  3. --- FAIL: TestAdd2/case1 (0.00s)
  4. add_test.go:21: Sum was incorrect, got: 4, want: 3.
  5. --- FAIL: TestAdd2/case2 (0.00s)
  6. add_test.go:21: Sum was incorrect, got: 6, want: 5.
  7. --- FAIL: TestAdd2/case3 (0.00s)
  8. add_test.go:21: Sum was incorrect, got: 8, want: 7.
  9. FAIL
  10. exit status 1
  11. FAIL go-test 0.004s

我们可以看到,上面的输出中,失败的单元测试带有每个子测试的名称,这样我们就能很方便地知道是哪个测试用例失败了。

setup 和 teardown

在一般的单元测试框架中,都会提供

  1. setup

  1. teardown

的功能,

  1. setup

用来初始化测试环境,

  1. teardown

用来清理测试环境。

方法一:通过 Go 的 TestMain 方法

很遗憾的是,Go 的测试框架并没有直接提供这样的功能,但是我们可以通过 Go 的特性来实现这样的功能。

**在 Go 的测试文件中,如果有

  1. TestMain

函数,那么执行

  1. go test

的时候会执行这个函数,而不会执行其他测试函数了,其他的测试函数需要通过

  1. m.Run

来执行**,如下面这样:

  1. package main
  2. import("fmt""os""testing")funcsetup(){
  3. fmt.Println("setup")}functeardown(){
  4. fmt.Println("teardown")}funcTestAdd(t *testing.T){ifAdd(1,2)!=3{
  5. t.Error("1 + 2 != 3")}}funcTestMain(m *testing.M){setup()
  6. code := m.Run()teardown()
  7. os.Exit(code)}

在这个例子中,我们在

  1. TestMain

函数中调用了

  1. setup

  1. teardown

函数,这样我们就实现了

  1. setup

  1. teardown

的功能。

方法二:使用

  1. testify

框架

我们也可以使用 Go 中的第三方测试框架

  1. testify

来实现

  1. setup

  1. teardown

的功能(使用

  1. testify

中的

  1. suite

功能)。

  1. package main
  2. import("fmt""github.com/stretchr/testify/suite""testing")type AddSuite struct{
  3. suite.Suite
  4. }func(suite *AddSuite)SetupTest(){
  5. fmt.Println("Before test")}func(suite *AddSuite)TearDownTest(){
  6. fmt.Println("After test")}func(suite *AddSuite)TestAdd(){
  7. suite.Equal(Add(1,2),3)}funcTestAddSuite(t *testing.T){
  8. suite.Run(t,new(AddSuite))}
  1. go test

输出:

  1. go-test go test
  2. Before test
  3. After test
  4. --- FAIL: TestAddSuite (0.00s)
  5. --- FAIL: TestAddSuite/TestAdd (0.00s)
  6. add_test.go:22:
  7. Error Trace: /Users/ruby/GolandProjects/go-test/add_test.go:22
  8. Error: Not equal:
  9. expected: 4
  10. actual : 3
  11. Test: TestAddSuite/TestAdd
  12. FAIL
  13. exit status 1
  14. FAIL go-test 0.006s

我们可以看到,这里也同样执行了

  1. SetupTest

  1. TearDownTest

函数。

testing.T 可用的方法

最后,我们可以直接从

  1. testing.T

提供的 API 来学习如何编写测试代码。

基本日志输出

  • t.Log(args ...any):打印信息,不会标记测试函数为失败。
  • t.Logf(format string, args ...any):打印格式化的信息,不会标记测试函数为失败。

可能有读者会有疑问,输出不用

  1. fmt

而用

  1. t.Log

,这是因为:

  • t.Logt.Logf 打印的信息默认不会显示,只有在测试函数失败的时候才会显示。又或者我们使用 -v 参数的时候才显示,这让我们的测试输出更加清晰,只有必要的时候日志才会显示。
  • t.Logt.Logf 打印的时候,还会显示是哪一行代码打印的信息,这样我们就能很方便地定位问题。
  • fmt.Println 打印的信息一定会显示在控制台上,就算我们的测试函数通过了,也会显示,这样会让控制台的输出很乱。

例子:

  1. // add.gopackage main
  2. funcAdd(a int, b int)int{return a + b
  3. }
  1. // add_test.gopackage main
  2. import("testing")funcTestAdd(t *testing.T){
  3. t.Log("TestAdd is running")ifAdd(1,2)!=3{
  4. t.Error("Expected 3")}}

输出:

  1. go-test go test
  2. PASS
  3. ok go-test 0.004s

我们修改一下

  1. Add

函数,让测试失败,再次运行,输出如下:

  1. go-test go test
  2. --- FAIL: TestAdd (0.00s)
  3. add_test.go:8: TestAdd is running
  4. add_test.go:10: Expected 3
  5. FAIL
  6. exit status 1
  7. FAIL go-test 0.004s

我们可以发现,在测试成功的时候,

  1. t.Log

打印的日志并没有显示,只有在测试失败的时候才会显示。

如果我们想要在测试成功的时候也显示日志,可以使用

  1. -v

参数:

  1. go test -v

标记测试函数为失败

  • t.Fail():标记测试函数为失败,但是测试函数后续代码会继续执行。(让你在测试函数中标记失败情况,并收集所有失败的情况,而不是在遇到第一个失败时就立即停止测试函数的执行。)
  • t.FailNow():标记测试函数为失败,并立即返回,后续代码不会执行(通过调用 runtime.Goexit,但是 defer 语句还是会被执行)。
  • t.Failed():返回测试函数是否失败。
  • t.Fatal(args ...any):标记测试函数为失败,并输出信息,然后立即返回。等价于 t.Log + t.FailNow
  • t.Fatalf(format string, args ...any):标记测试函数为失败,并输出格式化的信息,然后立即返回。等价于 t.Logf + t.FailNow

如:

  1. package main
  2. import("testing")funcTestAdd(t *testing.T){ifAdd(1,2)!=3{
  3. t.Fatal("Expected 3")}ifAdd(2,3)!=5{
  4. t.Fatal("Expected 4")}}

这里只会输出第一个失败的测试用例,因为

  1. t.Fatal

会立即返回。

标记测试函数为失败并输出信息

  • t.Error(args ...any):标记测试函数为失败,并打印错误信息。等价于 t.Log + t.Fail
  • t.Errorf(format string, args ...any):标记测试函数为失败,并打印格式化的错误信息。等价于 t.Logf + t.Fail

这两个方法会让测试函数立即返回,不会继续执行后面的代码。

测试超时控制

  • t.Deadline():返回测试函数的截止时间(这是通过 go test -timeout 60s 这种形式指定的超时时间)。

注意:如果我们通过

  1. -timeout

指定了超时时间,当测试函数超时的时候,测试会

  1. panic

跳过测试函数中后续代码

作用:可以帮助测试代码在特定条件下灵活地跳过测试,避免不必要的测试执行,同时提供清晰的信息说明为什么跳过测试。

  • t.Skip(args ...any):跳过测试函数中后续代码,标记测试函数为跳过。等同于 t.Log + t.SkipNow
  • t.Skipf(format string, args ...any):跳过测试函数中后续代码,并打印格式化的跳过信息。等同于 t.Logf + t.SkipNow
  • t.SkipNow():跳过测试函数中后续代码,标记测试函数为跳过。这个方法不会输出内容,前面两个会输出一些信息
  • t.Skipped():返回测试函数是否被跳过。

测试清理函数

  • t.Cleanup(f func()):注册一个函数,这个函数会在测试函数结束后执行。这个函数会在测试函数结束后执行,不管测试函数是否失败,都会执行。(可以注册多个,执行顺序类似 defer,后注册的先执行)
  1. package main
  2. import("fmt""testing")funcTestAdd(t *testing.T){
  3. t.Cleanup(func(){
  4. fmt.Println("cleanup 0")})
  5. t.Cleanup(func(){
  6. fmt.Println("cleanup 1")})}

输出:

  1. go-test go test
  2. cleanup 1
  3. cleanup 0
  4. PASS
  5. ok go-test 0.004s

使用临时文件夹

  • t.TempDir():返回一个临时文件夹,这个文件夹会在测试函数结束后被删除。可以调用多次,每次都是不同的文件夹。
  1. package main
  2. import("fmt""testing")funcTestAdd(t *testing.T){
  3. fmt.Println(t.TempDir())
  4. fmt.Println(t.TempDir())}

输出:

  1. go-test go test
  2. /var/folders/dm/r_hly4w5557b000jh31_43gh0000gp/T/TestAdd4259799402/001
  3. /var/folders/dm/r_hly4w5557b000jh31_43gh0000gp/T/TestAdd4259799402/002
  4. PASS
  5. ok go-test 0.004s

临时的环境变量

  • t.Setenv(key, value string):设置一个临时的环境变量,这个环境变量会在测试函数结束后被还原。

在单元测试中,使用

  1. Setenv

函数可以模拟不同的环境变量设置,从而测试代码在不同环境下的行为。例如,你可以在测试中设置特定的环境变量值,然后运行被测试的代码,以验证代码在这些环境变量设置下的正确性。

子测试

可以将一个大的测试函数拆分成多个子测试,使得测试代码更具组织性和可读性。

  • t.Run(name string, f func(t *testing.T)):创建一个子测试,这个子测试会在父测试中执行。子测试可以有自己的测试函数,也可以有自己的子测试。

获取当前测试的名称

  • t.Name():返回当前测试的名称(也就是测试函数名)。

t.Helper()

  • t.Helper():标记当前测试函数是一个辅助函数,这样会让测试输出更加清晰,只有真正的测试函数会被标记为失败。

例子:

  1. // add.gopackage main
  2. funcAdd(a int, b int)int{return a + b +1}
  1. // add_test.gopackage main
  2. import("testing")functest(a, b, sum int, t *testing.T){
  3. result :=Add(a, b)if result != sum {
  4. t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, sum)}}funcTestAdd(t *testing.T){test(1,2,3, t)test(2,3,5, t)}

输出如下:

  1. go-test go test -v
  2. === RUN TestAdd
  3. add_test.go:10: Add(1, 2) = 4; want 3
  4. add_test.go:10: Add(2, 3) = 6; want 5
  5. --- FAIL: TestAdd (0.00s)
  6. FAIL
  7. exit status 1
  8. FAIL go-test 0.004s

我们可以看到,两个测试失败输出的报错行都是

  1. test

函数里面的

  1. t.Errorf

,而不是

  1. test

函数的调用者

  1. TestAdd

,也就是说,在这种情况下我们不好知道是

  1. test(1, 2, 3, t)

还是

  1. test(2, 3, 5, t)

失败了(当然我们这里还是挺明显的,只是举个例子),这时我们可以使用

  1. t.Helper()

  1. functest(a, b, sum int, t *testing.T){
  2. t.Helper()// 在助手函数中加上这一行
  3. result :=Add(a, b)if result != sum {
  4. t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, sum)}}

输出如下:

  1. go-test go test -v
  2. === RUN TestAdd
  3. add_test.go:16: Add(1, 2) = 4; want 3
  4. add_test.go:17: Add(2, 3) = 6; want 5
  5. --- FAIL: TestAdd (0.00s)
  6. FAIL
  7. exit status 1
  8. FAIL go-test 0.004s

这个时候,我们就很容易知道是哪一个测试用例失败了,这对于我们需要封装 helper 函数的时候很有用。


本文转载自: https://blog.csdn.net/rubys007/article/details/138654545
版权归原作者 白如意i 所有, 如有侵权,请联系我们删除。

“Go 单元测试完全指南(一)- 基本测试流程”的评论:

还没有评论