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


98

在GoLang中,是否存在将单元测试和集成测试分离的最佳实践?我混合了单元测试(不依赖任何外部资源,因此运行速度非常快)和集成测试(不依赖任何外部资源,因此运行速度较慢)。因此,我希望能够控制说的时候是否包括集成测试go test

最简单的方法似乎是在main中定义-integrate标志:

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")
}

这是我能做的最好的吗?我搜索了证词文档,以查看是否存在命名约定或对我有用的命名约定,但没有找到任何东西。我想念什么吗?


2
我认为stdlib使用-short禁用击中网络的测试(以及其他运行时间更长的东西)。否则,您的解决方案就可以了。
Volker

-short和自定义构建标志一样是一个不错的选择,但是您的标志不必位于main中。如果将var定义为var integration = flag.Bool("integration", true, "Enable integration testing.")函数之外,则变量将显示在包范围内,并且标记将正常工作
Atifm

Answers:


156

@ Ainar-G建议了几种很好的模式来分离测试。

SoundCloud的这套Go做法建议使用构建标记(在构建包的“构建约束”部分中进行描述)来选择要运行的测试:

编写一个integration_test.go,并为其提供一个集成标记。为服务地址和连接字符串之类的东西定义(全局)标志,并在测试中使用它们。

// +build integration

var fooAddr = flag.String(...)

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

go test就像go build一样接受build标签,因此您可以致电go test -tags=integration。它还合成了一个调用flag.Parse的包main,因此任何声明和可见的标志都将被处理并可供您的测试使用。

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

@adamc评论:

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

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


1
我已经使用了一段时间,这是迄今为止最合乎逻辑和最简单的方法。
奥里乐队

1
如果您在同一软件包中有单元测试,则需要// + build unit在单元测试中进行设置,并使用-tag单元来运行测试
LeoCBS 2016年

2
@ Tyler.z.yang您能否提供有关标签弃用的链接或更多详细信息?我没有找到这样的信息。我在go1.8中使用的标记用于答案中所述的方式,还用于模拟测试中的类型和功能。我认为这是替代接口的好方法。
亚历山大·格罗夫

2
对于尝试使用构建标记的其他任何人,重要的是// +build测试注释是文件的第一行,并且注释后必须包含空白行,否则-tags命令将忽略该指令。此外,尽管允许使用下划线,但构建注释中使用的标记不能带有破折号。例如,// +build unit-tests将不起作用,而// +build unit_tests
起作用

6
如何处理通配符?go test -tags=integration ./...不起作用,它会忽略该标签
Erika Dsouza

54

为了详细说明我对@ Ainar-G出色答案的评论,过去一年中,我一直在结合-short使用Integration命名规则和约定,以实现两全其美。

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

构建标志先前迫使我有多个文件(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文件中,然后将其命名为“ day”。


3
爱简单
雅各布士丹利

您是否为此测试创建单独的程序包以仅访问程序包的公共部分?还是所有混合?
Dr.eel

@ Dr.eel好吧,答案是旧的。但就我个人而言,我更喜欢两者:测试使用不同的程序包名称,以便我可以import对它进行打包和测试,最终向我展示我的API对其他人的外观。然后,我跟进需要作为内部测试包名称涵盖的所有剩余逻辑。
eduncan911 '19

@ eduncan911感谢您的回答!因此,据我所知,它package services包含一个集成测试工具,因此要将该APIfo作为黑箱进行测试,我们应该使用另一种方式命名它package services_integration_test,即不会给我们提供使用内部结构的机会。因此,用于单元测试(访问内部组件)的软件包应命名为package services。是这样吗?
Dr.eel

是的,是的。这是我的一个干净示例:github.com/eduncan911/podcast(请注意,使用示例使用100%的代码覆盖率)
eduncan911

50

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

第二种是使用约定并调用您的测试,TestUnitFoo或者TestIntegrationFoo使用-run测试标志来表示要运行的测试。因此,您可以将其go test -run 'Unit'用于单元测试和go test -run 'Integration'集成测试。

第三种选择是使用环境变量,并使用在您的测试设置中获取它os.Getenv。然后,您将对go test单元测试和FOO_TEST_INTEGRATION=true go test集成测试使用simple 。

我个人更喜欢该-short解决方案,因为它更简单并且已在标准库中使用,因此看来这是分离/简化长时间运行的测试的实际方法。但是-runos.Getenv解决方案提供了更大的灵活性(由于还需要使用正则表达式,因此也需要更加谨慎-run)。


1
请注意,Tester-GoIDE(Atom,Sublime等)通用的社区测试运行程序(例如)具有内置选项,可与-shortflag -coverage以及其他程序一起运行。因此,我在测试名称中结合使用了Integration和if testing.Short()这些测试中的检查。它让我拥有了两全其美的优势:-short在IDE中运行,并且明确地仅使用go test -run "Integration"
eduncan911

5

我最近正试图找到一种解决方案。这些是我的标准:

  • 解决方案必须是通用的
  • 没有用于集成测试的单独软件包
  • 分离应该是完整的(我应该能够运行集成测试
  • 集成测试没有特殊的命名约定
  • 无需其他工具即可正常运行

前面提到的解决方案(自定义标志,自定义构建标签,环境变量)并不能真正满足上述所有条件,因此,经过一番挖掘和探索之后,我想到了这个解决方案:

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)
}

该实现非常简单明了。尽管它要求测试使用简单的约定,但是它不太容易出错。进一步的改进可能是将代码导出到辅助函数。

用法

仅跨项目中的所有程序包运行集成测试:

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

运行所有测试(常规测试和集成测试):

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

仅运行常规测试:

go test -v ./...

该解决方案无需工具即可很好地工作,但是Makefile或某些别名可以使用户更容易使用。它也可以轻松集成到任何支持运行go测试的IDE中。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.