在本教程中,我将使用语言设计师和社区开发的最佳实践,教您Go语言惯用测试的所有基础知识。 主要武器将是标准测试包。 目标将是一个示例程序,该程序可以解决来自Euler项目的一个简单问题。
平方和差
求和平方差问题非常简单:“找到前一百个自然数的平方和与平方和之间的差。”
这个特定问题可以相当简洁地解决,尤其是在您了解高斯的情况下。 例如,前N个自然数的总和为(1 + N) * N / 2
,前N个整数的平方和为: (1 + N) * (N * 2 + 1) * N / 6
。 因此,可以通过以下公式并为N分配100来解决整个问题:
(1 + N) * (N * 2 + 1) * N / 6 - ((1 + N) * N / 2) * ((1 + N) * N / 2)
好吧,这是非常具体的,不需要太多测试。 相反,我创建了一些功能,该功能比解决此问题所需的功能要通用一些,但可以在将来用于其他程序(Euler项目目前有559个问题)。
该代码可在GitHub上获得 。
这是四个功能的签名:
// The MakeIntList() function returns an array of consecutive integers
// starting from 1 all the way to the `number` (including the number)
func MakeIntList(number int) []int
// The squareList() function takes a slice of integers and returns an
// array of the quares of these integers
func SquareList(numbers []int) []int
// The sumList() function takes a slice of integers and returns their sum
func SumList(numbers []int) int
// Solve Project Euler #6 - Sum square difference
func Process(number int) int
现在,有了我们的目标程序(请原谅TDD狂热者),让我们看看如何为该程序编写测试。
测试包
测试包与go test
命令并驾齐驱。 您的程序包测试应放在带有“ _test.go”后缀的文件中。 您可以将测试分为几个遵循该约定的文件。 例如:“ whatever1_test.go”和“ whatever2_test.go”。 您应该将测试功能放在这些测试文件中。
每个测试函数都是一个公开导出的函数,其名称以“ Test”开头,接受指向testing.T
对象的指针,但不返回任何内容。 看起来像:
func TestWhatever(t *testing.T) {
// Your test code goes here
}
T对象提供了可用于指示失败或记录错误的各种方法。
请记住: go test
命令只会执行测试文件中定义的测试功能。
写作测试
每个测试遵循相同的流程:设置测试环境(可选),在测试输入下提供代码,捕获结果,并将其与预期输出进行比较。 请注意,输入和结果不必是函数的参数。
如果被测代码正在从数据库中获取数据,则输入将确保数据库包含适当的测试数据(可能涉及不同级别的模拟)。 但是,对于我们的应用程序来说,将输入参数传递给函数并将结果与函数输出进行比较的常见方案就足够了。
让我们从SumList()
函数开始。 此函数采用整数切片并返回其总和。 这是一个测试函数,用于验证SumList()
行为是否SumList()
。
它测试两个测试用例,并且如果预期的输出与结果不匹配,它将调用testing.T对象的Error()
方法。
func TestSumList_NotIdiomatic(t *testing.T) {
// Test []{} -> 0
result := SumList([]int{})
if result != 0 {
t.Error(
"For input: ", []int{},
"expected:", 0,
"got:", result)
}
// Test []{4, 8, 9} -> 21
result = SumList([]int{4, 8, 9})
if result != 21 {
t.Error(
"For input: ", []int{},
"expected:", 0,
"got:", result)
}
}
这很简单,但是看起来有些冗长。 惯用的Go测试使用表驱动测试,您可以在其中为输入对和预期输出对定义一个结构,然后获得这些对的列表,并以循环的方式输入到相同的逻辑。 这是测试SumList()
函数的方法。
type List2IntTestPair struct {
input []int
output int
}
func TestSumList(t *testing.T) {
var tests = []List2IntTestPair{
{[]int{}, 0},
{[]int{1}, 1},
{[]int{1, 2}, 3},
{[]int{12, 13, 25, 7}, 57},
}
for _, pair := range tests {
result := SumList(pair.input)
if result != pair.output {
t.Error(
"For input: ", pair.input,
"expected:", pair.output,
"got:", result)
}
}
}
这样好多了。 添加更多的测试用例很容易。 将所有测试用例放在一个地方很容易,而且,如果您决定更改测试逻辑,则无需更改多个实例。
这是另一个测试SquareList()
函数的示例。 在这种情况下,输入和输出都是整数的切片,因此测试对的结构不同,但是流程相同。 这里有趣的一件事是Go没有提供一种比较切片的内置方法,因此我使用了reflect.DeepEqual()
将输出切片与期望的切片进行比较。
type List2ListTestPair struct {
input []int
output []int
}
func TestSquareList(t *testing.T) {
var tests = []List2ListTestPair{
{[]int{}, []int{}},
{[]int{1}, []int{1}},
{[]int{2}, []int{4}},
{[]int{3, 5, 7}, []int{9, 25, 49}},
}
for _, pair := range tests {
result := SquareList(pair.input)
if !reflect.DeepEqual(result, pair.output) {
t.Error(
"For input: ", pair.input,
"expected:", pair.output,
"got:", result)
}
}
}
运行测试
运行测试就像在包目录中键入go test
一样简单。 Go将找到所有带有“ _test.go”后缀的文件以及所有带有“ Test”前缀的功能,并将它们作为测试运行。 一切正常后,显示以下内容:
(G)/project-euler/6/go > go test
PASS
ok _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s
不太戏剧化。 让我故意打破测试。 我将更改SumList()
的测试用例,以使求和1和2的预期输出为7。
func TestSumList(t *testing.T) {
var tests = []List2IntTestPair{
{[]int{}, 0},
{[]int{1}, 1},
{[]int{1, 2}, 7},
{[]int{12, 13, 25, 7}, 57},
}
for _, pair := range tests {
result := SumList(pair.input)
if result != pair.output {
t.Error(
"For input: ", pair.input,
"expected:", pair.output,
"got:", result)
}
}
}
现在,当您输入go test
,您将获得:
(G)/project-euler/6/go > go test
--- FAIL: TestSumList (0.00s)
006_sum_square_difference_test.go:80: For input: [1 2] expected: 7 got: 3
FAIL
exit status 1
FAIL _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s
这样可以很好地说明发生了什么,应该为您提供解决问题所需的所有信息。 在这种情况下,问题在于测试本身是错误的,期望值应该为3。这是一个重要的教训。 不要自动假定测试失败会导致被测代码损坏。 考虑整个系统,其中包括被测代码,测试本身和测试环境。
测试覆盖率
为了确保您的代码正常工作,仅通过测试是不够的。 另一个重要方面是测试覆盖率。 您的测试是否覆盖了代码中的每个语句? 有时甚至还不够。 例如,如果您的代码中有一个循环一直运行到满足条件,则可以在一个有效的条件下成功测试它,但没有注意到在某些情况下条件可能始终为假,从而导致无限循环。
单元测试
单元测试就像刷牙和使用牙线一样。 你不应该忽略他们。 它们是解决问题的第一个障碍,将使您对重构充满信心。 当尝试重现问题并能够编写失败的测试来演示在解决问题之后通过的问题时,它们也是一个福音。
整合测试
集成测试也是必要的。 认为他们是去看牙医。 暂时没有它们,您可能会没事的,但是如果您忽略它们太久,那将不会很漂亮。
大多数不平凡的程序都由多个相互关联的模块或组件组成。 将这些组件连接在一起时,经常会出现问题。 集成测试使您确信整个系统都按预期运行。 还有许多其他类型的测试,例如验收测试,性能测试,压力/负载测试和成熟的整个系统测试,但是单元测试和集成测试是测试软件的两种基本方法。
结论
Go具有对测试的内置支持,一种定义明确的编写测试的方式以及以表驱动测试的形式建议的准则。
需要为输入和输出的每种组合编写特殊的结构有点烦人,但这就是您为Go的简单设计方法付出的代价。
翻译自: https://code.tutsplus.com/tutorials/lets-go-testing-golang-programs--cms-26499