Go提供了test⼯具,可以对形式如 func TestXxx(*testing.T) 格式的函数进⾏⾃动化单元测试,这样的函数我们称之为单元测试函数。

函数名必须以 Test 开头,并且 Xxx 必须⾸字⺟⼤写,⽐如 AddDo 等。⾸字⺟⼩写如 adddo 不被认为是单元测试函数, go test 不会⾃动执⾏。

⼀般 Xxx 我们常常使⽤要单元测试的函数名,或者是要单元测试的结构体( struct )名称。但是也不是绝对的,只是⼤家习惯于这样写。

⼀般单元测试的名称我们还会命名为 TestXxx_Yyy 等形式,加⼀些后缀,⽤来区分⼀些⼦测试或者不同场景的测试,这种命名没有强制规定,但Go官⽅标准库的单元测试名称更倾向使⽤多个单词的⼤驼峰式命名法,如 TestAbcXyzWithIjk ,⽽不是以下划线分隔的⽅式。

所有的单元测试函数都放在以_test.go 结尾的⽂件中,此⽂件放在和要测试的⽂件相同的package中。

这个单元测试的⽂件的package名称可以和要测试的package名称相同,也可以以此package加_test 作为包名。⽐如要测试 package abc , 则单元测试⽂件中的包名可以是 package abc_test ,这样的好处是单元测试函数只能访问要测试的公开的函数、结构和变量,更贴近使⽤这个包的场景。Go标准库中这两种⽅法都有使⽤。

⽐如在包s1下定义了⼀个 greet 函数( hello.go):

package s1

func greet(s string) string {
    return "hello " + s
}

然后我们可以在相同的⽂件夹下创建⼀个 hello_test.go的⽂件,然后实现⼀个单元测试的函数(注意函数名称中的G需要⼤写,否则不会被认为是单元测试的函数):

package s1

import "testing"

func TestGreet(t *testing.T) {
    s := greet("test")

    if s != "hello test" {
        t.Errorf("want 'hello test' but got %s", s)
    }
}

在此⽬录下执⾏ go test -v . ,就可以看到此package下所有的单元测试就会被执⾏了。

事实上, 当你执⾏ go test 执⾏单元测试时,Go会把单元测试编译成⼀个test binary,也就是⽤来单元测试的可执⾏程序。在第四章我们再详细介绍它。

表格驱动型单元测试

Go官⽅还推崇⼀种叫做表格驱动型单元(TableDrivenTests)。

编写好的测试并⾮易事,但在许多情况下,表格驱动的测试可以覆盖很多领域:每个表条⽬都是⼀个完整的测试⽤例,其中包含输⼊和预期结果,有时还包含附加信息,例如测试名称,以使测试输出易于阅读。如果您发现⾃⼰在编写测试时使⽤复制和粘贴,请考虑⼀下重构到表格驱动的测试中或将复制的代码提取到帮助程序函数中是否是更好的选择。

给定⼀个测试⽤例表,实际测试只是循环访问所有表条⽬,并为每个条⽬执⾏必要的测试。测试代码编写⼀次,并在所有表条⽬上摊销,因此编写具有良好错误消息的仔细测试是有意义的。

表格驱动的测试不是⼯具,包或其他任何东⻄,它只是编写更⼲净测试的⼀种⽅式和视⻆。

表格驱动型单元测试并不是⼀种强制性⽅式,不过Go标准库中部分单元测试都是采⽤这种⽅式组织的。

cweill/gotests是⼀个可以为你的函数⾃动⽣成表格驱动型的单元测试的⼯具。事实上常⻅的Go IDE⼯具如vs code、Goland、Vim都集成了,通过⿏标右键菜单就可以⾃动⽣成表格驱动的单元测试框架。

⽐如官⽅标准库中的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 {
        t.Run(tt.in, func(t *testing.T) {
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("got %q, want %q", s, tt.out)
            }
        })
    }
}

它准备了12个条⽬,每个条⽬都有输⼊和期望值。单元测试执⾏每⼀个条⽬,并和期望值进⾏⽐较,不符合的话就是⼀个错误的测试。

你可以动⼿写⼀个例⼦,⽐如⼏个加减乘除的算术函数:

package s1

func Add(a, b int) int {
    return a + b
}

func Sub(a, b int) int {
    return a - b
}

func Mul(a, b int) int {
    return a * b
}

func Div(a, b int) int {
    return a / b
}

如果你使⽤IDE,⼀般常⻅的IDE都会有⾃动⽣成单元测试的能⼒。选择⼀个函数或者多个函数⽣成,并在此基础上修改:

package s1

import (
    "testing"
)

