基于属性的测试简介 生成测试的目的是什么?它会取代单元测试吗?它有哪些常见的属性?PBT 在“现实世界”中真的有用吗?PBT 套件中有哪些值得关注的优秀部分?我在哪里可以了解更多信息?

2025-06-07

基于属性的测试简介

生成测试的目的是什么?

它能取代单元测试吗?

有哪些常见属性?

PBT 在“现实世界”中真的有使用吗?

PBT 套件中有哪些值得寻找的精彩部件?

我可以在哪里了解更多信息?

基于属性的测试根据输入对代码的输出做出陈述,并且这些陈述针对许多不同的可能输入进行验证。 - Jessica Kerr (@jessitron

基于属性的测试属于生成测试。与单元测试不同,它不需要提供带有预期输出的具体示例输入。相反,它需要定义代码的属性,并使用生成测试引擎(例如 QuickCheck)创建随机输入,以确保定义的属性正确无误。

生成测试的目的是什么?

一般来说,基于属性的测试只需要几行代码(类似于单元测试),但与单元测试不同的是,它们每次会测试一组不同的输入。因此,您最终可以用大致相同数量的测试代码覆盖更大的领域空间。

基于属性的测试还能促进对被测函数更深入的理解。当然,我们知道加法 2 + 2 = 4,但你怎么能说你仅仅通过一个例子就真正正确地实现了一个加法函数呢?

从业务角度来看,更好地理解代码的功能将有助于更准确地确定代码中满足的要求。

它能取代单元测试吗?

虽然有人认为单元测试可以完全被高质量的基于属性的测试所取代,但单元测试在软件开发周期中仍然占有一席之地。基于示例的测试在 TDD 的早期阶段非常有用。它们可以作为锚点,确保你的开发工作按预期进行。例如:

  • 您可以确保您的正弦函数生成正确的值(sin(0) = 0sin(π/2) = 1等)
  • HTTP POST在没有适当的身份验证标头/cookie 的情况下将路由发送到您的/login路由会导致401响应代码

然而,这些基于示例的测试最终会变成被动测试;它们会成为回归测试套件的一部分,并且不会提供有关功能的任何新信息。然而,基于属性的测试始终是主动测试,因为它们每次运行测试套件时都会生成新数据。这可以帮助根除开发人员忽略的问题。例如,您可能期望任何浮点表示的正弦函数的输出都在集合 [-1,-1] 中,但开发人员可能不会想到要测试 ±∞ 或 NaN。因此,当发现失败案例时,可以将这个意外错误用作未来的单元测试,以验证是否已进行修复。仅凭这一点,基于示例的测试就成为增强基于属性的测试套件的有效实践。

“不要编写测试,而是生成测试!”—— John Hughes,Haskell 原始 QuickCheck 的合著者

有哪些常见属性?

如果将软件函数与数学函数关联起来,那么就可以将一些标准的数学属性应用于某些预期的功能。然而,有些属性在非数学应用中并不那么明显。以下是一些数学属性转换为函数属性的示例(Eric Normand 进行了更新),忽略了性能差异:

  • 联想 –a + (b + c) = (a + b) + c

    • hashmap1.merge(hashmap2.merge(hashmap3)) = (hashmap.merge(hashmap2)).merge(hashmap3)
    • list1.append(list2.append(list3)) = (list1.append(list2).append(list3)
    • Math.max(Math.max(a, b), c) = Math.max(a, Math.max(b, c))
    • (bool1 && bool2) && bool3 = bool1 && (bool2 && bool3)
  • 交换律 –a + b = b + a

    • users.Sort().Filter(x => !x.IsAdmin) = (users.Filter(x => !x.IsAdmin)).Sort()(从 Associative 迁移而来)
    • image.flipX().flipY() = image.flipY().flipX()
    • Math.max(a,b) = Math.max(b,a)
    • bool1 && bool2 = bool2 && bool1
    • average(a,b) = average(b, a)
  • 分配 –a(b + c) = ab + ac

    • title.ToUpper() + author.ToUpper() = (title + author).ToUpper()
  • 幂等 –f(a) = f(f(a))

    • Math.abs(x) = Math.abs(Math.abs(x))
    • hashmap.merge({a:1}) = hashmap.merge({a:1}).merge({a:1})
    • title.Trim() = title.Trim().Trim()
    • list.Sort() = list.Sort().Sort()
  • 身份 -f(a, i) where i is identity value of f = a

    • a + 0 = a
    • a * 1 = a
    • userNames.append([]) = userNames
    • hashmap.merge({}) = hashmap
    • bool1 && true = bool1
  • 零 -f(a, z) where z is zero value of f = z

    • a * 0 = 0
    • intersect(valueSet, emptySet) = emptySet
    • bool1 && false = false

还有一些额外的、常见的测试属性,它们不一定植根于数学,但同样有用:

  • 比尔博测试(又名《去而复返》)
    • list = list.Reverse().Reverse()
    • obj = JSON.parse(JSON.stringify(obj))
  • 没有意外的变化
    • list.Length = list.Sort().Length
  • 难以证明,易于验证
    • 排序:每个元素应大于或等于前一个元素
    • 标记化:连接带有分隔符的标记应等于原始字符串,标记数应等于原始字符串中的分隔符数 - 1

PBT 在“现实世界”中真的有使用吗?

简而言之,是的。基于属性的测试绝对可以用来解决现实世界的问题。

沃尔沃

沃尔沃已采用 QuviQ 的 QuickCheck 来验证第三方组件是否符合通信总线标准。John Hughes 和他的团队分析了 3000 页需求,编写了 2 万行 QuickCheck 测试,并将这些属性应用于超过 100 万行的供应商代码。通过检查根据规范生成的属性,他们发现了 200 个问题,这些问题在供应商生成的测试夹具中被遗漏……其中 100 个问题本身就存在于实际规范中。通过改进所分析的代码(以及定义规范),基于属性的测试挽救了生命。

Clojure

Clojure(JVM 上的 LISP 变体)默认使用不可变数据结构。然而,为了提升性能,可以使用可变数据类型。使用基于属性的测试时,在瞬态(可变)结构和持久(不可变)结构之间进行转换时发现了一个问题,所有基于示例的测试(例如:单元测试)均未发现问题。然而,基于属性的测试能够轻松复现该问题。我们向 Cognitect(Clojure 的开发者/维护者)提供了一个 diff 补丁,并将其整合到 Clojure 1.6 版本中。

个人经历

我还在一个项目中使用了基于属性的测试,该项目通过各种指标(QoS、数据包类型、数据包大小等)来计算网络性能。为了证明我的数据包分类和匹配算法不会产生误匹配,我设置了持续集成服务器,使其生成 1 亿个不同的数据包,并将它们分布在我们感兴趣的不同数据包类型中。到目前为止,我们已经随机生成了超过 100 亿个数据包,并且没有出现过一次误匹配。

PBT 套件中有哪些值得寻找的精彩部件?

收缩

虽然较大的输入可能会产生错误,但一些基于属性的测试套件(大多数 QuickCheck 变体)会尝试将输入序列缩小到能够重现错误的最小值。输入越小,重现和修复错误就越容易。

竞争条件

众所周知,竞争条件很难通过基于示例的测试来设置。一些 PBT 套件(尤其是 QuviQ 的 Erlang 版 QuickCheck)可以并行执行多个操作,以便测试这些操作以任何串行组合执行是否会产生相同的输出。如果并行化版本与串行化版本无法匹配,则会识别竞争条件,并将其缩减为尽可能最小的操作集以重现错误。

自定义生成器

您可能希望限制正在测试的输入域,或者以特定方式构建数据结构。大多数 PBT 套件允许您创建自定义生成器,并且通常也包含许多常见的自定义生成器——例如将字符串限制为可打印字符、限制浮点值等等。
如果您要从一种算法迁移到另一种算法,可以使用将相同输入同时传递给新旧实现的 PBT 测试套件,以确保新实现产生相同的输出。

我可以在哪里了解更多信息?

值得庆幸的是,互联网上有一些关于基于属性的测试的优秀学习资源。以下是我发现的一些非常有用的资源。

文章来源:https://dev.to/jdsteinhauser/intro-to-property-based-testing-2cj8
PREV
微服务通信的不同方面总结
NEXT
认识一下 Animatable,一个使用 WAAPI 作为组件的微型 Web 组件💫奖励:使用 StencilJS 的高阶组件(HOC)