服务端阅读 05月27日 19:58
如何在 Jest 中进行参数化测试?如何使用 test.each 和 describe.each?
为什么需要参数化测试写测试的时候,经常会遇到同一套逻辑需要用不同数据反复验证的情况。比如一个加法函数,你要测正数、负数、零、边界值,如果每组数据都单独写一个 test,代码会变得冗长且难以维护。参数化测试就是为了解决这个问题——把数据和断言逻辑分离,用一份测试代码覆盖多组输入。Jest 提供了 test.each 和 describe.each 两个 API 来实现参数化测试。前者对单条测试用例做参数化,后者对整组测试做参数化,两者搭配可以显著减少重复代码。test.each 的基本用法test.each 接收一个数组,数组中的每个元素代表一组测试数据,Jest 会为每组数据生成一条独立的测试用例。用二维数组传入参数,这是最直接的写法:test.each([ [1, 1, 2], [1, 2, 3], [2, 1, 3],])('adds %i + %i = %i', (a, b, expected) => { expect(add(a, b)).toBe(expected);});注意测试名称中的 %i 是占位符,Jest 会按顺序用数组元素替换它们。常用的占位符有:%s(字符串)、%i(整数)、%d(数字)、%p(pretty-format)、%#(测试索引)。用对象数组提高可读性二维数组的参数顺序容易搞混,特别是参数多的时候。用对象数组可以让每组数据的含义一目了然:test.each([ { a: 1, b: 1, expected: 2 }, { a: 1, b: 2, expected: 3 }, { a: 2, b: 1, expected: 3 },])('$a + $b = $expected', ({ a, b, expected }) => { expect(add(a, b)).toBe(expected);});对象数组的测试名称用 $key 的语法引用对象属性,比位置占位符更清晰。如果某个属性值是对象或数组,用 $key 也能自动展开显示。表格语法的写法Jest 还支持用模板字符串写表格式的参数化数据,可读性更好,特别适合数据量较多的场景:test.each` a | b | expected ${1} | ${1} | ${2} ${1} | ${2} | ${3} ${2} | ${1} | ${3}`('returns $expected when $a is added to $b', ({ a, b, expected }) => { expect(add(a, b)).toBe(expected);});表格语法有几个要点:表头行定义变量名,用 | 分隔;数据行中 JavaScript 表达式必须用 ${} 包裹;字符串值可以不用 ${},直接写即可。这种方式在测试报告里看起来像一张表格,维护和审查都很方便。describe.each 分组参数化当你需要针对不同环境或配置运行一整套测试时,describe.each 就派上用场了。它为每组数据生成一个 describe 块,里面可以包含多条测试:describe.each([ ['node', 'node'], ['jsdom', 'browser'],])('test environment: %s', (env, type) => { test(`runs in ${type} environment`, () => { expect(process.env.NODE_ENV).toBeDefined(); }); test('has correct global scope', () => { if (env === 'jsdom') { expect(window).toBeDefined(); } else { expect(global).toBeDefined(); } });});这个例子中,两组环境配置各自生成一个 describe 块,每个块里有两条测试。describe.each 同样支持对象数组和表格语法,用法和 test.each 一致。参数化测试边界情况和错误处理参数化测试不只是测正常路径,更实用的场景是批量覆盖边界值和异常输入:test.each([ [0, 0, 0], [Number.MAX_SAFE_INTEGER, 1, Number.MAX_SAFE_INTEGER + 1], [Number.MIN_SAFE_INTEGER, -1, Number.MIN_SAFE_INTEGER - 1],])('handles edge cases: %i + %i = %i', (a, b, expected) => { expect(add(a, b)).toBe(expected);});test.each([ [undefined, 'input is required'], [null, 'input is required'], ['', 'input cannot be empty'],])('throws error for invalid input: %p', (input, expectedError) => { expect(() => validate(input)).toThrow(expectedError);});把正常值、边界值、异常值分不同的 test.each 组织,测试报告里失败用例一目了然,比把所有数据塞进一个 each 更容易定位问题。常见踩坑点占位符和参数数量不匹配。测试名称里的 %s、%i 等占位符数量必须和数组元素个数一致,多一个少一个都会报错。如果嫌数占位符麻烦,推荐用对象数组加 $key 的方式。异步测试忘记返回 Promise。参数化测试中的回调函数如果是异步的,和普通测试一样需要返回 Promise 或使用 async/await,这个容易遗漏:test.each([ [1, 2], [3, 4],])('async test for %i and %i', async (a, b) => { const result = await asyncAdd(a, b); expect(result).toBe(a + b);});表格语法中的类型陷阱。表格语法里不加 ${} 的值会被当作字符串处理,所以数字、布尔值、对象必须用 ${} 包裹,否则拿到的是字符串类型的值,断言结果可能不符合预期。实战建议在实际项目中,参数化测试用得好可以大幅提升测试覆盖率和可维护性,但也要注意分寸。一组测试数据建议控制在 10 条以内,超过这个数量就要考虑是否该拆分场景。数据太多时测试报告可读性会下降,调试也不方便。选择哪种语法形式可以按场景来:两三个简单参数用二维数组就够了;参数多或者含义不明显时用对象数组;数据量大、需要表格化展示时用模板字符串语法。test.each 和 describe.each 也可以嵌套使用,外层用 describe.each 按环境或配置分组,内层用 test.each 跑具体数据,这样测试结构既清晰又紧凑。