func TestAdd(t *testing.T) {
    type args struct {
        a int
        b int
    }
    tests := []struct {
        name string
        args args
        want int
    }{
        {"普通的两个数相加", args{10, 20}, 30},
        {"一个参数是0", args{10, 0}, 10},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.args.a, tt.args.b); got != tt.want {
                t.Errorf("Add() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestAdd2(t *testing.T) {
    v := Add(1, 2)
    if v != 3 {
        t.Errorf("expect 3 but got %d", v)
    }
}

func TestSub(t *testing.T) {
    type args struct {
        a int
        b int
    }
    tests := []struct {
        name string
        args args // int
        want int  // out
    }{
        {"普通的两个数相减", args{10, 20}, -10},
        {"一个参数是0", args{10, 0}, 10},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Sub(tt.args.a, tt.args.b); got != tt.want {
                t.Errorf("Sub() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestMul(t *testing.T) {
    type args struct {
        a int
        b int
    }
    tests := []struct {
        name string
        args args
        want int
    }{
        {"普通的两个数相乘", args{10, 20}, 200},
        {"一个参数是0", args{10, 0}, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Mul(tt.args.a, tt.args.b); got != tt.want {
                t.Errorf("Mul() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestDiv(t *testing.T) {
    type args struct {
        a int
        b int
    }
    tests := []struct {
        name string
        args args
        want int
    }{
        {"普通的两个数相除", args{10, 20}, 0},
        // {"一个参数是0", args{10, 0}, int(math.NaN())},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Div(tt.args.a, tt.args.b); got != tt.want {
                t.Errorf("Div() = %v, want %v", got, tt.want)
            }
        })
    }

}

我使⽤的是vs code,启⽤了code lens,所以单元测试函数上⾯有按钮 run test|debug test ,直接点击 run test 就可以执⾏这个单元测试了。

golden file

Go标准库还使⽤了另外⼀种⽅式进⾏复杂的测试在⼀些复杂的测试中,你可以把期望的输出写⼊到⼀个测试⽂件中。当测试的时候的时候,单测的输出和这些⽂件进⾏测试。

Go中有⼀个特殊的⽂件夹叫testdata,你可以把golden file放⼊到这个⽂件夹中,这个⽂件夹不会⽤来作为编译Go代码。

⽐如go/gofmt_test.go⽂件中,读取testdata下所有的input⽂件和响应的golden⽂件做⽐较:

// TestRewrite processes testdata/*.input files and compares them to the
// corresponding testdata/*.golden files. The gofmt flags used to process
// a file must be provided via a comment of the form
//
// //gofmt flags
//
// in the processed file within the first 20 lines, if any.
func TestRewrite(t *testing.T) {
    // determine input files
    match, err := filepath.Glob("testdata/*.input")
    if err != nil {
        t.Fatal(err)
    }
    // add larger examples
    match = append(match, "gofmt.go", "gofmt_test.go")
    for _, in := range match {
        name := filepath.Base(in)
        t.Run(name, func(t *testing.T) {
            out := in // for files where input and output are identical
            if strings.HasSuffix(in, ".input") {
                out = in[:len(in)-len(".input")] + ".golden"
            }
            runTest(t, in, out)
            if in != out && !t.Failed() {
                // Check idempotence.
                runTest(t, out, out)
            }
        })
    }
}

关键是如何创建这些golden file。你可以⼿⼯创建这些golden files, 但是你也可以通过单元测试初始化golden files,或者在某次更新golden files,你可以通过参数传递给单元测试要不要更新:

update := flag.Bool("update", false, "update golden files.")
go test -update

⽐如go/gofmt_test.go

var update = flag.Bool("update", false, "update .golden files")
func runTest(t *testing.T, in, out string) {
    ......
    expected, err := os.ReadFile(out)
    if err != nil {
        t.Error(err)
        return
    }
    if got := buf.Bytes(); !bytes.Equal(got, expected) {
        if *update {
            if in != out {
                if err := os.WriteFile(out, got, 0666); err != nil {
                    t.Error(err)
                }
                return
            }
            // in == out: don't accidentally destroy input
            t.Errorf("WARNING: -update did not rewrite input file %s", in)
        }
        t.Errorf("(gofmt %s) != %s (see %s.gofmt)\n%s", in, out, in,
            diff.Diff("expected", expected, "got", got))
        if err := os.WriteFile(in+".gofmt", got, 0666); err != nil {
            t.Error(err)
        }
    }
}

更友好的测试结果显示

当⼀个项⽬下的单元测试⾮常多的时候,测试结果会刷屏,成功的测试和失败的测试混合在结果中,不容易区分,所以有些开发者提供了定制的⼯具,对测试结果进⾏染⾊,让成功的结果和失败的结果更容易区分出来。如果你以前使⽤过XUnit⻛格的测试⼯具,⽐如JUint,它会使⽤红绿颜⾊做区分。

rakyll是⼀位知名的Gopher,她提供了⼀个⼯具rakyll/gotest,可以彩⾊化单元测试的结果。

你可以执⾏ go install github.com/rakyll/gotest`@latest安装这个⼯具。先前我们进⾏单元测试的时候敲⼊go test …,使⽤这个⼯具的时候去掉空格就好了gotest …` 。

作者:admin  创建时间:2024-10-22 01:54
最后编辑:admin  更新时间:2024-10-22 02:05
上一篇:
下一篇: