Bootstrap

Go 语言快速入门指南:Go 测试

介绍

编程不易,好的程序员也不敢保证程序无 Bug。因此,软件开发过程的一个重要部分是测试。为我们的代码编写测试是确保质量和提高可靠性的好方法。

  • Go 提供了 testing 包,可以用来为代码编写自动化测试。

  • 命令可以运行这些测试的代码。

单元测试是编写有原则的 Go程序的一个重要部分。测试包提供了我们编写单元测试所需的工具,命令可以运行测试。为了便于演示,这段代码是在包 中,但它可以是任何包。测试代码通常与对应所需要测试的代码放在在同一个包里。

测试代码结构

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 结尾。比如,当前 package 有 一个文件,我们想测试 中的 函数,那么应该新建 作为测试文件。

example/
   |--average.go
   |--average_test.go

工具会帮我们查找以该后缀命名的文件。

一个测试文件应该有的样子:

package average

import "testing" // 导入标准库的“tesing” 包

func TestAverage(t *testing.T) { // 将一个指向 testing.T 值的指针传递给函数
	// 调用 testing.T 上的方法来表示测试失败
  t.Error("wrong anwser")
}

测试文件由普通的 Go 函数组成,但需要遵循一定得约定才能使用 命令:

  • 无需把测试代码与正在测试的代码放在同一个包中,但是如果你想从包中访问未导出的类型或函数,则最好这样做。

  • 测试需要使用 testing 包中的类型,所以需要在每个测试文件中的 块中导入这个包。

  • 测试函数名应该以 Test 开头。(名字的其余部分可以是你想要的任何内容,但它应该以大写字母开头)

  • 测试函数应该接受单个参数:一个指向 值的指针。

  • 可以对 值调用方法()来报告测试失败,但更多的选择是通过 接受一个字符串,可以帮忙解释测试失败的信息。 接受一个带格式化动词的字符串,就像 和 函数一样,可以使用 在测试的失败消息中包括其他信息,例如传递给函数的参数、得到的返回值和期望的值。

翻译一段测试的代码

我们将测试计算整数最小值的简单实现。

按照上文中测试代码的结构:

package main

import (
    "fmt"
    "testing"
)

func IntMin(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func TestIntMinBasic(t *testing.T) {
    ans := IntMin(2, -2)
    if ans != -2 {

        t.Errorf("IntMin(2, -2) = %d; want -2", ans)
    }
}

func TestIntMinTableDriven(t *testing.T) {
    var tests = []struct {
        a, b int
        want int
    }{
        {0, 1, 0},
        {1, 0, 0},
        {2, -2, -2},
        {0, -1, -1},
        {-1, 0, -1},
    }

    for _, tt := range tests {

        testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
        t.Run(testname, func(t *testing.T) {
            ans := IntMin(tt.a, tt.b)
            if ans != tt.want {
                t.Errorf("got %d, want %d", ans, tt.want)
            }
        })
    }
}

func BenchmarkIntMin(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IntMin(1, 2)
    }
}

通过编写一个名称以 Test 开头的函数来创建一个测试。

将报告测试失败但继续执行测试。 将报告测试失败并立即停止测试。

编写测试可能是重复性的,所以使用表格驱动的风格是习惯性的,测试输入和预期输出被列在一个表格中,一个单一的循环走过它们并执行测试逻辑。

可以运行 "子测试",每个表项都有一个。当执行 时,这些子测试会分别显示出来。

以粗略模式运行当前项目中的所有测试.

运行结果:

$ go test -v
=== RUN   TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
=== RUN   TestIntMinTableDriven
=== RUN   TestIntMinTableDriven/0,1
=== RUN   TestIntMinTableDriven/1,0
=== RUN   TestIntMinTableDriven/2,-2
=== RUN   TestIntMinTableDriven/0,-1
=== RUN   TestIntMinTableDriven/-1,0
--- PASS: TestIntMinTableDriven (0.00s)
    --- PASS: TestIntMinTableDriven/0,1 (0.00s)
    --- PASS: TestIntMinTableDriven/1,0 (0.00s)
    --- PASS: TestIntMinTableDriven/2,-2 (0.00s)
    --- PASS: TestIntMinTableDriven/0,-1 (0.00s)
    --- PASS: TestIntMinTableDriven/-1,0 (0.00s)
PASS
ok  	main.go	0.005s

以上代码翻译自:https://gobyexample.com/testing

一个简单的"average"函数测试

比如,我们的计算平均分的代码 可以这样写:

package average

import "testing"

func GetAverage(scores []float64) float64 {
	sum := float64(0)
	for _, value := range scores {
		sum += value
	}
	return sum / float64((len(scores)))
}

func TestAverage(t *testing.T) {

	var v float64
	v = GetAverage([]float64{1, 2, 3, 4, 5})

	if v != 3 {
		t.Error("Expected 3, got ", v)
	}
}

测试结果:

$ go test -v
=== RUN   TestAverage
--- PASS: TestAverage (0.00s)
PASS
ok      main.go/Go_test_demo    0.005s

表驱动开发

上面的 测试只能是单个测试数据,如果我们想要测试多个不同的数据,就可以使用到表驱动开发。

不需要单独维护测试函数,可以构建一个由输入数据和期望的输出构成的表,然后使用单个测试函数检查表中的每一项。

可以使用一个 testpair 的结构体来分别保存值 切片和 结果,如:

type testpair struct {
	values  []float64
	average float64
}

var tests = []testpair{
	{[]float64{1, 2}, 1.5},
	{[]float64{1, 1, 1, 1, 1, 1}, 1},
	{[]float64{-1, 1}, 0},
  {[]float64{1, 1.4}, 1.2},
}

然后循环遍历每个 tests 的值,将这些值传递给 函数:

package average

import "testing"

type testpair struct {
	values  []float64
	average float64
}

var tests = []testpair{
	{[]float64{1, 2}, 1.5},
	{[]float64{1, 1, 1, 1, 1, 1}, 1},
	{[]float64{-1, 1}, 0},
	{[]float64{1, 1.4}, 1.2},
}

func GetAverage(scores []float64) float64 {
	sum := float64(0)
	for _, value := range scores {
		sum += value
	}
	return sum / float64((len(scores)))
}

func TestAverage(t *testing.T) {
	for _, pair := range tests {
		v := GetAverage(pair.values)
		if v != pair.average {
			t.Error(
				"For", pair.values,
				"expected", pair.average,
				"got", v,
			)
		}
	}
}

运行上诉代码,得:

$ go test -v
=== RUN   TestAverage
--- PASS: TestAverage (0.00s)
PASS
ok      main.go/Go_test_demo    0.005s

测试驱动开发

我们经常能听到测试驱动开发(Test-Driven-Development,TDD)的模式:

总结

  • Go 包含 包,可以用来为代码编写自动化测试

  • 命令用来运行这些测试,这条命令只针对于以 结尾的文件。

  • 自动化测试使用一组特定的输入来运行代码,并寻找特定的结果。如果代码输入与期望值匹配,则测试通过,否则测试失败并报错。

  • 测试函数必须接受单个参数:一个指向 值得指针

  • 表驱动测试是处理输入和预期输出的“表”的测试,通过传入一组输入,测试这一组输出,检查代码的输出是否与预期值匹配。这样可以减少代码的重复率。

参考书籍:

  • 《Head First Go》

  • https://gobyexample.com/testing