译 | Prefer table driven tests(一)

简介: 译 | Prefer table driven tests

我是测试的忠实粉丝,特别是单元测试和TDD(当然前提是, 恰当的做好 )。 围绕Go项目的一种实践是 table driven test 方法。 这篇文章探讨了编写 table driven test 的方式和原因。

假设我们有一个分割字符串的函数:

// Split slices s into all substrings separated by sep and  
// returns a slice of the substrings between those separators.  
func Split(s, sep string) []string {  
    var result []string  
    i := strings.Index(s, sep)  
    for i > -1 {  
        result = append(result, s[:i])  
        s = s[i+len(sep):]  
        i = strings.Index(s, sep)  
    }  
    return append(result, s)  
}

在Go中,单元测试只是常规的Go函数(有一些规则),所以我们在同一目录的文件中,使用相同的包名 strings,开始为这个函数编写一个单元测试。

package split  
import (  
    "reflect"  
    "testing"  
)  
func TestSplit(t *testing.T) {  
    got := Split("a/b/c", "/")  
    want := []string{"a", "b", "c"}  
    if !reflect.DeepEqual(want, got) {  
         t.Fatalf("expected: %v, got: %v", want, got)  
    }  
}

测试只是常规的有一些规则的Go函数:

  1. 测试函数的名称必须以Test开头。
  2. 测试函数必须采用*testing.T 类型的一个参数。 *testing.T 是测试包本身注入的类型,用于提供打印,跳过和失败测试的方法。

在我们的测试中,我们使用一些输入调用 Split,然后将其与我们预期的结果进行比较。

Code coverage

接下来的问题是,这个包的覆盖范围是什么? 幸运的是,go tool 具有内置的分支覆盖。 我们可以像这样调用它:

% go test -coverprofile=c.out 
PASS  
coverage: 100.0% of statements  
ok      split   0.010s

结果表明,代码有100%的分支覆盖率,这并不奇怪,这段代码中只有一个分支。

如果我们想深入了解覆盖率报告,那么 go tool 有几个选项来打印覆盖率报告。 我们可以使用 go tool cover -func 来细分每个函数的覆盖率:

% **go tool cover -func=c.out**  
split/split.go:8:       Split          100.0%  
total:                  (statements)   100.0%

如果在该软件包中只有一个功能,并不足令人兴奋,但我相信你会发现更多令人兴奋的软件包来测试。

Spray some .bashrc on that

这两个命令对我来说非常有用,因此我有一个shell alias,它可以一个命令运行测试覆盖率并得到报告:

cover () {  
    local t=$(mktemp -t cover)  
    go test $COVERFLAGS -coverprofile=$t $@ \  
        && go tool cover -func=$t \  
        && unlink $t  
}

Going beyond 100% coverage

我们编写了一个测试用例,获得了100%的覆盖率,但这并不是故事的结尾。 我们有很好的分支覆盖,但我们可能需要测试一些边界条件。 例如,如果我们尝试将使用逗号分割字符串会发生什么?

func TestSplitWrongSep(t *testing.T) {  
    got := Split("a/b/c", ",")  
    want := []string{"a/b/c"}  
    if !reflect.DeepEqual(want, got) {  
        t.Fatalf("expected: %v, got: %v", want, got)  
    }  
}

抑或,如果源字符串中没有分隔符会发生什么?

func TestSplitNoSep(t *testing.T) {  
    got := Split("abc", "/")  
    want := []string{"abc"}  
    if !reflect.DeepEqual(want, got) {  
        t.Fatalf("expected: %v, got: %v", want, got)  
    }  
}

我们开始构建一组运行边界条件的测试用例。 这相当不错。

Introducing table driven tests

然而,我们的测试中有很多重复。 对于每个测试用例,只有输入,预期输出和测试用例的名称发生变化。 其他一切都是样板。 我们想要设置所有的输入和预期输出,感受它们在单个测试套件的效果。 这是引入 table driven test 的好时机。

func TestSplit(t *testing.T) {  
    type test struct {  
        input string  
        sep   string  
        want  []string  
    }  
    tests := []test{  
        {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},  
        {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},  
        {input: "abc", sep: "/", want: []string{"abc"}},  
    }  
    for _, tc := range tests {  
        got := Split(tc.input, tc.sep)  
        if !reflect.DeepEqual(tc.want, got) {  
            t.Fatalf("expected: %v, got: %v", tc.want, got)  
        }  
    }  
}

我们声明了一个结构来保存我们的测试输入和预期输出。 这是我们的表。tests 结构通常是局部声明,因为我们希望将此名称重用于此包中的其他测试。

实际上,我们甚至不需要给类型命名,我们可以使用匿名结构字面值来减少样板文件,如下所示:

func TestSplit(t *testing.T) {  
    tests := []struct {  
        input string  
        sep   string  
        want  []string  
    }{  
        {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},  
        {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},  
        {input: "abc", sep: "/", want: []string{"abc"}},  
    }   
    for _, tc := range tests {  
        got := Split(tc.input, tc.sep)  
        if !reflect.DeepEqual(tc.want, got) {  
            t.Fatalf("expected: %v, got: %v", tc.want, got)  
        }  
    }  
}

现在,添加一个新的测试是直截了当的事情; 只需在 tests 结构中添加另一行。 例如,如果我们的输入字符串有一个尾随分隔符会发生什么?

{input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},  
{input: "a/b/c", sep: ",", want: []string{"a/b/c"}},  
{input: "abc", sep: "/", want: []string{"abc"}},  
**{input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, // trailing sep**

但是,当我们运行 go test,我们得到了

% go test
--- FAIL: TestSplit (0.00s)  
    split_test.go:24: expected: [a b c], got: [a b c ]

抛开测试失败,有一些问题需要讨论。

第一种,将每个测试从函数重写到表中的一行,我们已经丢失了失败测试的名称。 我们在测试文件中添加了一个注释来强调这种情况,但我们无法在 go test 输出中访问该注释。

有几种方法可以解决这个问题。 你会在Go代码库中看到混合风格的使用,因为table testing的习惯用法随着人们对该类型的不断试验而不断发展。

目录
相关文章
|
5月前
|
人工智能 机器人 测试技术
【CMake报错】报错:Flow control statements are not properly nested.
【CMake报错】报错:Flow control statements are not properly nested.
|
存储 测试技术 Go
译 | Prefer table driven tests(二)
译 | Prefer table driven tests(二)
85 0
|
缓存 测试技术 Go
译 | Prefer table driven tests(三)
译 | Prefer table driven tests(三)
80 0
|
弹性计算 Linux Docker
|
Java 测试技术
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
345 0
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
annotationProcessor‘ dependencies won‘t be recognized as kapt annotation processors. Please change
annotationProcessor‘ dependencies won‘t be recognized as kapt annotation processors. Please change
351 0
How to create ABAP implicit enhancement implementation
How to create ABAP implicit enhancement implementation
132 0
How to create ABAP implicit enhancement implementation
How to deactivate Customer Vendor Integration synchronization
How to deactivate Customer Vendor Integration synchronization
107 0
How to deactivate Customer Vendor Integration synchronization
How does ABAP check table work
Created by Wang, Jerry, last modified on Jan 06, 2017
119 0
How does ABAP check table work