Logo
Section 1 / 5

为什么使用基于属性的测试?

请查看基于属性的测试及其相较于传统测试技术的优势。

导入

为什么使用基于属性的测试?

请查看基于属性的测试及其相较于传统测试技术的优势。

测试

测试可能会显得相当枯燥,但这是我们必须面对的必要环节。测试对于程序的安全性和长期稳定性至关重要,尤其是那些会随时间不断变化的程序。它们还能帮助我们更好地设计程序,并以用户和实现者两种视角来编写代码。

来看一个示例测试,它验证 Erlang 函数能否接收一组已排序的列表,并始终返回合并后的一个单一有序列表:

1merge_test() -> 2 [] = merge([]), 3 [] = merge([[]]), 4 [] = merge([[],[]]), 5 [] = merge([[],[],[]]), 6 [1] = merge([[1]]), 7 [1,1,2,2] = merge([[1,2],[1,2]]), 8 [1] = merge([[1],[],[]]), 9 [1] = merge([[],[1],[]]), 10 [1] = merge([[],[],[1]]), 11 [1,2] = merge([[1],[2],[]]), 12 [1,2] = merge([[1],[],[2]]), 13 [1,2] = merge([[],[1],[2]]), 14 [1,2,3,4,5,6] = merge([[1,2],[],[5,6],[],[3,4],[]]), 15 [1,2,3,4] = merge([[4],[3],[2],[1]]), 16 [1,2,3,4,5] = merge([[1],[2],[3],[4],[5]]), 17 [1,2,3,4,5,6] = merge([[1],[2],[3],[4],[5],[6]]), 18 [1,2,3,4,5,6,7,8,9] = merge([[1],[2],[3],[4],[5],[6],[7],[8],[9]]), 19 Seq = seq(1,100), 20 true = Seq == merge(map(fun(E) -> [E] end, Seq)), 21 ok.

这段代码稍作修改后来自 Erlang/OTP 中 lists 模块的测试套件,该模块是整个语言中最核心的库之一。开发者试图设想所有可能的使用方式,确保结果始终可预测。我们或许还能再添加十几行甚至几十行测试用例,依然能有效覆盖代码的不同场景。尽管如此,这段测试代码仍然完全合理、可用、清晰且高效。问题在于它过于重复,机器完全可以替代人工完成这些工作。事实上,这正是传统测试令人感到无聊的原因:它们是一组精心安排的指令,告诉机器每次运行哪些测试,毫无变化,仅作为安全检查。

手动测试这类代码非常重复,容易引入人为错误。让我们学习如何编写属性测试,让其自动逐行帮我们完成验证工作。

属性测试

近年来,属性测试已成为软件开发领域最令人兴奋的实践之一。它承诺提供比几乎所有其他工具都更优秀、更可靠的测试,而且只需极少的代码量。这意味着我们使用属性测试开发的软件质量也在持续提升。尽管入门门槛较高,但属性测试提供了一种自动化机制,确保软件在开发与维护过程中始终保持高质量。以下是一个等效的属性测试示例:

1sorted_list(N) -> ?LET(L, list(N), sort(L)). 2 3prop_merge() -> 4 ?FORALL(List, list(sorted_list(pos_integer())), 5 merge(List) == sort(append(List))).

这个测试不仅只有四行代码,更加简洁,而且覆盖的场景远超传统测试。实际上,它可以覆盖数十万种情况。目前,这个属性测试看起来可能像一堆无法执行的乱码,至少在没有 PropEr 框架的情况下确实如此。但随着时间推移,我们会发现它其实很容易理解,而且编写起来比传统测试更快。

在本章中,我们将了解属性测试应带来的效果,深入探讨其背后的原理以及这些原理如何影响我们的测试编写方式。同时,我们也会选择合适的工具来开始实践,因为属性测试需要依赖特定框架才能发挥作用。

属性测试的潜力

属性测试不同于传统测试,需要不同的技能。优秀的属性测试是一种需要学习和不断练习的技能,就像演奏乐器或使用画笔一样。我们永远有提升空间,总能找到创新的方法改进测试策略。精通属性测试的人能够对代码做出令人惊叹的事情。

即使是初学者,也能从属性测试中获益良多。我们可以写出简单、简短而精炼的测试,自动以最彻底的方式遍历代码。即使我们在不修改测试的前提下修改程序,代码覆盖率也会上升。我们甚至可以利用这些测试主动发现新的边界情况,而无需额外改动。

随着经验积累,我们还能编写出直接针对状态化系统的直观集成测试,找出那些连最资深的工程师都难以想到的复杂、隐蔽的缺陷。属性测试还教会我们关注哪些潜在问题,帮助我们挖掘出隐藏的 bug。

示例 1:Project FIFO

总体而言,我们会发现属性测试不仅仅是用工具自动化枯燥任务,它本身代表了一种全新的测试与软件设计思维方式。例如,Thomas Arts 在 2016 年 Erlang Factory 大会上的演讲中提到,他们使用QuickCheck——这一经典的属性测试工具,对开源云项目 Project FIFO 进行测试。仅用 460 行属性测试代码,就覆盖了 6 万行生产代码,并发现了 25 个重要缺陷,包括:

  • 时间相关错误
  • 竞态条件
  • 类型错误
  • 库 API 的误用
  • 文档错误
  • 程序逻辑错误
  • 系统限制问题
  • 故障处理错误
  • 一个硬件错误
...
属性测试示例

考虑到一些研究估计开发者平均每千行代码存在六个软件缺陷,仅用 460 行测试就发现 25 个重要缺陷,这无疑是一项非凡成就。相当于每千行测试代码覆盖了超过 50 个缺陷,而每行测试平均覆盖了 140 行生产代码。

示例 2:Google 的 levelDB

再来看一个更高级的应用案例。Joseph Wayne Norton 使用不到 600 行的 QuickCheck 测试套件,在 Google 的 levelDB 中发现了特定的十七次和三十一次调用序列,这些序列会导致数据库出现“幽灵键”(ghost keys)的损坏。无论一个人多么专注,要手动构造出恰好能导致数据库损坏的三十一项调用序列都极其困难。而这一切,仅通过极少量的代码就成功识别出了大量非平凡的错误,而这些软件此前已经过充分测试并正在生产环境中运行。

属性测试的强大之处使其在多个行业中扎根落地,包括:

  • 关键任务通信组件
  • 数据库系统
  • 云服务提供商的路由模块
  • 证书管理层
  • 物联网平台
  • 汽车系统

请记住,属性测试并非仅限于高级程序员。虽然持续提升需要投入努力,但其带来的收益显然值得付出。请始终记得:每一次遇到的障碍,都是改进的机会。我们一步步前行,终将抵达目标。

属性