前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#90 Not exploring all the Go testing features

Go语言中常见100问题-#90 Not exploring all the Go testing features

作者头像
数据小冰
发布2023-09-10 15:45:16
1900
发布2023-09-10 15:45:16
举报
文章被收录于专栏:数据小冰数据小冰
golang测试技巧

单元测试是每个开发人员必须掌握的开发技能,Go语言特别注重单元测试,所以每个Gopher需要知道如何进行单元测试,使用什么参数控制测试效果,提升我们编写的代码质量,本文讨论相关单测技巧。

代码覆盖率

在开发过程中,想要直观的看到哪些代码已近被测试代码覆盖,使用 -coverprofile 参数,操作命令如下, 注意下面 ./...表示递归目录。

代码语言:javascript
复制
go test -coverprofile=coverage.out ./...

执行上述命令会产生一个 coverage.out 文件, 然后使用 go tool cover 命令将 coverage.out 转换为html格式,在浏览器查看,具体命令如下:

代码语言:javascript
复制
go tool cover -html=coverage.out

默认情况下,只对当前包中的代码产生覆盖率。下面举例说明,现有代码结构如下。

各个文件中的代码如下:

  • foo.go
代码语言:javascript
复制
package foo

func sub(a, b int) int {
 return a - b
}
  • foo_test.go
代码语言:javascript
复制
package foo

import (
 "myapp/bar"
 "testing"
)

func Test_sub(t *testing.T) {
 type args struct {
  a int
  b int
 }
 tests := []struct {
  name string
  args args
  want int
 }{
  {
   name: "sub",
   args: args{
    a: 2,
    b: 1,
   },
   want: 1,
  },
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   if got := sub(tt.args.a, tt.args.b); got != tt.want {
    t.Errorf("sub() = %v, want %v", got, tt.want)
   }
  })
 }
}

func Test_Add(t *testing.T) {
 type args struct {
  a int
  b int
 }
 tests := []struct {
  name string
  args args
  want int
 }{
  {
   name: "Add",
   args: args{
    a: 1,
    b: 2,
   },
   want: 3,
  },
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   if got := bar.Add(tt.args.a, tt.args.b); got != tt.want {
    t.Errorf("add() = %v, want %v", got, tt.want)
   }
  })
 }
}

  • bar.go
代码语言:javascript
复制
package bar

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

func Add(a, b int) int {
 return a + b
}
  • bar_test.go
代码语言:javascript
复制
package bar

import "testing"

func Test_add(t *testing.T) {
 type args struct {
  a int
  b int
 }
 tests := []struct {
  name string
  args args
  want int
 }{
  {
   name: "add",
   args: args{
    a: 1,
    b: 2,
   },
   want: 3,
  },
 }
 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   if got := add(tt.args.a, tt.args.b); got != tt.want {
    t.Errorf("add() = %v, want %v", got, tt.want)
   }
  })
 }
}

在myapp目录下,执行单元测试,得到覆盖率文件 coverage.out文件

将coverage.out转成html形式在浏览器中查看, 可以看到foo.go覆盖率为100%, bar.go覆盖率只有50%, Add函数没有覆盖。

为啥Add函数没有覆盖呢?我们不是在foo_test.go中编写了Add函数测试代码吗?这验证了前面说的,「默认情况下,只对当前包中的代码产生覆盖率」, 因为Add函数不属于当前foo包中的代码,所以没有产生它的覆盖率。有解决办法吗?

当然有,使用参数 -coverpkg,可以解决上述问题,下面进行验证。

同样在转为html形式在浏览器查看如下, 此时所有代码都是100%覆盖。

「NOTE 不要一味追求代码覆盖率,代码覆盖率100%并不表示代码没有任何bug, 我们更应该关注各种场景功能,而不是覆盖率本身。」

如何对包外代码进行测试

编写单元测试时,有两种关注点,一种是关注内部实现,另一种是关注外在行为。假设对外提供一个API,我们测试的关注重点应该是外在行为,而不是实现细节。因为如果代码重构了或者内部逻辑修改了,对外提供的API通常是不变的,所以测试也将保持不变。具体就是在包外编写测试代码。

在Go语言中,同一个文件夹中的代码都属于同一个包,但是有一种例外情况,测试文件可以属于 _test 包。例如,下面的代码在 counter.go文件中并且属于 counter包。

代码语言:javascript
复制
package counter

import "sync/atomic"

var count uint64

func Inc() uint64 {
   atomic.AddUint64(&count, 1)
   return count
}

对于counter.go代码的测试文件counter_test.go通常都是放在counter.go所在包中,这样可以直接访问count变量。但也可以放在 counter_test包中,像下面这样。

代码语言:javascript
复制
package counter_test

import (
 "counter"
 "testing"
)

func TestCount(t *testing.T) {
 if counter.Inc() != 1 {
  t.Errorf("expected 1")
 }
}

整个代码目前结构如下,counter.go和counter_test.go文件虽然都放在counter目录下,但是它们属于不同的包,counter.go的package为counter, counter_test.go的package为counter_test.

执行单元测试,同样输出覆盖率文件。

通过上面这种方法,在测试文件中只能访问被测代码对外提供的函数和可导出变量,不能访问内部变量,像counter.go中的count变量,确保测试代码只关注外在行为,而不是内部实现。

简化代码

编写测试代码时, 我们可以采用与正式代码不同的方法处理错误。例如,测试函数中需要一个 Customer对象,我们要创建这样一个结构体对象,考虑到创建过程可以复用,决定编写一个 createCustomer函数用于构建Customer对象,函数返回值为创建的对象和error,实现代码如下。

代码语言:javascript
复制
func TestCustomer(t *testing.T) {
    customer, err := createCustomer("foo")
    if err != nil {
        t.Fatal(err)
    }
    // ...
}
 
func createCustomer(someArg string) (Customer, error) {
    // Create customer
    if err != nil {
        return Customer{}, err
    }
    return customer, nil
}

在测试函数TestCustomer中调用构造函数createCustomer创建一个Customer对象,并判断err是否非nil, 没有问题执行后续代码。由于现在编写的是测试代码,可以简化错误处理, 具体是将 *testing.T变量传递给createCustomer函数, 实现代码如下。

代码语言:javascript
复制
func TestCustomer(t *testing.T) {
    customer := createCustomer(t, "foo")
    // ...
}
 
func createCustomer(t *testing.T, someArg string) Customer {
    // Create customer
    if err != nil {
        t.Fatal(err)
    }
    return customer
}

这样在测试代码TestCustomer中只管获取Customer对象,不用关心error,将error处理下沉到创建函数中, 如果创建失败,直接通过 t.Fatal(err)抛出问题终止代码执行,使得测试代码更清爽易读。

测试固件

在某些测试场景下,我们需要预先构造测试环境。例如,在集成测试中,我们需要启动一个docker,测试完成后stop掉它。可以使用 setUptearDown测试固件来执行创建与销毁操作。

在每次测试时调用设置函数预先执行,结合defer函数,调用销毁函数当测试执行完后, 示例代码如下。

代码语言:javascript
复制
 func TestMySQLIntegration(t *testing.T) {
    setupMySQL()
    defer teardownMySQL()
    // ...
}

我们可以向t中注册清理函数,当测试逻辑执行完后调用。举例说明,假定TestMySQLIntegration函数需要通过createConnection创建一个数据库连接,当测试函数执行完成之后,需要关闭连接。可以将关闭连接逻辑注册到 t.Cleanup中。

代码语言:javascript
复制
func TestMySQLIntegration(t *testing.T) {
    // ...
    db := createConnection(t, "tcp(localhost:3306)/db")
    // ...
}
 
func createConnection(t *testing.T, dsn string) *sql.DB {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        t.FailNow()
    }
    t.Cleanup(
        func() {
            _ = db.Close()
        })
    return db
}

TestMySQLIntegration执行完后,将会自动调用t.Cleanup方法,关闭数据库连接。通过这种方法,有效降低资源泄露风险。注意,我们可以注册多个cleanup函数,它们调用顺序像defer那样,先注册的后被调用。

代码语言:javascript
复制
// 注册多个cleanup函数
t.Cleanup(func() {
   fmt.Println("clean up 1")
  })

t.Cleanup(func() {
   fmt.Println("clean up 2")
  })

通过TestMain函数可以在运行所有测试前执行一些初始化逻辑(如创建数据库链接),或所有测试都运行结束之后执行一些清理逻辑(释放数据库连接),如果测试文件中定义了这个函数,则go test命令会直接运行这个函数,否则go test会创建一个默认的TestMain()函数。这个函数的默认行为就是运行文件中定义的测试。我们自定义TestMain()函数时,也需要手动调用m.Run()方法运行测试函数,否则测试函数不会运行。

默认的TestMain函数如下:

代码语言:javascript
复制
func TestMain(m *testing.M) {
    os.Exit(m.Run())
}

编写自定义的TestMain函数,在测试函数执行前执行后做一些其它逻辑。

代码语言:javascript
复制
func TestMain(m *testing.M) {
    setupMySQL()
    code := m.Run()
    teardownMySQL()
    os.Exit(code)
}
本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-25,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • golang测试技巧
    • 代码覆盖率
      • 如何对包外代码进行测试
        • 简化代码
          • 测试固件
          相关产品与服务
          腾讯云服务器利旧
          云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com