英文:
How can I have a common test suite for multiple packages in go?
问题
当我编写一个接口时,通常方便的做法是在同一个包中定义我的测试,然后定义多个实现接口集的包,例如:
package/
package/impl/x < -- 实现 X
package/impl/y < -- 实现 Y
有没有一种简单的方法可以在子包中运行相同的测试套件(在这种情况下,位于package/*_test.go
中)?
到目前为止,我想到的最好的解决方案是添加一个测试包:
package/tests/
该包实现了测试套件,并在每个实现中添加一个测试来运行这些测试,但这有两个缺点:
1)package/tests
中的测试不在_test.go
文件中,而是成为实际库的一部分,由godoc
等文档化。
2)package/tests
中的测试由自定义测试运行器运行,该运行器必须基本上复制go test
的所有功能来扫描go测试并运行它们。
看起来这是一个相当笨拙的解决方案。
有没有更好的方法来解决这个问题?
英文:
When I'm writing an interface, its often convenient to define my tests in the same package as the interface, and then define multiple packages that implement the interface set, eg.
package/
package/impl/x <-- Implementation X
package/impl/y <-- Implementation Y
Is there an easy way to run the same test suite (in this case, located in package/*_test.go
) in the sub packages?
The best solution I've come up with so far is to add a test package:
package/tests/
Which implements the test suite, and a test in each of the implementations to run the tests, but this has two downsides:
-
The tests in
package/tests
are not in_test.go
files, and end up being part of the actual library, documented bygodoc
, etc. -
The tests in
package/tests
are run by a custom test runner, which has to basically duplicate all the functionality ofgo test
to scan for go tests and run them.
Seems like a pretty tacky solution.
Is there is a better way of doing this?
答案1
得分: 7
我不是真的不喜欢使用单独的测试库的想法。如果你有一个接口,并且对每个接口都有通用的测试,那么实现该接口的其他人可能也想使用这些测试。
你可以创建一个名为"package/test"
的包,其中包含一个函数:
// 用于测试每个实现所需的函数
type Tester struct {
func New() package.Interface
func (*package.Interface) Done()
// 需要什么就写什么。如果函数不适用,则留空
}
func TestInterface(t *testing.T, tester Tester)
注意,TestInterface
的签名与go test
所期望的不匹配。现在,对于每个包package/impl/x
,您需要添加一个文件generic_test.go
:
package x
import "testing"
import "package/test"
// 在这个特定的实现上运行通用测试
func TestInterface(t *testing.T) {
test.TestInterface(t,test.Tester{New:New})
}
其中New()
是您实现的构造函数。这种方案的优点是:
- 您的测试对于实现您的接口的任何人都是可重用的,甚至来自其他包
- 立即明确您正在运行通用测试套件
- 测试用例在实现的地方,而不是在另一个隐晦的地方
- 如果一个实现需要特殊的初始化或类似的东西,代码可以很容易地进行调整
- 它与
go test
兼容(非常好!)
当然,在某些情况下,您可能需要一个更复杂的TestInterface
函数,但这是基本思想。
英文:
I don't really dislike the idea to use a separate testing library. If you have an interface and you have generic tests for each interface, other people that implement that interface might like to use these tests as well.
You could create a package "package/test"
that contains a function
// functions needed for each implementation to test it
type Tester struct {
func New() package.Interface
func (*package.Interface) Done()
// whatever you need. Leave nil if function does not apply
}
func TestInterface(t *testing.T, tester Tester)
Notice that the signature of TestInterface
does not match to what go test
expects. Now, for each package package/impl/x
you add one file generic_test.go
:
package x
import "testing"
import "package/test"
// run generic tests on this particular implementation
func TestInterface(t *testing.T) {
test.TestInterface(t,test.Tester{New:New})
}
Where New()
is the constructor function of your implementation. The advantage with this scheme is that
- Your tests are reusable for whoever implements your interface, even from other packages
- It is immediately obvious that you run the generic test suite
- The test cases are where the implementation is and not at another, obscure place
- The code can be adapted easily if one implementation needs special initialization or similar stuff
- It's
go test
compatible (big plus!)
Of course, in some cases you need a more complicated TestInterface
function, but this is the basic idea.
答案2
得分: 1
如果您分享一段代码以供不同的包重用,那么根据定义,它是一个库。即使仅在*_test.go文件中用于测试也是如此。这与在_test.go文件中导入"testing"或"fmt"没有什么不同。而且,通过godoc对API进行文档化是一个优点,而不是缺点,在我看来。
英文:
If you share a piece of code for reuse by different packages then yes, it is a library by definition. Even when used only for testing from *_test.go files. It's no different from importing "testing" of "fmt" in the _test.go file. And having the API documented by godoc is a plus, not minus IMHO.
答案3
得分: 1
也许这里有一些混淆:
如果包a只定义了一个接口,那么就没有代码可以测试了,因为在Go中接口是没有实现的。
所以我假设包a中的接口方法有约束条件。例如,在以下接口中:
interface Walker {
Walk(step int)
Tired() bool
}
你的约定假设如果Walk步数超过500步,则Tired返回true(否则返回false),你的测试代码检查这些依赖关系(或假设、约定、不变量,无论你如何称呼它们)。
如果是这种情况,我会在包a中提供一个导出函数:
func TestWalkerContract(w Walker) error {
w.Walk(100)
if w.Tired() { return errors.New("100步后感到疲劳") }
w.Walk(450)
if !w.Tired() { return errors.New("100+450步后不感到疲劳") }
}
这样可以正确记录约定,并且可以被包b和c中实现walker接口的类型在b_test.go和c_test.go中使用来测试它们的实现。在我看来,这些类似TestWalkerContract的函数在godoc中显示是完全可以接受的。
附注:比Walk和Tired更常见的可能是保持并报告错误状态,直到清除/重置。
英文:
Maybe something gets mixed up here a bit:
If package a defines an interface only than there is no code to
test as interfaces in Go are implementation free.
So I assume the methods in your interface in package a
have constraints. E.g. in
interface Walker {
Walk(step int)
Tired() bool
}
you contract assumes that Tired returns true if more than
500 steps have been Walk'ed (and false otherwise)
and your test code checks these dependencies
(or assumption, contracts, invariants whatever you
name it).
If this is the case I would provide (in package a) an exported
function
func TestWalkerContract(w Walker) error {
w.Walk(100)
if w.Tired() { return errors.New("Tired after 100 steps") }
w.Walk(450)
if !w.Tired() { return errors.New("Not tired after 100+450 steps") }
}
Which documents the contract properly and can be used by packages
b and c with types implementing walker to test their implementations
in b_test.go and c_test.go. IMHO it is perfectly okay that these
function like TestWalkerContract are displayed by godoc.
P.S. More common than Walk and Tired might be an error state
which is kept and reported until cleared/reseted.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论