把测试留给 Test 函数

Go 区分了 “测试辅助函数 “和 “断言辅助函数”。

  • 测试辅助函数就是做设置或清理任务的函数。所有发生在测试辅助函数中的故障都被认为是环境的故障(而不是来自被测试的代码)–例如,当一个测试数据库不能被启动,因为在这台机器上没有更多的空闲端口。对于这样的函数,调用t.Helper通常是合适的,将其标记为测试辅助函数。参见 测试辅助函数的错误处理 了解更多细节。
  • 断言辅助函数是检查系统正确性的函数,如果没有达到预期,则测试失败。断言辅助函数在 Go 中不被认为是常见用法

测试的目的是报告被测试代码的通过/失败情况。测试失败的理想场所是在Test函数本身,因为这样可以确保失败信息和测试逻辑是清晰的。

随着你的测试代码的增长,可能有必要将一些功能分解到独立的函数中。标准的软件工程考虑仍然适用,因为测试代码仍然是代码。如果这些功能不与测试框架交互,那么所有的常规规则都适用。然而,当通用代码与框架交互时,必须注意避免常见的陷阱,这些陷阱会导致语焉不详的失败信息和不可维护的测试。

如果许多独立的测试用例需要相同的验证逻辑,请以下列方式之一安排测试,而不是使用断言辅助函数或复杂的验证函数。

  • Test函数中内联逻辑(包括验证和失败),即使它是重复的。这在简单的情况下效果最好。
  • 如果输入是类似的,可以考虑把它们统一到一个表格驱动的测试,同时在循环中保持逻辑的内联。这有助于避免重复,同时在 “测试 “中保持验证和失败。
  • 如果有多个调用者需要相同的验证功能,但表格测试不适合(通常是因为输入不够简单或验证需要作为操作序列的一部分),安排验证功能,使其返回一个值(通常是一个 “错误”),而不是接受一个 “testing.T “参数并使用它来让测试失败。在测试中使用逻辑来决定是否失败,并提供有用的测试失败。你也可以创建测试辅助函数,以剔除常见的模板设置代码。

最后一点中概述的设计保持了正交性。例如,cmp不是为了测试失败而设计的,而是为了比较(和差异)值。因此,它不需要知道进行比较的上下文,因为调用者可以提供这个。如果你的普通测试代码为你的数据类型提供了一个cmp.Transformer,这通常是最简单的设计。对于其他的验证,可以考虑返回一个error值。

// Good:
// polygonCmp returns a cmp.Option that equates s2 geometry objects up to
// some small floating-point error.
func polygonCmp() cmp.Option {
    return cmp.Options{
        cmp.Transformer("polygon", func(p *s2.Polygon) []*s2.Loop { return p.Loops() }),
        cmp.Transformer("loop", func(l *s2.Loop) []s2.Point { return l.Vertices() }),
        cmpopts.EquateApprox(0.00000001, 0),
        cmpopts.EquateEmpty(),
    }
}

func TestFenceposts(t *testing.T) {
    // This is a test for a fictional function, Fenceposts, which draws a fence
    // around some Place object. The details are not important, except that
    // the result is some object that has s2 geometry (github.com/golang/geo/s2)
    got := Fencepost(tomsDiner, 1*meter)
    if diff := cmp.Diff(want, got, polygonCmp()); diff != "" {
        t.Errorf("Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\n%v", diff)
    }
}

func FuzzFencepost(f *testing.F) {
    // Fuzz test (https://go.dev/doc/fuzz) for the same.

    f.Add(tomsDiner, 1*meter)
    f.Add(school, 3*meter)

    f.Fuzz(func(t *testing.T, geo Place, padding Length) {
        got := Fencepost(geo, padding)
        // Simple reference implementation: not used in prod, but easy to
        // reasonable and therefore useful to check against in random tests.
        reference := slowFencepost(geo, padding)

        // In the fuzz test, inputs and outputs can be large so don't
        // bother with printing a diff. cmp.Equal is enough.
        if !cmp.Equal(got, reference, polygonCmp()) {
            t.Errorf("Fencepost returned wrong placement")
        }
    })
}

polygonCmp函数对它的调用方式是不可知的;它不接受具体的输入类型,也不规定在两个对象不匹配的情况下该怎么做。因此,更多的调用者可以使用它。

注意:在测试辅助函数和普通库代码之间有一个类比。库中的代码通常应该不 panic,除非在极少数情况下;从测试中调用的代码不应该停止测试,除非继续进行没有意义