如何在多个Go包中只运行一次数据库设置?

huangapple go评论72阅读模式
英文:

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

问题

我正在尝试在我的数据库中创建一些对象,以便我的测试可以使用一些数据。我将设置逻辑放在了一个名为testsetup的包中。然而,我发现go test会将每个包作为完全独立的实例运行,所以即使我在testsetup包中使用了sync.OnceSetup函数仍然会运行多次,因为每个包的测试都作为一个单独的Go实例运行。我真的希望保持并行运行测试,因为这样速度更快,所以我目前不考虑关闭并行化。有没有一种简洁的方法可以做到这一点?

我甚至开始考虑使用一些"dirty hacks",比如使用一个shell脚本来实现操作系统级别的同步。

这是我的包结构:

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

这是我testsetup函数的简化版本:

var onceSetup sync.Once
var data model.MockData

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

func createData() {
    // 执行一些SQL调用来创建对象。我们只想执行一次。

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

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: ...,
    }
}

答案1

得分: 1

可以做到,但可能不值得,你需要自己决定。

你需要一个实现了"测试注册表"和"测试运行器"的包,还需要另一个包作为"入口点",将它们全部连接起来并启动运行器。

最终的结构可能如下所示:

../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 # 测试注册表
│         └── runner.go # 测试运行器
└── tests
    └── start_test.go # 测试入口点

首先,让我们考虑一下完成后入口点的样子。如果你不喜欢看到的内容,那么你应该忽略答案的其余部分。

文件 module/tests/start_test.go

package tests

import (
	"testing"

    // 使用空白标识符进行“仅副作用”的导入
	_ "module/app/pkg1/tests"
	_ "module/app/pkg2/tests"
	// ...

	"module/internal/testutil"
)

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

接下来,是 module/internal/testutil/registry.go 中的注册表:

package testutil

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

//                  v: 包的目录
//                          v: 目录中的文件
//                            v: 文件中的测试
var tests = make(map[string][][]func(*testing.T))

func Register(ft ...func(*testing.T)) int {
	// 使用调用者文件的目录
	// 来映射测试。为什么这样做有用
	// 将在后面展示。
	_, f, _, _ := runtime.Caller(1)
	dir := filepath.Dir(f)

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

    // 这不是必需的,但带有返回值的函数
    // 可以用于顶级变量声明中,以避免不必要的 init() 函数。
	return 0
}

运行器在 module/internal/testutil/runner.go 中:

package testutil

import (
	"testing"
)

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

	defer func() {
		// TODO 清理...
	}()

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

现在是各个单独的包,例如 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) {
	// ...
}

这样就完成了,现在你可以进入 module/tests 目录并运行:

go test

附加说明 #1

如果你想保留单独测试各个包的能力,也可以集成进来。

首先,在 module/internal/testutil/runner.go 中添加一个新函数:

package testutil

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

// ...

func TestPkg(t *testing.T) {
	// 现在调用者文件的目录
	// 很有用。我们可以使用它来确保
	// 除了调用者的测试之外,不会执行其他测试。
	_, f, _, _ := runtime.Caller(1)
	dir := filepath.Dir(f)

	// TODO 设置...

	defer func() {
		// TODO 清理...
	}()

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

然后,在单独的测试包中添加一个单独的测试文件,例如 module/app/pkg1/tests/pkg1_test.go

package tests

import (
	"testing"

	"module/internal/testutil"
)

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

这样,你现在可以进入 module/app/pkg1/tests 目录并运行:

go test

附加说明 #2

现在,由于各个包都有自己的 _test.go 文件,如果你想使用 go test module/... 来执行模块中的所有测试,你又回到了起点,因为这不仅会运行入口点,还会导致单独的测试包被单独执行。

然而,你可以通过一个简单的环境变量来解决这个问题。只需对 testutil.TestPkg 函数进行一小的调整:

package testutil

import (
	// ...
	"os"
)

// ...

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

	// ...
}

现在...

# ... 下面的命令将按预期工作
skippkg=yes go test module/...
go test module/tests
go test module/app/pkg1/tests
英文:

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

答案2

得分: 0

你的测试设置中是否有某种阻塞机制?我认为每个包都会并行运行其测试,并并行运行它们在测试设置中所需的内容。否则,你可以按照以下方式进行设置:

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

然后在testpackage/packages_test.go中,你可以运行你的测试,并导入package1package2中的代码。

代码可能如下所示:

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)
}
英文:

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)
}

huangapple
  • 本文由 发表于 2022年4月13日 06:27:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/71849785.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定