Skip to content

proposal: testing: middleware for testing.TB (?) #40984

Closed
@seebs

Description

@seebs

What version of Go are you using (go version)?

N/A, Any, or 1.14/1.15?

Does this issue reproduce with the latest release?

Sure.

What operating system and processor architecture are you using (go env)?

N/A.

What did you do?

Tried to write smarter tests.

What did you expect to see?

A thing sort of parallel to TestMain which provided a way to do things before/after individual tests.

What did you see instead?

Nothing like that.

This grew out of some discussions in the #tools channel in Gopher Slack, and some vague thoughts I've had about this a few times before. The original use case that got me thinking about this was trying to track down stray goroutines that were appearing during testing, because some tests weren't closing some resources. I implemented a tracker, which can check when these resources are opened or closed, and use TestMain to check, and report on, unclosed resources after all tests complete.

But what would be sort of neat would be a way to have each individual test check for unclosed resources after it's done, and then potentially, a corresponding way to make them use separate trackers so that they could coexist with parallel testing.

dominikh made a passing remark about this being a little structurally similar to something like middleware implementations in net/http, which take a handler and make a new handler that fixes a thing up, so that individual handlers people are writing in their problem domain don't need to think about all the other things that might be getting done for system-wide reasons.

I am not sure whether I like this idea or not, but I think it merits exploration. A significant challenge: Fatal/Fatalf exit the goroutine, and set test failure status, but some test wrappers might want a way to override that, and at least to run their own cleanup code.

Approximate sketch of an idea:

// this feels like the intro to an infomercial advertising Generics
type TestWrapper func(func (*testing.T)) func(*testing.T)
type BenchmarkWrapper func(func (*testing.B)) func(*testing.B)
type TBWrapper func(func (testing.TB)) func(testing.TB)
func (m *M) WrapTests(TestWrapper) { ... }

But it's not clear how the actual wrapper would be written. For an example, say you have a resource tracker which can confirm/deny that some class of resources has been correctly closed up. The obvious corresponding code for the above signatures is clunky:

func TrackTest(fn func (*testing.T)) (func (*testing.T)) {
    return func(t *testing.T) {
        tracker := newTracker()
        fn()
        if err := tracker.Check(); err != nil {
            t.Fatalf("resource tracker: %v", err)
        }
    }
}

That feels very duplicative. It might be nicer to have a simpler interface where the wrapper function is just expected to be invoked in place:

func TrackTest(fn func (*testing.T), t *testing.T) {
    tracker := newTracker()
    fn(t)
    if err := tracker.Check(); err != nil {
        t.Fatalf("resource tracker: %v", err)
    }
}

But what if we want this code to run even if the test failed? I suppose one answer would be that the function passed to this is not the original test function, but rather, a fancy wrapper synthesized by pkg/testing which arranges that the goroutine this is called in doesn't abort if the inner test fails.

And you might want the abiity to intercept failures, or check that you got "expected" failures (see the discussion in #39903). Which might imply a rather more elaborate setup for a test wrapper, but I'm not able to think of one that seems reasonable and sensible. It seems non-ideal to require the user to implement the full testing.T interface (and raises its own new questions in the case where T picks up new methods, like Cleanup).

I think the underlying proposal is roughly "it would be nice to be able to run things before/after individual tests, which could cause those tests to fail". The middleware-flavored solution might be the wrong one.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions