jest单元测试

551 阅读3分钟

Jest是一种JavaScript测试框架,用于编写单元测试以确保代码的正确性和可靠性。单元测试是指对代码中的最小单元进行测试,例如函数或方法,以验证其是否按预期工作。同时,编写测试用例可以促使开发人员编写更好的代码,包括更好的代码设计和更好的代码可读性。

1、jest覆盖率指标

  1. line coverage:行覆盖率;
  2. function coverage:函数覆盖率(不仅仅包含文件中自定义的函数,还包含外部引入的函数);
  3. branch:分支覆盖率(if else;|| 、??、三元运算符等都属是);
  4. Statement:语句覆盖率(代码语句,简单的是分号结尾的赋值、return语句,复合语句是if、try、for、switch等)。

2、jest插桩

Jest 一般使用 Istanbul 作为其代码覆盖率工具。覆盖率指标原理是函数插桩(instrument),例如:

greeting.js

function greeting(name = "HZFE") {
  if (DEBUG) {
    name = `${name} (DEBUG)`;
  }
  const foo = DEBUG ? "(DEBUG) foo" : "foo";
  for (const i = 0; i < name.length; i++) {
    name += "!";
  }
  return `Hello ${name}`;
}

经过 nyc instrument 后,大致代码如下:

out.js

function cov_g4lly5ec() {
  // ...
  var coverageData = {
    path: "...",
    statementMap: {
      0: { start: { line: 2, column: 2 }, end: { line: 4, column: 3 } },
      // ...
    },
    fnMap: {
      0: {
        name: "greeting",
        decl: { start: { line: 1, column: 9 }, end: { line: 1, column: 17 } },
        loc: { start: { line: 1, column: 33 }, end: { line: 13, column: 1 } },
        line: 1,
      },
    },
    branchMap: {
      0: {
        loc: { start: { line: 1, column: 18 }, end: { line: 1, column: 31 } },
        type: "default-arg",
        locations: [
          { start: { line: 1, column: 25 }, end: { line: 1, column: 31 } },
        ],
        line: 1,
      },
      // ...
    },
    s: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 },
    f: { 0: 0 },
    b: { 0: [0], 1: [0, 0], 2: [0, 0] },
    // ...
  };
  // ...
  return actualCoverage;
}
cov_g4lly5ec();
function greeting(name = (cov_g4lly5ec().b[0][0]++, "HZFE")) {
  cov_g4lly5ec().f[0]++;
  cov_g4lly5ec().s[0]++;
  if (DEBUG) {
    cov_g4lly5ec().b[1][0]++;
    cov_g4lly5ec().s[1]++;
    name = `${name} (DEBUG)`;
  } else {
    cov_g4lly5ec().b[1][1]++;
  }
  const foo =
    (cov_g4lly5ec().s[2]++,
    DEBUG
      ? (cov_g4lly5ec().b[2][0]++, "(DEBUG) foo")
      : (cov_g4lly5ec().b[2][1]++, "foo"));
  cov_g4lly5ec().s[3]++;
  for (const i = (cov_g4lly5ec().s[4]++, 0); i < name.length; i++) {
    cov_g4lly5ec().s[5]++;
    name += "!";
  }
  cov_g4lly5ec().s[6]++;
  return `Hello ${name}`;
}

f、s、b分别表示function、statement和branch。在cov_g4lly5ec().b[1][0]++;中 1 代表第一个分支语句,1 后面紧跟的 0 代表分支的第一个叉路。

3、jest编程注意事项

  1. 外部导入

(1)不能直接import方法并模拟,实际使用的方法并不是通过import导入并mock的。如下:

import * as SaveApply from '../xx/SaveModel';
import { save } from '../xx/SaveModel';
// ok
SaveApply.save = jest.fn().mockResolvedValue({ resultCode: 200 });
// wrong
save = jest.fn().mockResolvedValue({ resultCode: 200 });

(2)可以使用Jest.mock来模拟整个模块或者模块的一部分。jest.mock是放到文件开头全局使用。

import { save } from '../xx/SaveModel';
jest.mock('../xx/SaveModel', () => {
    const origin = jest.requireActual('../xx/SaveModel');
    return {
        ...origin,
        save: jest.fn()
    };
});
  1. jest.fn和jest.spyOn区别

jest.fn和jest.spyOn都可以用来mock一个函数,区别是jest.fn mock的函数不会去执行,而jest.spyOn mock的函数是会去正常执行的,并提供mockRestore来恢复。

首先,真正执行代码有没有意义呢? jest.spyOn() 监视一个对象的方法时,我们可以确保 mock 函数与原始方法的行为是一致的,从而可以测试代码的实际行为。如果我们只是手动创建一个 mock 函数,并假定它的行为与原始方法相同,我们可能会遇到一些意想不到的问题,因为我们无法确保 mock 函数的行为与原始方法的行为完全一致。例如:

// 一个判断折扣的model
const Discount = {
    checkDiscount: name => (
        if(name === 'apple') {
            return true;
        }
        return false;
};

const calculatePrice = (goods, checkDiscount) => {
    let total = 0;
    goods.forEach(item => {
        let price = Number(item.price) * Number(item.count);
        if(checkDiscount(item.name)) {
            price *= 0.5;
        }
        total += price;
    });
    return total;
};

下面对calculatePrice函数进行test,如果直接mock则永远发现不了折扣是否发生改变。

describe('test', () => {
    const goods = [
        {name: 'apple', price: 20, count: 2},
        {name: 'banana', price: 10, count: 1}
    ];
    const spyCheckDiscount = jest.spyOn(Discount, 'checkDiscount');
    // mock假数据
    // spyCheckDiscount.mockReturnValueOnce(false).mockReturnValueOnce(true);
    test('calculatePrice', () => {
        expect(calculatePrice(goods, spyCheckDiscount).toBe(30));
    });
});

其次,jest.spyOn的监视作用,如下:

const obj = {
  method: (a, b) => a + b,
};

jest.spyOn(obj, 'method');
console.log(obj.method(1, 2)); // 输出 3
console.log(obj.method.mock.calls); // 输出 [ [ 1, 2 ] ]
console.log(obj.method.mock.calls.length); // 输出 1
  1. 其他

Jest.fn()默认返回的是underfined。toBeNull/toBeUndefined/toBeTruthy/toBeFalsy等,均是toEqual的简单封装,用于检测返回值的,而toBeCalled等是检测函数是否调用的。