引言:为什么单元测试无法解决所有问题
\n\n想象一下,您编写了一个数组排序函数。您用十几个单元测试覆盖了它:空数组、单个元素、已排序列表、逆序排列。测试全部通过。您对代码充满信心。但一周后,生产环境崩溃了——问题出在一个包含 1000 个元素、有重复项和负数的数组上。熟悉吗?
\n\n传统的基于示例的 TypeScript 测试 只检查您想到的场景。但如果 Bug 隐藏在您未曾想到的组合中呢?这时,基于属性的测试 登场了——这种方法会自动生成数千个随机输入数据,并检查您代码的不变量。
\n\n在本文中,我们将探讨如何使用 fast-check 库在 TypeScript 中进行基于属性的测试,应该检查哪些属性,以及这如何改变开发方法。
\n\n什么是基于属性的测试?
\n\n与其编写:
\nit('排序 [3, 1, 2] → [1, 2, 3]', () => {\n expect(sort([3, 1, 2])).toEqual([1, 2, 3]);\n});\n\n\n不如描述一个属性——一个对于所有有效输入数据都必须为真的断言:
\nit('排序后的数组不改变元素顺序', () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n const sorted = sort(arr);\n // 属性 1:长度保持不变\n expect(sorted.length).toBe(arr.length);\n // 属性 2:原始数组的每个元素都存在\n // 属性 3:数组按升序排序\n for (let i = 1; i < sorted.length; i++) {\n expect(sorted[i - 1]).toBeLessThanOrEqual(sorted[i]);\n }\n })\n );\n});\n\n\nfast-check 库会自动生成数百个随机数组(包括空数组、包含 NaN、小数、极大值的数组),并在每个数组上运行您的测试。只要有一个测试失败,您就会发现问题所在。
\n\n为什么在 TypeScript 中特别需要它?
\n\nTypeScript 提供了静态类型检查,但它不能保证逻辑的正确性。基于属性的 TypeScript 测试 弥补了“类型匹配”和“代码正确运行”之间的差距。它特别适用于:
\n\n- \n
- 数据转换函数(map、filter、reduce) \n
- 解析器和验证器 \n
- 具有复杂逻辑的算法 \n
- 处理边界值的函数 \n
fast-check 库:快速入门
\n\n在项目中安装 fast-check:
\nnpm install --save-dev fast-check\n\n\n基本语法:
\nimport fc from 'fast-check';\n\ndescribe('reverse 函数的属性', () => {\n it('reverse(reverse(arr)) === arr', () => {\n fc.assert(\n fc.property(fc.array(fc.anything()), (arr) => {\n expect(reverse(reverse(arr))).toEqual(arr);\n })\n );\n });\n});\n\n\n这里 fc.array(fc.anything()) 是一个任意数组的生成器。您可以组合使用生成器:
- \n
fc.integer()— 整数 \n fc.string()— 字符串 \n fc.record({ name: fc.string(), age: fc.integer({ min: 0, max: 120 }) })— 对象 \n fc.oneof(...)— 从多个生成器中随机选择 \n
应该检查哪些属性?5 个实用示例
\n\n1. 不变性(可逆性)
\n任何具有逆运算的操作都应该是可逆的:
\nit('JSON 序列化和反序列化', () => {\n fc.assert(\n fc.property(fc.object(), (obj) => {\n const json = JSON.stringify(obj);\n const parsed = JSON.parse(json);\n expect(parsed).toEqual(obj);\n })\n );\n});\n<