用Jest在JavaScript中进行参数化测试的详细教程

394 阅读2分钟

参数化测试是用来测试不同条件下的相同代码。人们可以设置一个测试方法,从数据源检索数据。这个数据源可以是一个对象的集合,外部文件,甚至可以是一个数据库。一般的想法是,用同一个测试方法来测试不同的条件,以避免重复,使代码更容易阅读和维护。

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
  • 生成独特的测试名称,以获得更好的错误信息和更容易调试失败的测试。
  • 记住,每个数据行都创建一个新的测试,有一个默认的测试生命周期。

也请看