-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: testing: per-test setup and teardown support #27927
Comments
As a proposal, it would be helpful to suggest exactly how this should work, so we can see what new API would be required. |
There are many different ways this could work, and I don't want to prescribe one for fear of flailing wildly and having it be rejected when other ways of implementing the same feature would work. A few ideas:
The last one sounds the most approachable to me, so I will flesh it out a bit. How I would imagine the usage would look: package my_test
func TestMain(m *testing.M) {
for _, t := range m.Tests {
setup()
t.Run() // invokes the current test function
if err := cleanup(); err != nil {
t.Error("error cleaning up test:", err)
}
}
} Implementation: package testing
type M struct {
...
Tests []TestRunner
}
type TestRunner struct {
*T // embed the testing.T that will be used for this test.
// other unexported fields as needed
}
func (TestRunner) Run() // runs this test with this TestRunner's *T I haven't looked into the implementation of the |
/cc @mpvl |
Various of us who have had to write code like this have found it helpful to write it explicitly instead of having an implicit thing that applies to every test in the package. Sometimes only 9 of 10 tests need a particular setup/teardown. Another common pattern is to put setup/teardown in one Test and then put the tests that need that setup/teardown into subtests. Or write a helper that does the setup/teardown and pass in the 'body' to run in between. There are lots of ways. We shouldn't hard-code one. |
Per-test setup and teardown is a common feature in most testing frameworks, and is supported in Java, C++, and Python, in OSS and at Google. I know there are other ways to implement this, but they are all cumbersome or error-prone or both. Something included in the language's primary testing framework would make it both easy to use and reliable. |
+1. This is something extremely common in test frameworks. @rsc, please reconsider. This might not be something y'all use, but it is something the community uses:
|
Why not just use a single test function with subtests? Why is that any harder to use than the suggestions in this issue? |
@ianlancetaylor What would that look like for a package with 10~100 tests? Something like this is what I was imagining: func testFoo(t *testing.T) { ... }
func testBar(t *testing.T) { ... }
func testBaz(t *testing.T) { ... }
func TestEverything(t *testing.T) {
// Maintaining this map is error-prone and cumbersome (note the subtle bug):
fs := map[string]func(*testing.T){"testFoo": testFoo, "testBar": testBar, "testBaz": testBar}
// You may be able to use the `importer` package to enumerate tests instead,
// but that starts getting complicated.
for name, f := range fs {
setup()
t.Run(name, f)
teardown()
}
} Is there a better way to do this? |
var tests = []func(t *testing.T){
func(t *testing.T) {
...
},
func(t *testing.T) {
...
},
...
}
func TestEverything(t *testing.T) {
for i, fn := range tests {
setup()
t.Run(strconv.Itoa(i), fn)
teardown()
}
} Yes, it's more awkward, but it seems manageable, and there are cases where a bit of awkwardness is preferable to hidden magic. |
There are several undesirable aspects to your variant:
|
There are obvious mitigations for those comments, but I said up front that the approach is awkward. On the other hand, implicit test setup and teardown adds actions that are not clearly visible when looking at a large testsuite. And a single package will often have different kinds of tests, some of which need a particular kind of setup and teardown and some of which need a different kind, so over time the complexity of the setup and teardown tend to increase. I don't think there is an obvious win here so we are choosing to be explicit and simple, as is the common pattern for the Go language. |
The 3rd approach proposed in #27927 (comment) is explicit, simple, and no more magical than any test that uses the existing Just because some test suites don't want per-test setup/cleanup for all tests in the package doesn't mean that all test suites don't. I'd like to ensure that all tests in grpc check for goroutine leaks (similar to |
I suggest that you try writing a new proposal that changes the |
That is exactly what this issue proposed. If you would like to change the title or file a new proposal based on this one, please feel free to do so. Instead, I have found a workaround to accomplish what we need using reflection, sub-tests, and a simple grep command to enforce its usage. If anyone else needs per-test setup/teardown and is interested in seeing our approach, take a look at grpc/grpc-go#2523, and feel free to copy and use I still believe this is a significant shortcoming of Go's built-in testing framework, and our workaround is not ideal, but it is effective enough and unobtrusive enough that I can live with it and move on. |
I've written a work-around for I hope it helps someone out there! |
For those who need fixture injection and setup/teardown at different scopes, please feel free to give https://github.com/houqp/gtest a try. |
This blog: https://blog.golang.org/subtests describes how to use subtests for setup and tear-down. @dfawley: to your points:
As it is already possible to do setup/tear-down, it comes down to a matter of style and opinion. It seems to me that this kind of functionality falls into the same category as BDD testing support, which belongs in a separate package possibly implemented on top of the testing package along the lines @houqp referred to. But I would first and strongly consider whether the suggested techniques are not sufficient. Adding setup/tear-down magic is yet another thing to learn for readers of code. |
The only setup & teardown mentioned in this blog post is a single setup/teardown surrounding a group of sub-tests. This issue is to have the setup/teardown before/after each sub-test indivually.
It's not the "bit more writing" that bothers me, but the duplication. Needing to list all the test functions as sub-tests means the same test needs to be mentioned three times: first when writing the test, second to give t.Run() a name string, and third to invoke it. This adds too much potential for error in either omission or substitution. What I came up with avoids any of this duplication in exchange for a small amount of magic. Ideally the language would provide some help for this, but given the absence of it, I'll take some magic over error-prone techniques any day. |
I dont get why this has to be so hacky. Simply extend the existing TestMain to provide an area for setup/teardown for each test like the following:
Why not something like this? |
I like @mgwidmann's approach. It is quite straightforward and clear. |
How about to use context.Context to manage resources associated with the test? Consider the following: func dbHelper(t *testing.T) *sql.DB {
t.Helper()
// create database connection...
// teardown routine
go func() {
// routine waits on context channel and closes database handler
<-t.Context().Done()
db.Close()
}()
return db
}
func TestFoo(t *testing.T) {
db := dbHelper(t)
// test code continues...
} Lots of resource providers already use context.Context to manage resource lifetime. |
@sineemore this misses the purpose of this proposal. Needing to call a function at the beginning/end of every test in a package introduces a large chance of error, i.e. it's easy to forget to make the call. In your case it's providing a resource -- it's more traditional in that case to have |
@dfawley, yes, that's the way I'm doing it right now. For me most of the time a test case requires some kind of resource, so it is possible to add per test functionality to resource provider function. There were proposals to add context support already, btw. |
@mgwidmann In the source code, I can only see the Where is the |
Can we reopen this issue given the support for @mgwidmann's approach? Personally I think it's a very good compromise and will offer the flexibility of having a nice setup/teardown per test without being hacky or introducing a sledgehammer like Ginkgo or Testify Suites. I can think of multiple reasons why it's useful to have and it's common place in other languages' testing tools for good reason too |
@stevestotter This issue has gotten quite a few comments and is fairly unclear. I suggest that you open a new issue explaining precisely what change should be made to the testing package. Thanks. |
There are some checks that are helpful to perform after every test runs, e.g. monitoring for leaked goroutines. However, remembering to add these checks to every single test in a package is difficult and cumbersome. The
testing
package should provide a way to allow setup and cleanup code to be run globally for every test in a package, which has access to thetesting.T
for each test case. Note thatTestMain
does not allow you to run code between test functions; only at the very beginning and end of the whole package.The text was updated successfully, but these errors were encountered: