Home > Enterprise >  How to run database setup only once from multiple Go packages?
How to run database setup only once from multiple Go packages?

Time:04-15

I'm trying to create some objects in my database, so that my tests can have some data to work with. I've put my setup logic into a package testsetup. However, I've discovered that go test runs each package as a totally separate instance, so that even though I'm using sync.Once in my testsetup package, Setup still runs multiple times because each package's tests run as a separate Go instance. I really want to keep running my tests in parallel because it's a lot faster, so I'm not currently considering turning off parallelization. Is there a clean way I can do this?

I'm even starting to consider dirty hacks at this point, like using a shell script to implement os-level synchronization.

Here's my package structure:

testsetup
    testsetup.go
package1
    package1.go
    package1_test.go
package2
    package2.go
    package2_test.go


And here's a simplified version of my testsetup function:

var onceSetup sync.Once
var data model.MockData

func Setup() model.MockData {
    onceSetup.Do(createData)
    return data
}

func createData() {
    // Do some SQL calls to create the objects. We only want to do this once.

    data = model.Data{
        Object1: ...,
        Object2: ...,
    }
}

CodePudding user response:

Is there some sort of blocking mechanism in your testsetup? I would think that each package would run its tests in parallel still and run what they need from testsetup in parallel. Otherwise you could make it like this:

testsetup
    testsetup.go
    packages_test.go
package1
    package1.go
package2
    package2.go

And then in testpackage/packages_test.go, is where you run your tests, importing the code in package1 and package2

It could look something like this:

package testpackage

import (
 p1 "project/root/package1"
 p2 "project/root/package2"
)

func TestPackages(t *testing.T) {

    setup := Setup()
    t.Parallel()
    t.Run("Package1Test", func(t *testing.T) { package1Test(t, setup) })
    t.Run("Package2Test", func(t *testing.T) { package2Test(t, setup) })

}

func package1Test(t *testing.T, d model.MockData) {
   err := p1.RunYourFunc(d.data)
   require.NoError(t, err)
}

func package2Test(t *testing.T, d model.MockData) {
   err := p2.OtherFunc(d.data)
   require.NoError(t, err)
}

CodePudding user response:

It can be done but it may not be worth it, you'll have to decide that for yourself.

You'll need a package that implements a "test registry" and a "test runner", and another package that is the "entrypoint" that ties it all together and starts the runner.

The resulting structure could look something like this:

../module
├── app
│   ├── pkg1
│   │   ├── foo.go
│   │   ├── ...
│   │   └── tests
│   │       ├── test_foo.go
│   │       ├── ...
│   │       └── pkg1_test.go
│   └── pkg2
│       ├── ...
│       ├── bar.go
│       └── tests
│           ├── ...
│           ├── test_bar.go
│           └── pkg2_test.go
├── go.mod
├── internal
│   └── testutil
│       ├── registry.go # the test registry
│       └── runner.go # the test runner
└── tests
    └── start_test.go # the test entrypoint

First, let's consider what the entrypoint will look like once this is done. It may be that you don't like what you see, in that case you should probably ignore the rest of the answer.

File module/tests/start_test.go:

package tests

import (
    "testing"

    // Use the blank identifier for "side-effect-only" imports
    _ "module/app/pkg1/tests"
    _ "module/app/pkg2/tests"
    // ...

    "module/internal/testutil"
)

func Test(t *testing.T) {
    testutil.TestAll(t)
}

Next, the registry in module/internal/testutil/registry.go:

package testutil

import (
    "path/filepath"
    "runtime"
    "testing"
)

//                  v: the directory of a package
//                          v: the files in a directory
//                            v: the tests in a file
var tests = make(map[string][][]func(*testing.T))

func Register(ft ...func(*testing.T)) int {
    // Use the directory of the Caller's file
    // to map the tests. Why this can be useful
    // will be shown later.
    _, f, _, _ := runtime.Caller(1)
    dir := filepath.Dir(f)

    tests[dir] = append(tests[dir], ft)

    // This is not necessary, but a function with a return
    // can be used in a top-level variable declaration which
    // can be used to avoid unnecessary init() functions.
    return 0
}

The runner in module/internal/testutil/runner.go:

package testutil

import (
    "testing"
)

func TestAll(t *testing.T) {
    // TODO setup ...

    defer func() {
        // TODO teardown ...
    }()

    // run
    for _, dir := range tests {
        for _, file := range dir {
            for _, test := range file {
                test(t)
            }
        }
    }
}

Now the individual packages, e.g. module/app/pkg1/tests/test_foo.go:

package tests

import (
    "testing"

    "module/internal/testutil"
)

var _ = testutil.Register(
    TestFoo1,
    TestFoo2,
)

func TestFoo1(t *testing.T) {
    // ...
}

func TestFoo2(t *testing.T) {
    // ...
}

That's it, you can now go to the module/tests "entrypoint" and run:

go test

ADDENDUM #1

If you want to retain the ability to test the individual packages separately then that can be integrated as well.

First, add a new function to the runner in module/internal/testutil/runner.go:

package testutil

import (
    // ...
    "path/filepath"
    "runtime"
)

// ...

func TestPkg(t *testing.T) {
    // Now the directory of the Caller's file
    // comes in handy. We can use it to make
    // sure no other tests but the caller's
    // will get executed.
    _, f, _, _ := runtime.Caller(1)
    dir := filepath.Dir(f)

    // TODO setup ...

    defer func() {
        // TODO teardown ...
    }()

    // run
    for _, file := range tests[dir] {
        for _, test := range file {
            test(t)
        }
    }
}

And in the individual test package add a single test file, e.g. module/app/pkg1/tests/pkg1_test.go:

package tests

import (
    "testing"

    "module/internal/testutil"
)

func Test(t *testing.T) {
    testutil.TestPkg(t)
}

That's it, now you can cd into module/app/pkg1/tests and run:

go test

ADDENDUM #2

Now, with the individual packages having their own _test.go file, you are back to square one if you want to use go test module/... to execute all the tests in the module, since that would not only run the entrypoint but also cause the individual test packages to be executed individually.

You can work around that problem with a simple environment variable however. Just a small adjustment to the testutil.TestPkg function:

package testutil

import (
    // ...
    "os"
)

// ...

func TestPkg(t *testing.T) {
    if os.Getenv("skippkg") == "yes" {
        return
    }

    // ...
}

And now...

# ... the following will work as you'd expect
skippkg=yes go test module/...
go test module/tests
go test module/app/pkg1/tests
  • Related