-
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
testing: add Cleanup method #32111
Comments
I would use the heck out of this in my tests. |
Can you provide an example of when this is better than a basic |
Because it means you can do the defer from inside a helper function. So you get this: func TestSomething(t *testing.T) {
dir := mkDir(t)
// use dir
}
func mkDir(t *testing.T) string {
name, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
t.Defer(func() {
err := os.RemoveAll(name)
if err != nil {
t.Error(err)
})
return name
} Without Defer, you have to return a func TestSomething(t *testing.T) {
dir, cleanup := mkDir(t)
defer cleanup()
// use dir
}
func mkDir(t *testing.T) (string, func()) {
name, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
return name, func() {
err := os.RemoveAll(name)
if err != nil {
t.Error(err)
})
} If it's just one thing, like a single directory you're creating, it's not that bad. But when it's a whole bunch of things, it adds a lot of noise for little benefit. |
If there are many things, then you can create a test helper wrapping type that initializes them all for you at once and the close method can clean up/close/delete all of them. This proposal adds an API that explicitly overlaps with a language feature, making tests look yet a bit more different from normal code. There is a nice symmetry (common in test and non-test code) where the creation and deferred cleanup of a thing are paired:
(think files or mutexes); where it exists, this pattern is clear and obvious, and In the end, this is all to save a single line of code per test, which feels like an un-Go-like tradeoff to me. So initially I'm mildly opposed to this feature. |
I understand where you're coming from, so let me try to explain one use case with reference to some existing tests that use this feature. Firstly, tests already look different from normal code. In tests, unlike production code, we can concern ourselves with happy path only. There's always an easy solution when things don't happen the way they should - just abort the test. That's why we have When testing more complex packages, it becomes useful to make test helper functions that set up or return one or more dependencies for the test. Sometimes helper functions run several levels deep, with a high level function calling various low level ones, all of which can call Here's an test helper function from some real test code: It's building a Without the However, given |
A simple way to solve this problem is to use a helper that opens and closes the resource and that takes the real test as a closure function parameter. This is a common idiom in Ruby, which I also use often in Go. Something like this:
|
Yes, that is a possible way of working around this, but as a personal preference in Go, I prefer to avoid this callback-oriented style of programming - it leads to more levels of indentation than I like, and I don't find it as clear. Rather than just asking for a resource to use, you have to give up your flow of control to another function which makes this style less orthogonal to other use cases. For example, callback style doesn't play well with goroutines - if you wanted to create two such resources concurrently, you'd need to break the implied invariant that you shouldn't use the resource outside of the passed function. It also means that if you do want to add some cleanup to an existing test helper, significant refactoring can be required. |
I expect Defer would get widely adopted by various test helpers, as checking for conditions at the end of a test is common pattern. For example: gomock has a Finish method that you’re supposed to defer in your test, but this is commonly overlooked. A built-in t.Defer would allow gomock to automatically register this Finish method to ensure all the conditions are met at the end of the test. |
Because the argument f to t.Defer(f) does not run at the end of the function in which that call appears, t.Defer is probably not the right name. Perhaps t.Cleanup(f) would be clearer. Except for the name, it does seem like a test case does have a well-defined "everything is done now" moment that could be worth attaching a blocking cleanup to, so this proposal seems plausible. (We have declined to add an os.AtExit, but completing a test is different from exiting a process. You really do want to get everything cleaned up before starting the next test. AtExit is too often abused for things that the OS is going to clean up anyway, clean exit or not.) |
FWIW Cleanup is similar to the name I first gave to this functionality (I actually used AddCleanup), but I changed it to defer because I thought that it's a more obvious name. The fact that you're calling it on the testing value and that it's not the usual defer keyword is, I think, sufficient clue that it won't be called at the end of the current function. You're still "deferring" the function, even if it's deferred at test level rather than function level. To me, "Cleanup" sounds like it's actually doing the cleanup there and then, rather than scheduling something to happen later, and "AddCleanup" is more accurate but clumsier than "Defer". |
I like the idea but agree (slightly) that Defer might not be the best name. |
@rogpeppe any reason not to use a func that expects an I would think that closing something that implements An example for handling a temporary file:
|
@egonelbre what would we do if we encounter an error? The current approach just lets the user decide, and it's more general. For example:
|
@mvdan since it's intended for deferred code, and would always need to run, so it shouldn't make a difference whether |
I spoke to @robpike, who said he was fine with the functionality and had a slight preference against Defer (because it's not exactly what that keyword does). AddCleanup is longer and more precise, but it doesn't seem like people would get too confused by seeing t.Cleanup taking a func(). (That is, Are there any objections to adding this as t.Cleanup? |
What about |
FWIW I still think that There are other useful semantic implications from calling it "Defer", by analogy with the
From personal experience, having used the |
I think @rogpeppe expressed concerns about I was also considering the name |
I think I propose we simply be direct and call it |
Don't mean to be nitpicky, but this would run at the end of a test, not after it. |
The emoji on #32111 (comment) suggest that Cleanup is at least not terrible - 5up 1down. The comments since then are mixed and veering into bikeshedding that is unlikely to converge. But we appear to have converged on adding the function, just not on the name. I suggest we move forward with Cleanup in the absence of a consensus about a better name. This seems like a likely accept. Leaving open a week for any further comments. |
I agree that we don't have the perfect name, but I still think |
While I can't say I like the method It's true that the method won't work exactly as Another point for names like |
-- for @golang/proposal-review |
Change https://golang.org/cl/201359 mentions this issue: |
Could the interaction with t.Parallel and subtests be documented somewhere? defer already had surprising behaviour here. |
@nightlyone please file a new issue to track and discuss. FWIW, I was also initially confused here. I ultimately found the explanation spread over a couple of places. The docs for Run say:
The package level docs for subtests say:
And the Cleanup docs read:
|
Could someone explain what's the difference between https://tip.golang.org/pkg/testing/#hdr-Subtests_and_Sub_benchmarks Test generators in IDE also generate table-driven tests so adding tear-down code is just like adding call at the end of function. |
If the subtest called by t.Run invokes t.Parallel, the Run method will return immediately, so the tear-down code will run before the test has completed. |
Just group set of Documentation specifically points this out:
|
Yes, you can do this, but it exposes an internal detail (the existence of the "group" subtest, which isn't really a subtest at all) to the external test interface, which isn't ideal. That is, if you want to specifically run |
Of course, the main reason for adding |
Another big advantage of Cleanup is that it can be called from test helpers, which is otherwise hard to do. |
It's a common requirement to need to clean up resources at the end of a test - removing temporary directories, closing file servers, etc.
The standard Go idiom is to return a Closer or a cleanup function and defer a call to that, but in a testing context, that can be tedious and add cognitive overhead to reading otherwise simple test code. It is useful to be able to write a method that returns some domain object that can be used directly without worrying the caller about the resources that needed to be created (and later destroyed) in order to provide it.
Some test frameworks allow tear-down methods to be defined on "suite" types, but this does not compose well and doesn't feel Go-like.
Instead, I propose that we add a Defer method to the testing.B, testing.T and testing.TB types that will register a function to be called at the end of the test.
The implementation could be something like this:
The quicktest package uses this approach and it seems to work well.
The text was updated successfully, but these errors were encountered: