在 Go 中分离单元测试和集成测试

在 GoLang 中是否存在分离单元测试和集成测试的最佳实践(证明) ?我有单元测试(不依赖任何外部资源,因此运行得非常快)和集成测试(确实依赖任何外部资源,因此运行得较慢)。因此,当我说 go test时,我希望能够控制是否包含集成测试。

最直接的方法似乎是在主体中定义一个整合标志:

var runIntegrationTests = flag.Bool("integration", false
, "Run the integration tests (in addition to the unit tests)")

然后在每个集成测试的顶部添加一个 if 语句:

if !*runIntegrationTests {
this.T().Skip("To run this test, use: go test -integration")
}

我只能做到这样吗?我搜索了作证文件看看是否有什么变数命名原则能帮我完成这件事但什么也没找到。我错过了什么吗?

74719 次浏览

我看到了三种可能的解决方案。第一种方法是使用 短模式进行单元测试。因此,您可以使用 go test -short和单元测试,但是没有使用 -short标志来运行集成测试。标准库使用 short 模式跳过长时间运行的测试,或者通过提供更简单的数据使测试运行得更快。

第二种方法是使用一个约定,调用您的测试 TestUnitFooTestIntegrationFoo,然后使用 -run测试标志来表示要运行哪些测试。因此,单元测试使用 go test -run 'Unit',集成测试使用 go test -run 'Integration'

第三种选择是使用一个环境变量,并在测试设置中使用 os.Getenv。然后使用简单的 go test进行单元测试,使用 FOO_TEST_INTEGRATION=true go test进行集成测试。

我个人更喜欢 -short解决方案,因为它更简单,而且在标准库中使用,所以它似乎是分离/简化长时间运行的测试的一种事实上的方法。但是 -runos.Getenv解决方案提供了更大的灵活性(由于 regexp 与 -run有关,因此也需要更加谨慎)。

@ Ainar-G 建议了几种不同的测试模式。

这组来自 SoundCloud 的 Go 实践推荐使用 build 标记(在构建包的“构建约束”部分中描述)来选择运行哪些测试:

编写一个集成测试。去,并给它一个集成的构建标签。为服务地址和连接字符串等定义(全局)标志,并在测试中使用它们。

// +build integration


var fooAddr = flag.String(...)


func TestToo(t *testing.T) {
f, err := foo.Connect(*fooAddr)
// ...
}

Go test 采用 build 标记,就像 go build 一样,所以可以调用 go test -tags=integration。它还合成了一个调用标志的包 main。解析,这样任何已声明和可见的标志都将被处理并可用于您的测试。

作为一个类似的选项,您还可以使用构建条件 // +build !unit默认运行集成测试,然后通过运行 go test -tags=unit根据需要禁用它们。

@ adamc 评论:

对于任何其他尝试使用 build 标记的人来说,重要的是 // +build test注释应该是文件的第一行,并且在注释之后应该包含一个空行,否则 -tags命令将忽略该指令。

此外,构建注释中使用的标记不能有破折号,但允许使用下划线。例如,// +build unit-tests将不工作,而 // +build unit_tests将。

为了详细说明我对@Ainner-g 的出色回答的评论,在过去的一年里,我一直在使用 -shortIntegration变数命名原则的组合,以实现两个世界的最佳结合。

单元和集成在同一个文件中测试和谐性

构建标志以前强迫我有多个文件(services_test.goservices_integration_test.go等)。

相反,以下面这个例子为例,其中前两个是单元测试,最后是一个集成测试:

package services


import "testing"


func TestServiceFunc(t *testing.T) {
t.Parallel()
...
}


func TestInvalidServiceFunc3(t *testing.T) {
t.Parallel()
...
}


func TestPostgresVersionIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
...
}

注意,最后一个测试的约定是:

  1. 在测试名称中使用 Integration
  2. 检查是否在 -short标志指令下运行。

基本上,规范是这样的: “正常编写所有测试。如果它是一个长期运行的测试,或者是一个集成测试,遵循这个变数命名原则,检查 -short是否对你的同行友好。”

只运行单元测试:

go test -v -short

这将为您提供一组很好的消息,如:

=== RUN   TestPostgresVersionIntegration
--- SKIP: TestPostgresVersionIntegration (0.00s)
service_test.go:138: skipping integration test

仅运行集成测试:

go test -run Integration

这只运行集成测试。对于生产中的烟雾测试金丝雀很有用。

显然,这种方法的缺点是,如果有人运行 go test,而没有 -short标志,那么默认情况下将运行所有测试——单元测试和集成测试。

实际上,如果您的项目足够大,可以进行单元测试和集成测试,那么您很可能正在使用 Makefile,在这里您可以使用简单的指令在其中使用 go test -short。或者,只是把它放在你的 README.md文件,然后叫它一天。

我最近也在想办法解决这个问题。 这是我的标准:

  • 解决方案必须具有普遍性
  • 没有用于集成测试的单独包
  • 分离应该是完整的(我应该能够运行集成测试 只有)
  • 集成测试没有特殊的变数命名原则
  • 不需要额外的工具就可以很好地工作

上面提到的解决方案(自定义标志、自定义构建标记、环境变量)并不能真正满足上述所有条件,所以经过一番深入研究,我想出了这个解决方案:

package main


import (
"flag"
"regexp"
"testing"
)


func TestIntegration(t *testing.T) {
if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name()) {
t.Skip("skipping as execution was not requested explicitly using go test -run")
}


t.Parallel()


t.Run("HelloWorld", testHelloWorld)
t.Run("SayHello", testSayHello)
}

该实现简单明了,并且非常简单。虽然它需要一个简单的测试约定,但它不太容易出错。进一步的改进可能是将代码导出到一个 helper 函数。

用法

仅对项目中的所有包运行集成测试:

go test -v ./... -run ^TestIntegration$

运行所有测试(普通的和集成) :

go test -v ./... -run .\*

只运行 普通的测试:

go test -v ./...

这个解决方案在没有工具的情况下也能很好地工作,但是 Makefile 或某些别名可以使它更易于用户使用。它还可以很容易地集成到任何支持运行 go 测试的 IDE 中。

完整的例子可以在这里找到: https://github.com/sagikazarmark/modern-go-application

我鼓励大家看看彼得 · 伯根的方法,它很简单,避免了一些问题的建议,在其他答案: https://peter.bourgon.org/blog/2021/04/02/dont-use-build-tags-for-integration-tests.html

使用 build 标记、短模式或标志有许多缺点,请参阅 给你

我建议使用环境变量和一个可以导入到单个包中的测试助手:

func IntegrationTest(t *testing.T) {
t.Helper()
if os.Getenv("INTEGRATION") == "" {
t.Skip("skipping integration tests, set environment variable INTEGRATION")
}
}

在您的测试中,您现在可以很容易地在测试函数的开头调用:

func TestPostgresQuery(t *testing.T) {
IntegrationTest(t)
// ...
}

为什么我不推荐使用 -short或标志:

第一次检查您的存储库的人应该能够运行 go test ./...,并且所有的测试都通过了,但是如果这依赖于外部依赖关系,情况通常就不是这样了。

flag包的问题在于,它会一直工作,直到在不同的包之间进行集成测试,有些会运行 flag.Parse(),有些则不会,这会导致如下错误:

go test ./... -integration
flag provided but not defined: -integration
Usage of /tmp/go-build3903398677/b001/foo.test:

环境变量似乎是最灵活、最健壮的,并且需要的代码量最少,没有可见的缺点。