参数化测试是用来测试不同条件下的相同代码。人们可以设置一个测试方法,从数据源检索数据。这个数据源可以是一个对象的集合,外部文件,甚至可以是一个数据库。一般的想法是,用同一个测试方法来测试不同的条件,以避免重复,使代码更容易阅读和维护。
Jest 有一个内置的支持测试参数化的数据表,它可以由一个数组提供或作为标记的模板字面。
内容表
代码
让我们考虑一个简单的Calculator fn,它接受一个运算符和数字数组:
type Operator = '+' | '-' | '*' | '/';
export default function calculator(operator: Operator, inputs: number[]) {
if (inputs.length < 2) {
throw new Error(`inputs should have length >= 2`);
}
switch (operator) {
case '+':
return inputs.reduce((prev, curr) => prev + curr);
case '-':
return inputs.reduce((prev, curr) => prev - curr);
case '*':
return inputs.reduce((prev, curr) => prev * curr);
case '/':
return inputs.reduce((prev, curr) => prev / curr);
default:
throw new Error(`Unknown operator ${operator}`);
}
}
该Calculator ,可以使用以下场景进行测试:
import calculator from './calculator';
describe('Calculator', () => {
it('throws error when input.length < 2', () => {
expect(() => calculator('+', [0])).toThrow('inputs should have length >= 2');
});
it('throws error when unsupported operator was used', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(() => calculator('&', [0, 0])).toThrow('unknown operator &');
});
it('adds 2 or more numbers incl. `NaN` and `Infinity`', () => {
expect(calculator('+', [1, 41])).toEqual(42);
expect(calculator('+', [1, 2, 39])).toEqual(42);
expect(calculator('+', [1, 2, NaN])).toEqual(NaN);
expect(calculator('+', [1, 2, Infinity])).toEqual(Infinity);
});
it('subtracts 2 or more numbers incl. `NaN` and `Infinity`', () => {
expect(calculator('-', [43, 1])).toEqual(42);
expect(calculator('-', [44, 1, 1])).toEqual(42);
expect(calculator('-', [1, 2, NaN])).toEqual(NaN);
expect(calculator('-', [1, 2, Infinity])).toEqual(-Infinity);
});
it('multiplies 2 or more numbers incl. `NaN` and `Infinity`', () => {
expect(calculator('*', [21, 2])).toEqual(42);
expect(calculator('*', [3, 7, 2])).toEqual(42);
expect(calculator('*', [42, NaN])).toEqual(NaN);
expect(calculator('*', [42, Infinity])).toEqual(Infinity);
});
it('divides 2 or more numbers incl. `NaN` and `Infinity`', () => {
expect(calculator('/', [84, 2])).toEqual(42);
expect(calculator('/', [42, 0])).toEqual(Infinity);
expect(calculator('/', [42, NaN])).toEqual(NaN);
expect(calculator('/', [168, 2, 2])).toEqual(42);
});
});
最重要的场景集中在Calculator 的主要功能*(加*、减、乘、除),每个功能都用不同的数据值集进行测试。这些测试可以被参数化,因为它们是用不同的数据重复相同的测试逻辑。
参数化(数据驱动的)测试在jest
在Jest 中,参数化测试可以用.each 创建,它带有 API:.each(table)(name, fn) 和.each`table`(name, fn) ,区别在于如何提供测试数据。
test.each(table)(name, fn)
在这个例子中,数据是以数组的形式提供的,每个行的参数都被注入到测试函数中。独特的测试名称是通过定位注入参数而创建的:
import calculator from './calculator';
describe('Calculator', () => {
it.each([
[[1, 41], 42],
[[1, 2, 39], 42],
[[1, 2, NaN], NaN],
[[1, 2, Infinity], Infinity],
])('adds %p expecting %p', (numbers: number[], result: number) => {
expect(calculator('+', numbers)).toEqual(result);
});
it.each([
[[43, 1], 42],
[[44, 1, 1], 42],
[[1, 2, NaN], NaN],
[[1, 2, Infinity], -Infinity],
])('subtracts %p expecting %p', (numbers: number[], result: number) => {
expect(calculator('-', numbers)).toEqual(result);
});
it.each([
[[21, 2], 42],
[[3, 7, 2], 42],
[[42, NaN], NaN],
[[42, Infinity], Infinity],
])('multiplies %p expecting %p', (numbers: number[], result: number) => {
expect(calculator('*', numbers)).toEqual(result);
});
it.each([
[[84, 2], 42],
[[168, 2, 2], 42],
[[168, 2, 2], 42],
[[42, 0], Infinity],
[[42, NaN], NaN],
])('divides %p expecting %p', (numbers: number[], result: number) => {
expect(calculator('/', numbers)).toEqual(result);
});
});
请注意,在一个参数化的测试中,每个数据表行都会创建一个新的测试,其寿命与用测试云创建的常规测试完全相同。对于这个例子,有16个测试(4个测试,每个有4组数据值):
PASS src/parameterized/calculatorParameterized1.test.ts
Calculator
✓ adds [1, 41] expecting 42 (2 ms)
✓ adds [1, 2, 39] expecting 42
✓ adds [1, 2, NaN] expecting NaN
✓ adds [1, 2, Infinity] expecting Infinity
✓ subtracts [43, 1] expecting 42
✓ subtracts [44, 1, 1] expecting 42
✓ subtracts [1, 2, NaN] expecting NaN
✓ subtracts [1, 2, Infinity] expecting -Infinity
✓ multiplies [21, 2] expecting 42 (1 ms)
✓ multiplies [3, 7, 2] expecting 42 (1 ms)
✓ multiplies [42, NaN] expecting NaN
✓ multiplies [42, Infinity] expecting Infinity (1 ms)
✓ divides [84, 2] expecting 42
✓ divides [168, 2, 2] expecting 42
✓ divides [168, 2, 2] expecting 42
✓ divides [42, 0] expecting Infinity
✓ divides [42, NaN] expecting NaN (1 ms)
Test Suites: 1 passed, 1 total
Tests: 17 passed, 17 total
Snapshots: 0 total
Time: 2.361 s, estimated 3 s
Ran all test suites matching /src\/parameterized\/calculatorParameterized1.test.ts/i.
✨ Done in 3.55s.
在失败的情况下,你可能期望只有失败的测试被报告,就像下面的例子:
FAIL src/parameterized/calculatorParameterized1.test.ts
Calculator
✓ adds [1, 41] expecting 42 (1 ms)
✓ adds [1, 2, 39] expecting 42 (3 ms)
✕ adds [1, 2, NaN] expecting Infinity (1 ms)
✓ adds [1, 2, Infinity] expecting Infinity
✓ subtracts [43, 1] expecting 42 (1 ms)
✓ subtracts [44, 1, 1] expecting 42
✓ subtracts [1, 2, NaN] expecting NaN (1 ms)
✓ subtracts [1, 2, Infinity] expecting -Infinity
✓ multiplies [21, 2] expecting 42
✓ multiplies [3, 7, 2] expecting 42
✓ multiplies [42, NaN] expecting NaN
✓ multiplies [42, Infinity] expecting Infinity
✓ divides [84, 2] expecting 42 (1 ms)
✓ divides [168, 2, 2] expecting 42
✓ divides [168, 2, 2] expecting 42
✓ divides [42, 0] expecting Infinity
✕ divides [42, NaN] expecting Infinity (1 ms)
● Calculator › adds [1, 2, NaN] expecting Infinity
expect(received).toEqual(expected) // deep equality
Expected: Infinity
Received: NaN
8 | [[1, 2, Infinity], Infinity],
9 | ])('adds %p expecting %p', (numbers: number[], result: number) => {
> 10 | expect(calculator('+', numbers)).toEqual(result);
| ^
11 | });
12 |
13 | it.each([
at src/parameterized/calculatorParameterized1.test.ts:10:42
● Calculator › divides [42, NaN] expecting Infinity
expect(received).toEqual(expected) // deep equality
Expected: Infinity
Received: NaN
36 | [[42, NaN], Infinity],
37 | ])('divides %p expecting %p', (numbers: number[], result: number) => {
> 38 | expect(calculator('/', numbers)).toEqual(result);
| ^
39 | });
40 | });
41 |
at src/parameterized/calculatorParameterized1.test.ts:38:42
Test Suites: 1 failed, 1 total
Tests: 2 failed, 15 passed, 17 total
Snapshots: 0 total
Time: 2.493 s, estimated 3 s
test.each`table`(name, fn)
在这个例子中,数据是用模板字面提供的,其中第一行代表变量的名称,随后几行提供测试数据对象,注入到每一行的测试函数中。独特的测试名称是通过注入参数的名称来创建的:
import calculator from './calculator';
describe('Calculator', () => {
it.each`
numbers | result
${[1, 41]} | ${42}
${[1, 2, 39]} | ${42}
${[1, 2, NaN]} | ${NaN}
${[1, 2, Infinity]} | ${Infinity}
`('adds $numbers expecting $result', ({ numbers, result }) => {
expect(calculator('+', numbers)).toEqual(result);
});
it.each`
numbers | result
${[43, 1]} | ${42}
${[44, 1, 1]} | ${42}
${[1, 2, NaN]} | ${NaN}
${[1, 2, Infinity]} | ${-Infinity}
`('subtracts $numbers expecting $result', ({ numbers, result }) => {
expect(calculator('-', numbers)).toEqual(result);
});
it.each`
numbers | result
${[21, 2]} | ${42}
${[3, 7, 2]} | ${42}
${[42, NaN]} | ${NaN}
${[42, Infinity]} | ${Infinity}
`('multiples $numbers expecting $result', ({ numbers, result }) => {
expect(calculator('*', numbers)).toEqual(result);
});
it.each`
numbers | result
${[84, 2]} | ${42}
${[168, 2, 2]} | ${42}
${[42, 0]} | ${Infinity}
${[42, NaN]} | ${NaN}
`('divides $numbers expecting $result', ({ numbers, result }) => {
expect(calculator('/', numbers)).toEqual(result);
});
});
在这个例子中,也有16个测试被创建:
PASS src/parameterized/calculatorParameterized2.test.ts
Calculator
✓ adds [1, 41] expecting 42 (1 ms)
✓ adds [1, 2, 39] expecting 42
✓ adds [1, 2, NaN] expecting NaN (1 ms)
✓ adds [1, 2, Infinity] expecting Infinity
✓ subtracts [43, 1] expecting 42 (1 ms)
✓ subtracts [44, 1, 1] expecting 42
✓ subtracts [1, 2, NaN] expecting NaN
✓ subtracts [1, 2, Infinity] expecting -Infinity
✓ multiples [21, 2] expecting 42
✓ multiples [3, 7, 2] expecting 42 (1 ms)
✓ multiples [42, NaN] expecting NaN (1 ms)
✓ multiples [42, Infinity] expecting Infinity
✓ divides [84, 2] expecting 42 (1 ms)
✓ divides [168, 2, 2] expecting 42
✓ divides [42, 0] expecting Infinity
✓ divides [42, NaN] expecting NaN
Test Suites: 1 passed, 1 total
Tests: 16 passed, 16 total
Snapshots: 0 total
Time: 2.432 s, estimated 3 s
Ran all test suites matching /src\/parameterized\/calculatorParameterized2.test.ts/i.
✨ Done in 3.36s.
的终极参数化测试Calculator
前面的例子可以通过增加一个额外的测试参数来进一步改进:operator ,最终减少了代码的重复性:
import calculator from './calculator';
describe('Calculator', () => {
it.each`
numbers | operator | result
${[1, 41]} | ${"+"} | ${42}
${[1, 2, 39]} | ${"+"} | ${42}
${[1, 2, NaN]} | ${"+"} | ${NaN}
${[1, 2, Infinity]} | ${"+"} | ${Infinity}
${[43, 1]} | ${"-"} | ${42}
${[44, 1, 1]} | ${"-"} | ${42}
${[1, 2, NaN]} | ${"-"} | ${NaN}
${[1, 2, Infinity]} | ${"-"} | ${-Infinity}
${[21, 2]} | ${"*"} | ${42}
${[3, 7, 2]} | ${"*"} | ${42}
${[42, NaN]} | ${"*"} | ${NaN}
${[42, Infinity]} | ${"*"} | ${Infinity}
${[84, 2]} | ${"/"} | ${42}
${[168, 2, 2]} | ${"/"} | ${42}
${[42, 0]} | ${"/"} | ${Infinity}
${[42, NaN]} | ${"/"} | ${NaN}
`('verifies "$operator" on $numbers expecting $result', ({ numbers, operator, result }) => {
expect(calculator(operator, numbers)).toEqual(result);
});
});
和测试的运行:
PASS src/parameterized/calculatorParameterized3.test.ts
Calculator
✓ verifies "+" on [1, 41] expecting 42
✓ verifies "+" on [1, 2, 39] expecting 42
✓ verifies "+" on [1, 2, NaN] expecting NaN
✓ verifies "+" on [1, 2, Infinity] expecting Infinity
✓ verifies "-" on [43, 1] expecting 42
✓ verifies "-" on [44, 1, 1] expecting 42
✓ verifies "-" on [1, 2, NaN] expecting NaN
✓ verifies "-" on [1, 2, Infinity] expecting -Infinity
✓ verifies "*" on [21, 2] expecting 42
✓ verifies "*" on [3, 7, 2] expecting 42
✓ verifies "*" on [42, NaN] expecting NaN
✓ verifies "*" on [42, Infinity] expecting Infinity
✓ verifies "/" on [84, 2] expecting 42
✓ verifies "/" on [168, 2, 2] expecting 42
✓ verifies "/" on [42, 0] expecting Infinity
✓ verifies "/" on [42, NaN] expecting NaN
Test Suites: 1 passed, 1 total
Tests: 16 passed, 16 total
Snapshots: 0 total
Time: 2.463 s, estimated 3 s
✨ Done in 3.66s.
在回顾
- 当你为不同的测试数据重复测试逻辑时,使用参数化测试。
- 不要过度使用参数化测试,特别是在较慢的测试中,如集成或e2e。
- 生成独特的测试名称,以获得更好的错误信息和更容易调试失败的测试。
- 记住,每个数据行都创建一个新的测试,有一个默认的测试生命周期。