英文:
Separating unit tests and integration tests in Go
问题
在Go语言(使用testify框架)中,是否有一种已经确立的最佳实践来区分单元测试和集成测试?我有一些单元测试(不依赖于任何外部资源,因此运行非常快),还有一些集成测试(依赖于外部资源,因此运行较慢)。因此,我希望能够控制在运行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")
}
这是我能做的最好的方法吗?我搜索了testify的文档,看是否有命名约定或其他方法可以实现这一点,但没有找到。我有什么遗漏吗?
英文:
Is there an established best practice for separating unit tests and integration tests in GoLang (testify)? I have a mix of unit tests (which do not rely on any external resources and thus run really fast) and integration tests (which do rely on any external resources and thus run slower). So, I want to be able to control whether or not to include the integration tests when I say go test
.
The most straight-forward technique would seem to be to define a -integrate flag in main:
var runIntegrationTests = flag.Bool("integration", false
, "Run the integration tests (in addition to the unit tests)")
And then to add an if-statement to the top of every integration test:
if !*runIntegrationTests {
this.T().Skip("To run this test, use: go test -integration")
}
Is this the best I can do? I searched the testify documentation to see if there is perhaps a naming convention or something that accomplishes this for me, but didn't find anything. Am I missing something?
答案1
得分: 196
@Ainar-G建议使用几种很好的模式来分离测试。
SoundCloud的这组Go实践建议使用构建标签(在构建包的“构建约束”部分中描述)来选择要运行的测试:
编写一个integration_test.go文件,并给它一个名为integration的构建标签。为诸如服务地址和连接字符串之类的事物定义(全局)标志,并在测试中使用它们。
// +build integration var fooAddr = flag.String(...) func TestToo(t *testing.T) { f, err := foo.Connect(*fooAddr) // ... }
go test像go build一样接受构建标签,所以你可以调用
go test -tags=integration
。它还会合成一个调用flag.Parse的main包,因此任何声明和可见的标志都将被处理并在测试中可用。
作为类似的选项,您还可以通过使用构建条件// +build !unit
来默认运行集成测试,然后通过运行go test -tags=unit
来按需禁用它们。
@adamc评论道:
对于其他试图使用构建标签的人来说,重要的是// +build test
注释是文件的第一行,并且在注释之后包含一个空行,否则-tags
命令将忽略该指令。
此外,构建注释中使用的标签不能有破折号,但允许使用下划线。例如,// +build unit-tests
将无法工作,而// +build unit_tests
将可以。
英文:
@Ainar-G suggests several great patterns to separate tests.
This set of Go practices from SoundCloud recommends using build tags (described in the "Build Constraints" section of the build package) to select which tests to run:
> Write an integration_test.go, and give it a build tag of integration. Define (global) flags for things like service addresses and connect strings, and use them in your tests.
>
> // +build integration
>
> var fooAddr = flag.String(...)
>
> func TestToo(t *testing.T) {
> f, err := foo.Connect(*fooAddr)
> // ...
> }
>
> go test takes build tags just like go build, so you can call go test -tags=integration
. It also synthesizes a package main which calls flag.Parse, so any flags declared and visible will be processed and available to your tests.
As a similar option, you could also have integration tests run by default by using a build condition // +build !unit
, and then disable them on demand by running go test -tags=unit
.
@adamc comments:
For anyone else attempting to use build tags, it's important that the // +build test
comment is the first line in your file, and that you include a blank line after the comment, otherwise the -tags
command will ignore the directive.
Also, the tag used in the build comment cannot have a dash, although underscores are allowed. For example, // +build unit-tests
will not work, whereas // +build unit_tests
will.
答案2
得分: 98
详细说明一下我对@Ainar-G的出色回答的评论,过去一年来,我一直在使用-short
与Integration
命名约定的组合来实现最佳效果。
单元测试和集成测试和谐共存,同一文件中
以前的构建标志强制我使用多个文件(services_test.go
,services_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")
}
...
}
注意最后一个测试遵循以下约定:
- 在测试名称中使用
Integration
。 - 检查是否在
-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
文件中并完成即可。
英文:
To elaborate on my comment to @Ainar-G's excellent answer, over the past year I have been using the combination of -short
with Integration
naming convention to achieve the best of both worlds.
Unit and Integration tests harmony, in the same file
Build flags previously forced me to have multiple files (services_test.go
, services_integration_test.go
, etc).
Instead, take this example below where the first two are unit tests and I have an integration test at the end:
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")
}
...
}
Notice the last test has the convention of:
- using
Integration
in the test name. - checking if running under
-short
flag directive.
Basically, the spec goes: "write all tests normally. if it is a long-running tests, or an integration test, follow this naming convention and check for -short
to be nice to your peers."
Run only Unit tests:
go test -v -short
this provides you with a nice set of messages like:
=== RUN TestPostgresVersionIntegration
--- SKIP: TestPostgresVersionIntegration (0.00s)
service_test.go:138: skipping integration test
Run Integration Tests only:
go test -run Integration
This runs only the integration tests. Useful for smoke testing canaries in production.
Obviously the downside to this approach is if anyone runs go test
, without the -short
flag, it will default to run all tests - unit and integration tests.
In reality, if your project is large enough to have unit and integration tests, then you most likely are using a Makefile
where you can have simple directives to use go test -short
in it. Or, just put it in your README.md
file and call it the day.
答案3
得分: 62
我看到三种可能的解决方案。第一种是在单元测试中使用短模式。因此,你可以在单元测试中使用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
命令运行集成测试。
个人而言,我更喜欢使用-short
解决方案,因为它更简单,并且在标准库中被使用,所以它似乎是一种事实上的分离/简化长时间运行测试的方式。但是,-run
和os.Getenv
解决方案提供了更多的灵活性(同时也需要更多的注意,因为-run
涉及正则表达式)。
英文:
I see three possible solutions. The first is to use the short mode for unit tests. So you would use go test -short
with unit tests and the same but without the -short
flag to run your integration tests as well. The standard library uses the short mode to either skip long-running tests, or make them run faster by providing simpler data.
The second is to use a convention and call your tests either TestUnitFoo
or TestIntegrationFoo
and then use the -run
testing flag to denote which tests to run. So you would use go test -run 'Unit'
for unit tests and go test -run 'Integration'
for integration tests.
The third option is to use an environment variable, and get it in your tests setup with os.Getenv
. Then you would use simple go test
for unit tests and FOO_TEST_INTEGRATION=true go test
for integration tests.
I personally would prefer the -short
solution since it's simpler and is used in the standard library, so it seems like it's a de facto way of separating/simplifying long-running tests. But the -run
and os.Getenv
solutions offer more flexibility (more caution is required as well, since regexps are involved with -run
).
答案4
得分: 17
我最近试图找到一个解决方案。这是我的标准:
- 解决方案必须是通用的。
- 不需要单独的集成测试包。
- 分离必须完全(我应该能够仅运行集成测试)。
- 集成测试不需要特殊的命名约定。
- 它应该能够在没有额外工具的情况下正常工作。
上述的解决方案(自定义标志、自定义构建标签、环境变量)并不能完全满足上述所有标准,所以经过一番探索和尝试,我提出了以下解决方案:
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
英文:
I was trying to find a solution for the same recently.
These were my criteria:
- The solution must be universal
- No separate package for integration tests
- The separation should be complete (I should be able to run integration tests only)
- No special naming convention for integration tests
- It should work well without additional tooling
The aforementioned solutions (custom flag, custom build tag, environment variables) did not really satisfy all the above criteria, so after a little digging and playing I came up with this solution:
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)
}
The implementation is straightforward and minimal. Although it requires a simple convention for tests, but it's less error prone. Further improvement could be exporting the code to a helper function.
Usage
Run integration tests only across all packages in a project:
go test -v ./... -run ^TestIntegration$
Run all tests (regular and integration):
go test -v ./... -run .\*
Run only regular tests:
go test -v ./...
This solution works well without tooling, but a Makefile or some aliases can make it easier to user. It can also be easily integrated into any IDE that supports running go tests.
The full example can be found here: https://github.com/sagikazarmark/modern-go-application
答案5
得分: 4
我鼓励你看一下Peter Bourgon的方法,它简单且避免了其他答案中的一些问题:https://peter.bourgon.org/blog/2021/04/02/dont-use-build-tags-for-integration-tests.html
英文:
I encourage you to look at Peter Bourgons approach, it is simple and avoids some problems with the advice in the other answers: https://peter.bourgon.org/blog/2021/04/02/dont-use-build-tags-for-integration-tests.html
答案6
得分: 3
使用构建标签、短模式或标志存在许多缺点,可以参考这里。
我建议使用环境变量和一个可导入到各个包中的测试助手:
func IntegrationTest(t *testing.T) {
t.Helper()
if os.Getenv("INTEGRATION") == "" {
t.Skip("跳过集成测试,请设置环境变量 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:
环境变量似乎是最灵活、最健壮且需要最少代码的选择,没有明显的缺点。
英文:
There are many downsides to using build tags, short mode or flags, see here.
I would recommend using environment variables with a test helper that can be imported into individual packages:
func IntegrationTest(t *testing.T) {
t.Helper()
if os.Getenv("INTEGRATION") == "" {
t.Skip("skipping integration tests, set environment variable INTEGRATION")
}
}
In your tests you can now easily call this at the start of your test function:
func TestPostgresQuery(t *testing.T) {
IntegrationTest(t)
// ...
}
Why I would not recommend using either -short
or flags:
Someone who checks out your repository for the first time should be able to run go test ./...
and all tests are passing which is often not the case if this relies on external dependencies.
The problem with the flag
package is that it will work until you have integration tests across different packages and some will run flag.Parse()
and some will not which will lead to an error like this:
go test ./... -integration
flag provided but not defined: -integration
Usage of /tmp/go-build3903398677/b001/foo.test:
Environment variables appear to be the most flexible, robust and require the least amount of code with no visible downsides.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论