Golang中的数据驱动和参数化测试

此篇文章转自Elliot Chance的Go: Data-Driven or Parameterized Tests。欢迎大家访问我的博客,代码可以在@Zuozuohao下载。

Glang的核心原则是保持简单和问题解决方法的唯一性,Python也遵循了这一原则。

在Go中可以轻松的完成单元测试,然而依旧会遇到一些测试功能上的问题。至少对于我来说, 与其写令人厌烦的测试样板文件,我宁愿去找一个功能完善的第三方测试包。

这篇文章详细介绍了在书写数据驱动或者说参数化测试框架过程中,数据驱动测试是指基于相同的测试逻辑使用表格作为输入和期望输出源的测试方法。

The Traditional Way

Go的测试方式非常简单,并且该方式应用于整个内建库中,这里是取自fmt包的示例:

var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}

func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        s := Sprintf(tt.in, &flagprinter)
        if s != tt.out {
            t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out)
        }
    }
}

这种方式的优点是:
不需要外部库的支持

这种方式的不足在于:
受CPU限制的串行的测试方式
不支持过滤部分测试用例集合*

Named-tests in Go 1.7+

Go 1.7引入了一个被称作”named tests”的新特性,named tests是指你可以使用嵌套的测试方式,嵌套测试方式支持自定义测试和过滤测试用例集合。下面是示例代码:

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) { ... })
    // <tear-down code>
}

采取这种方式对之前的示例进行调整:

func TestFlagParser(t *testing.T) {
    for _, tt := range flagtests {
        t.Run(tt.in, func(t *testing.T) {
            var flagprinter flagPrinter
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q",
                    tt.in, s, tt.out)
            }
        })
    }
}

这种方式的优点是:
不需要外部库的支持
测试用例并行执行
可以进行测试用例的过滤
这种方式的不足在于:
只支持Go 1.7以上版本
可能存在与现有的测试框架不兼容的情况

Ginko Table Driven Tests

Ginkgo和Gomega是我最喜欢的测试框架。我觉得自己已经深深的喜欢上了这两个测试框架。下面是使用Ginkgo写的上面示例的测试(为了简明起见,一些测试用例被去除)

import (
    . "github.com/onsi/ginkgo/extensions/table"
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

var _ = Describe("Sprintf", func() {
    DescribeTable("FlagParser",
        func(in string, out string) {
            var flagprinter flagPrinter
            s := Sprintf(in, &flagprinter)
            Expect(s).To(Equal(out, "Expected " + in))
        },
        Entry("%a", "%a", "[%a]"),
        Entry("%-a", "%-a", "[%-a]"),
        Entry("%+a", "%+a", "[%+a]"),
    )
})

在这里可以获得详细信息
这种方式的优点是:
不需要外部库的支持
测试用例并行执行
支持过滤测试用例集合
与原生测试其他测死框架兼容性好
这种方式的优点是:
需要外部依赖

Matrix-style Tests

这部分内容跟这篇文章有一定的关系,却是沿着另外一个方向去阐述测试。我所指的是通过Matrix-style Tests,数据驱动测试能够满足多维输入数据的各种测试用例组合。例如我想测试一个输入数据的各种可能性(每种有true和false两种取值),那么测试用例需要2x2=4个。

当你进行3个或者更多维度的测试的时候就会遇到测试用例爆炸的问题。我就曾经遇到过这种问题而不得不进行输入数据的有效性检测。如果测试结构被重构或者新的输入值域被引入,这个时候需要确保所有的输入数据组合都被覆盖。

func verifyMatrixTestsR(t *testing.T, tests [][]interface{},
    values []interface{}, dimentions [][]interface{}) {
    if len(dimentions) >= 1 {
        for _, dimention := range dimentions[0] {
            verifyMatrixTestsR(t, tests, append(values, dimention),
                dimentions[1:])
        }

        return
    }

    found := false
    for _, test := range tests {
        match := true

        for i, value := range values {
            if value != test[i] {
                match = false
                break
            }
        }

        if match {
            found = true
            break
        }
    }

    if !found {
        t.Errorf("Missing test for %v", values)
    }
}

func verifyMatrixTests(t *testing.T, tests [][]interface{},
    dimentions ...[]interface{}) {
    verifyMatrixTestsR(t, tests, []interface{}{}, dimentions)
}

func TestOr(t *testing.T) {
    // Each test is written as:
    // 
    //     { dimension 1, dimension 2, ...dimension N, ...extra values }
    // 
    tests := [][]interface{}{
        { true, false, true }, // eg. true OR false = true
        { true, true, true },
        { false, false, false },
        { false, true, true },
    }

    // verifyMatrixTests validates that we have tests for every combination
    // of the provided dimensions. An takes the form of:
    // 
    //     verifyMatrixTests(t, tests, dimension 1 values, dimension 2 values, ... }
    // 
    verifyMatrixTests(t, tests,
        []interface{}{ true, false }, []interface{}{ true, false })

    // Now we can handle the test data however we want.
    // Unfortunately the interface{} have to be cast. I'm sure there is a
    // neater way to handle this.
    for _, test := range tests {
        if (test[0].(bool) || test[1].(bool)) != test[2].(bool) {
            t.Errorf("%v", test)
        }
    }
}

非常感谢您读完这篇冗长的文章,如有错误之处请指出,我会尽快修改,谢谢!

其他链接
C的面向对象编程

共 0 个回复