Golang数据结构库测试的可用性建议
Golang数据结构库测试的可用性建议 我有一个用于复杂数据结构(12个方法)的接口,以及多个以各自方式(不同算法)实现该接口的结构体。
创建一个包含通用测试函数的文件,该文件仅使用接口,然后为每个实现结构体调用该文件,这样做是否有意义?
这是一种设计模式吗?有没有人见过类似的做法?
Bartłomiej Klimczak:
可以举个例子吗?
我不确定是否理解了你的问题:我有一个 Graph 接口,以及它的多种实现(无向图、树、邻接矩阵等)。这个接口有 12 个方法(String()、Find()、Add() 等)。
目前我的单元测试对于所有实现都有(几乎)相同的逻辑。这意味着相同的测试代码有多个副本(每个包都有一个测试文件,其中的测试非常相似)。
我正在构思一种方法,即拥有一个包含测试的文件(该文件接收一个接口对象作为输入),然后每个测试文件(每个包一个测试文件)都调用这个公共测试文件中的函数。对于如何优雅地实现这一点,有什么建议吗?
你好,@john_stuart,我最近也在使用这种设计,觉得它非常有趣。
如果你有一个接收接口的测试文件,在不知道具体类型的情况下,你如何断言其行为?如果不同类型没有产生不同的行为,那它们之间又有什么区别呢?
对我来说,单元测试需要测试与其关联的单元。由于接口是隐式实现的,你的 Tree 类型可能会无意中实现任意数量的接口。Tree 的行为只对它的使用者重要,因此必须针对其使用者来测试它的公共 API(如 String、Find 等)。
这可能会导致你实现的其他图类型之间存在一些相似的代码,但为了保持松耦合,它们之间应该互不关心。举例来说,邻接矩阵需要关心 Tree 上的 Add 方法是如何实现的吗?
或许你可以通过尽可能继承通用行为来减少重复代码。例如,一个 DirectedGraph 类型可以像这样嵌入 Graph 类型:
type DirectedGraph struct {
Graph
backCheck T // 用于防止向后遍历的某些属性
}
如果 DirectedGraph 的 String、Find 等方法与 Graph 相同,这允许你在 Graph 上实现这些方法,从而只需在它们被实现的地方测试一次。
在 Go 中,通过接口进行通用测试是一种常见且有效的模式,尤其适用于多个实现同一接口的结构体。这种做法可以确保所有实现都满足接口的契约,同时避免重复编写测试代码。
以下是一个示例,展示如何为具有多个实现的接口创建通用测试:
首先,定义接口及其实现:
// data_structure.go
package mypackage
type ComplexDataStructure interface {
Method1() int
Method2() string
// ... 其他方法
}
// 实现 A
type ImplementationA struct {
value int
}
func (a *ImplementationA) Method1() int {
return a.value
}
func (a *ImplementationA) Method2() string {
return "ImplementationA"
}
// 实现 B
type ImplementationB struct {
data string
}
func (b *ImplementationB) Method1() int {
return len(b.data)
}
func (b *ImplementationB) Method2() string {
return "ImplementationB"
}
然后,创建通用测试文件:
// data_structure_test.go
package mypackage
import (
"testing"
)
// 通用测试函数,接受接口实例
func testComplexDataStructure(t *testing.T, ds ComplexDataStructure) {
// 测试 Method1
result := ds.Method1()
if result < 0 {
t.Errorf("Method1 returned negative value: %d", result)
}
// 测试 Method2
str := ds.Method2()
if str == "" {
t.Error("Method2 returned empty string")
}
// ... 其他接口方法的测试
}
为每个实现创建测试文件:
// implementation_a_test.go
package mypackage
import (
"testing"
)
func TestImplementationA(t *testing.T) {
impl := &ImplementationA{value: 42}
testComplexDataStructure(t, impl)
}
// implementation_b_test.go
package mypackage
import (
"testing"
)
func TestImplementationB(t *testing.T) {
impl := &ImplementationB{data: "test"}
testComplexDataStructure(t, impl)
}
这种模式在 Go 标准库和许多开源项目中都有使用。例如,testing/iotest 包中的 TestReader 就是类似的模式。它通过接受 io.Reader 接口来测试不同的读取器实现。
这种方法的优点包括:
- 确保所有实现都通过相同的测试套件
- 减少重复测试代码
- 易于添加新实现的测试
- 测试逻辑集中在一个地方,便于维护
在 Go 社区中,这种模式通常被称为"表格驱动测试的接口变体"或"可重用的接口测试"。它不是一种正式的设计模式,但被认为是 Go 测试的最佳实践之一。

