译 | 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的习惯用法随着人们对该类型的不断试验而不断发展。

目录
相关文章
|
存储 测试技术 Go
译 | Prefer table driven tests(二)
译 | Prefer table driven tests(二)
84 0
This job is stuck, because the project doesn‘t have any runners online assigned to it. Go to Runners
This job is stuck, because the project doesn‘t have any runners online assigned to it. Go to Runners
This job is stuck, because the project doesn‘t have any runners online assigned to it. Go to Runners
|
缓存 测试技术 Go
译 | Prefer table driven tests(三)
译 | Prefer table driven tests(三)
79 0
|
弹性计算 Linux Docker
|
Java 测试技术
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
343 0
出现Error creating bean with name与CONDITIONS EVALUATION REPORT问题
Important table for launchpad Designer and configuration
Important table for launchpad Designer and configuration
126 0
Important table for launchpad Designer and configuration
Action framework BAdI Definition TRIGGER_EXECUTED
Created by Jerry Wang, last modified on May 28, 2014
Action framework BAdI Definition TRIGGER_EXECUTED
How to find implementation of Requirement defined in Pricing Procedure
How to find implementation of Requirement defined in Pricing Procedure
119 0
How to find implementation of Requirement defined in Pricing Procedure
How to create ABAP implicit enhancement implementation
How to create ABAP implicit enhancement implementation
131 0
How to create ABAP implicit enhancement implementation
why our extension project didn't load S3 view and controller
Created by Wang, Jerry, last modified on May 20, 2015
103 0
why our extension project didn't load S3 view and controller