smarty / gunit

xUnit-style test fixture adapter for go test
Other
120 stars 11 forks source link

Can't share fixtures through struct embedding #21

Closed dmoles closed 3 years ago

dmoles commented 5 years ago

I have a couple of tests that require temporary directories. I tried to set up a shared fixture along these lines:

// Embeddable sub-fixture for temporary directories
type tempdirFixture struct {
    *gunit.Fixture
    tempDir string
}

func (f *tempdirFixture) SetupTempDir() {
    tempDir, err := ioutil.TempDir("", "tempdirFixture")
    f.So(err, should.BeNil) // or assertions.ShouldBeNil(err)
    f.tempDir = tempDir
}

func (f *tempdirFixture) TeardownTempDir() {
    err := os.RemoveAll(f.tempDir)
    f.So(err, should.BeNil) // or assertions.ShouldBeNil(err)
}

// plus a few other utility methods for things like creating dummy files

However, when I try embedding this in my test fixture proper, e.g.

type MyFixture {
    tempdirFixture
}

I inevitably get either panic: reflect: indirection through nil pointer to embedded struct or PANIC: runtime error: invalid memory address or nil pointer dereference, sometimes in a test method, sometimes in tempdirFixture.SetupTempDir(), sometimes deep in reflection code where I can't follow what it's doing, depending on exactly how I set up the embeds and initialize the fixtures. (I've tried embedding *tempdirFixture instead of tempdirFixture; I've tried moving *gunit.Fixture from tempdirFixture to the test fixture proper; I've tried various combinations of new() and &Type{}.)

Is this a use case you've ever tried to cover with gunit? And if so, what's the right way to do it?

Obviously I can give up and manually initialize things, but I'd like to take advantage of gunit's reflection-based setup/teardown autodetection.

mdwhatcott commented 5 years ago

@dmoles - That's a good question. It's one I've tinkered with before, but haven't gone the distance to implement. Currently, the reflection code that instantiates new instances of the fixture doesn't look for types embedded on the fixture and so they don't get initialized (hence the panic). One way to sort of get what you're going for is to do something like this:

type MyFixture {
    *gunit.Fixture // embed gunit fixture alongside your tempdirFixture.
    tempdirFixture
}

func (this *MyFixture) Setup() {
    this.tempdirFixture = & tempdirFixture{Fixture: this.Fixture}
    this.tempdirFixture. SetupTempDir()
}

func (this *MyFixture) Teardown() {
    this.tempdirFixture.TeardownTempDir()
}

In this case because of the simplicity of the embedded tempdirFixture, this approach isn't a big win since you'd have to include the Setup() and Teardown() methods on any type that embeds tempdirFixture.

I'm not opposed to exploring an implementation of your original use case of embedding a type that itself embeds the *gunit.Fixture. Feel free to submit a PR or, I might experiment a bit, depending on available time for such tinkering. Surprisingly for us, the need for this kind of feature/approach hasn't come up much over the last few years so I haven't felt a strong need to extend the software.

jerrdasur commented 5 years ago

@dmoles As I can see it's no longer a problem with go 1.13.1 and github.com/smartystreets/gunit v1.1.1

Example:

func TestEmbeddedFixture(t *testing.T) {
    gunit.Run(new(MyFixture), t)
}

// Embeddable sub-fixture for temporary directories
type tempdirFixture struct {
    *gunit.Fixture
    tempDir string
}

func (f *tempdirFixture) SetupTempDir() {
    tempDir, err := ioutil.TempDir("", "tempdirFixture")
    f.So(err, should.BeNil) // or assertions.ShouldBeNil(err)
    f.tempDir = tempDir
}

func (f *tempdirFixture) TeardownTempDir() {
    err := os.RemoveAll(f.tempDir)
    f.So(err, should.BeNil) // or assertions.ShouldBeNil(err)
}

type MyFixture struct {
    tempdirFixture
}

func (this *MyFixture) Test1() {
    this.So(1, should.Equal, 1)
}

Output:

=== RUN   TestEmbeddedFixture
=== PAUSE TestEmbeddedFixture
=== CONT  TestEmbeddedFixture
=== RUN   TestEmbeddedFixture/Test1
=== PAUSE TestEmbeddedFixture/Test1
=== CONT  TestEmbeddedFixture/Test1
--- PASS: TestEmbeddedFixture (0.00s)
    --- PASS: TestEmbeddedFixture/Test1 (0.00s)
        test_case.go:67: Test definition:

PASS

Process finished with exit code 0
mdwhatcott commented 5 years ago

@dmoles - Are you saying it now works in go 1.13.1 and gunit v1.1.1? What versions were you using before? (What changed??)