如果测试能自动编写呢?认识基于属性的测试
你是否曾编写过一个单元测试,运行后得到绿色通过标记,但一个月后却在同一个函数中发现了 Bug?很熟悉吧?经典的单元测试就像只盯着钞票的一面,来检查你的钱包是否为空。你只检查一个特定的场景,一组特定的输入数据。但如果我告诉你,有一种方法可以让计算机自行生成数千种不同的场景,并检查你的代码是否会崩溃呢?
这不是幻想。这就是基于属性的 JavaScript 测试(或属性测试)。与其编写“如果传入 2 和 3,结果应为 5”,不如描述你函数的属性:“对于任意两个数字,相加的结果总是一个大于或等于每个加数的数字”。然后测试环境会生成数百对随机数字,来验证这个断言。
在本文中,我们将探讨基于属性的测试如何工作,它与常规测试有何不同,以及如何通过优秀的 fast-check 库将其引入你的项目。你将看到,发现那些你甚至都没想到要去寻找的“边界” Bug 是多么容易。
为什么常规的单元测试并不总能解决问题?
让我们直接切入正题。假设你有一个按升序对数字数组进行排序的函数:
function sortNumbers(arr) { return arr.slice().sort((a, b) => a - b);}你编写了一个单元测试:
test('对数组 [3, 1, 2] 进行排序', () => { expect(sortNumbers([3, 1, 2])).toEqual([1, 2, 3]);});测试通过了。你很高兴。但是,如果数组中有 null 呢?或者 undefined?或者非常大的数字?或者一个长度为 10000 的数组?你不会为这些情况都编写测试。但基于属性的 JavaScript 测试会。
与其列举示例,不如描述那些必须始终为真的不变量(属性):
- 排序后数组的长度等于原始数组的长度。
- 原始数组中的每个元素都存在于排序后的数组中。
- 对于任何索引
i,都有arr[i] <= arr[i+1]成立。
这些正是属性测试将要验证的属性。
fast-check 库:你进行随机测试的得力助手
在 JavaScript 世界中,进行基于属性测试的主要工具是 fast-check。它能够生成任意类型的随机数据:数字、字符串、数组、对象、日期——甚至复杂的数据结构。
安装很简单:
npm install fast-check --save-dev现在,让我们为 sortNumbers 函数编写第一个基于属性的测试。我们使用 Jest 框架,但 fast-check 可以与任何测试运行器配合使用。
const fc = require('fast-check');
test('排序保持数组长度不变', () => { fc.assert( fc.property(fc.array(fc.integer()), (arr) => { const sorted = sortNumbers(arr); return sorted.length === arr.length; }) );});这里发生了什么?fc.array(fc.integer()) 是一个生成器,它创建由随机整数组成的随机数组。fc.property 接收这个生成器,并在 100 个(默认情况下)不同的数据集上验证断言(我们的箭头函数)。如果至少有一个测试失败——fc.assert 将抛出一个错误并显示反例。
添加更多属性
一个属性不错,但三个更好。让我们验证排序后的数组是否确实有序:
test('排序返回有序数组', () =>