基于属性的 JavaScript 测试:捕获你未曾寻找的 Bug

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

如果测试能自动编写呢?认识基于属性的测试



你是否曾编写过一个单元测试,运行后得到绿色通过标记,但一个月后却在同一个函数中发现了 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 测试会。



与其列举示例,不如描述那些必须始终为真的不变量(属性):

  1. 排序后数组的长度等于原始数组的长度。
  2. 原始数组中的每个元素都存在于排序后的数组中。
  3. 对于任何索引 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('排序返回有序数组', () =>                

博客

书籍推荐