Golang数据结构库测试的可用性建议

Golang数据结构库测试的可用性建议 我有一个用于复杂数据结构(12个方法)的接口,以及多个以各自方式(不同算法)实现该接口的结构体。

创建一个包含通用测试函数的文件,该文件仅使用接口,然后为每个实现结构体调用该文件,这样做是否有意义?

这是一种设计模式吗?有没有人见过类似的做法?

4 回复

能举个例子吗?

更多关于Golang数据结构库测试的可用性建议的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Bartłomiej Klimczak:

可以举个例子吗?

我不确定是否理解了你的问题:我有一个 Graph 接口,以及它的多种实现(无向图、树、邻接矩阵等)。这个接口有 12 个方法(String()、Find()、Add() 等)。

目前我的单元测试对于所有实现都有(几乎)相同的逻辑。这意味着相同的测试代码有多个副本(每个包都有一个测试文件,其中的测试非常相似)。

我正在构思一种方法,即拥有一个包含测试的文件(该文件接收一个接口对象作为输入),然后每个测试文件(每个包一个测试文件)都调用这个公共测试文件中的函数。对于如何优雅地实现这一点,有什么建议吗?

你好,@john_stuart,我最近也在使用这种设计,觉得它非常有趣。

如果你有一个接收接口的测试文件,在不知道具体类型的情况下,你如何断言其行为?如果不同类型没有产生不同的行为,那它们之间又有什么区别呢?


对我来说,单元测试需要测试与其关联的单元。由于接口是隐式实现的,你的 Tree 类型可能会无意中实现任意数量的接口。Tree 的行为只对它的使用者重要,因此必须针对其使用者来测试它的公共 API(如 StringFind 等)。

这可能会导致你实现的其他图类型之间存在一些相似的代码,但为了保持松耦合,它们之间应该互不关心。举例来说,邻接矩阵需要关心 Tree 上的 Add 方法是如何实现的吗?


或许你可以通过尽可能继承通用行为来减少重复代码。例如,一个 DirectedGraph 类型可以像这样嵌入 Graph 类型:

type DirectedGraph struct {
   Graph
   backCheck T  // 用于防止向后遍历的某些属性
}

如果 DirectedGraphStringFind 等方法与 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 接口来测试不同的读取器实现。

这种方法的优点包括:

  1. 确保所有实现都通过相同的测试套件
  2. 减少重复测试代码
  3. 易于添加新实现的测试
  4. 测试逻辑集中在一个地方,便于维护

在 Go 社区中,这种模式通常被称为"表格驱动测试的接口变体"或"可重用的接口测试"。它不是一种正式的设计模式,但被认为是 Go 测试的最佳实践之一。

回到顶部