引言:为什么您的测试在撒谎,而您却相信它们
\n\n想象一下:您编写了 50 个单元测试,覆盖了所有 if-else 分支,通过了 CI 流水线——全部通过。一周后,生产环境因错误 "Division by zero" 而崩溃。怎么会?测试明明都通过了!熟悉吗?
经典单元测试(示例性的,带有具体数字)的问题在于,它们只检查您已经知道的内容。您选择那些您认为会导致错误的参数。但程序员的思维是可预测的:我们都会写 testDivideByZero,却忘记了 testDivideByNegativeZero 或 testDivideByVeryLargeFloat。
这时,基于属性的测试在 PHP 中 登场了——这种方法不检查具体的场景,而是针对数千个随机输入数据来证明您代码的属性。今天,我们将探讨如何使用 Eris 和 PHPUnit 将此方法集成到您的 PHP 项目中。您将学习如何发现普通测试遗漏的 bug,并理解这并非魔法,而是工程学。
\n\n什么是基于属性的测试?(以及为什么它不是随机测试)
\n\n一个普通的单元测试看起来像这样:assert(2 + 2 === 4)。您检查一个具体的断言。基于属性的测试 (PBT) 则不同:您阐述一个属性——一个对于任何有效输入数据都必须为真的断言。
加法函数属性的示例:
\n- \n
- 交换律:
a + b === b + a对于任何数字都成立。 \n - 幂等性: 重复应用函数不会改变结果。 \n
- 不变性: 结果始终为正(如果这是属性的话)。 \n
PBT 生成数百个随机值(包括边界值、零值、负值、NaN),并检查属性是否被违反。如果找到反例,测试将失败,并显示导致失败的具体输入数据。您得到的不仅仅是“测试失败”,而是一个可重现的 bug。
\n\n在 PHP 中,由于动态类型,这项技术尤其有用:您永远不知道实际传入函数的类型是什么。
\n\nPHP 中基于属性测试的工具
\n\n主要工具是 Eris 库(github.com/giorgiosironi/eris)。它与 PHPUnit 集成,并提供随机数据生成器。替代方案是 PHPUnit 10+ 的内置功能(但尚不成熟)。我们将使用 Eris,因为它是一个成熟且有文档的工具。
\n\n通过 Composer 安装:
\ncomposer require --dev giorgiosironi/eris\n\n同时,Faker 对于生成复杂结构(电子邮件、姓名、地址)也很有用。
\n\ncomposer require --dev fakerphp/faker\n\n编写第一个基于属性的测试:破坏排序函数
\n\n假设我们有一个使用快速排序的函数 sortArray()。我们编写一个属性:“排序后的数组不应包含比前一个元素更小的元素”。
use Eris\\TestTrait;\nuse Eris\\Generator\\IntegerGenerator;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SortTest extends TestCase\n{\n use TestTrait;\n\n public function testSortedArrayProperty(): void\n {\n $this->forAll(\n Generator\\seq(Generator\\int())\n )->then(function (array $input) {\n $sorted = sortArray($input);\n for ($i = 0; $i < count($sorted) - 1; $i++) {\n $this->assertLessThanOrEqual(\n $sorted[$i + 1],\n $sorted[$i],\n "数组未排序: " . json_encode($sorted)\n );\n }\n });\n }\n}\n\n\n运行