基于属性的测试

模糊测试是测试应用程序中边缘情况的一大进步。我们可以将其类比为混沌测试,我们在其中测试各种边缘情况,以期检测到错误。然而,我们无法控制随机输入。这导致了两个问题:

  • 我们测试了大量不太可能在我们系统中发生的无关场景。

  • 我们不知道真正重要的场景是否已被我们的模糊测试覆盖。

相反,如果我们有一种更结构化的方法可用,那将是非常好的。

基于属性的测试(Property-Based Testing) 是一种测试技术,它涉及根据对我们的用户旅程和系统行为重要的一组属性或规范来测试程序。这使工程师能够遵循系统化的测试方法,而不是专注于验证输入。

在基于属性的测试中,我们生成满足我们已识别的约束或属性集的随机输入。生成方面确保我们测试的边缘情况空间比传统手动编写的测试更大。对属性的关注确保我们涵盖对我们应用程序重要的边缘情况。同样,这并不能保证没有错误,但它确实确保我们将时间花在测试重要的事情上。

testing/quick 包提供了我们可以利用来实现基于属性的测试的测试辅助功能:

  • quick.Check 函数接收一个具有 bool 返回值的函数,并搜索使输入函数返回 false 的任意值。

  • quick.CheckEqual 函数接收两个函数,并寻找使函数返回不同结果的输入。

  • quick.Generator 接口定义了一个 Generate 方法,自定义类型可以实现该方法。一旦它们满足此接口,我们就可以使用 quick.Value 函数为我们的自定义类型生成随机值。这为我们提供了为任何导出类型生成值的灵活性。

quick 包的 Check 函数还接收一个 *quick.Config 参数,该参数允许我们配置测试运行的最大迭代次数或另一个随机生成器。

这种类型的测试直观且易于实现。回顾我们实现的模糊测试示例,模糊目标中的验证仅断言元素的顺序,而不是值本身。事实上,我们没有意识到,我们编写了第一个基于属性的测试。然而,基于属性测试的真正价值在于它搜索失败函数输入,而不是生成完全随机的值。

我们可以重新实现我们之前实现的模糊测试,考虑到基于属性的测试:

func TestGetSortedValues_ASC(t *testing.T) {
    input := map[int]string{
        99: "B",
        0:  "A",
    }
    isSorted := func(k int, val string) bool {
        input[k] = val
        keys := make([]int, 0, len(input))
        for k := range input {
            keys = append(keys, k)
        }
        sort.Ints(keys)
        sortedValues, err := fr.GetSortedValues(input, fr.ASC)
        if err != nil || sortedValues == nil {
            return false
        }
        for index, v := range sortedValues {
            key := keys[index]
            if input[key] != v {
                return false
            }
        }
        return true
    }
    if err := quick.Check(isSorted, nil); err != nil {
        t.Error(err)
    }
}

测试的结构不同,但它使用了与模糊测试相同的验证:

  1. 测试使用常规单元测试签名,以 Test 前缀开头并接收一个 *testing.T 参数。

  2. 在测试内部,我们声明了一个 isSorted 辅助函数,它接收我们将生成的两个参数,一个用于键,一个用于新映射条目的值。它还返回一个 bool 值,使其适合与 quick.Check 函数一起使用。

  3. 在函数内部,我们将生成的值添加到 input 映射中。然后,我们复制键并对它们进行排序。我们调用 GetSortedValues 函数和我们的 UUT 并获取实际值进行验证。

  4. 如果出现错误或 nil 切片,我们返回 false,停止测试。如果排序后的值不符合预期,我们也返回 false。这将向 quick.Check 函数发出信号,表明发生了错误。

  5. 在测试内部,我们将 isSorted 辅助函数传递给 quick.Check 函数,如果它返回错误,则使测试失败。

在出现错误的情况下,quick.Check 函数将报告导致失败的值。强制测试失败,我们将收到有关导致失败的输入的输出:

$ go test -run TestGetSortedValues_ASC ./chapter10/fragile-revised -v
=== RUN TestGetSortedValues_ASC
sort_test.go:62: #1: failed on input 73546389, "\U000773b8"
--- FAIL: TestGetSortedValues_ASC (0.00s)
FAIL
exit status 1
FAIL github.com/PacktPublishing/Test-Driven-Development-in-Go/chapter10/fragile-revised 0.154s

导致失败的输入值可以由工程师用来调试应用程序并修复失败的原因。

我们在本章中介绍的两种测试技术,模糊测试和基于属性的测试,使我们能够利用值生成并测试系统输入的各种边缘情况。这些测试技术是对本章开头讨论的健壮代码最佳实践的补充,使我们能够确保服务的稳定性和可靠性。