Jest介绍及快速上手

2,559 阅读7分钟

单元测试的意义

  • 大规模代码重构时,能保证重构的正确性
  • 保证代码的质量,验证功能完整性

单元测试分类

  • TDD - (测试驱动开发)侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码
  • BDD - (行为驱动开发) 由外到内的开发方式,从外部定义业务成果,再深入到能实现这些成果,每个成果会转化成为相应的包含验收标准

简单来说就是TDD先写测试模块,再写主功能代码,然后能让测试模块通过测试,而BDD是先写主功能模块,再 写测试模块

主流的前端测试框架

  • **Karma **- 基于Node.js的JavaScript测试执行过程管理工具(Test Runner),让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行
  • Mocha - Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试( Mocha+chai )
  • **jest **-Jest 是 Facebook 开发的一款 JavaScript 测试框架。在 Facebook 内部广泛用来测试各种 JavaScript 代码

Jest简介

Jest是一个Javascript测试框架,由Facebook开源,致力于简化测试,降低前端测试成本,已被create-react-app@vue/cli等脚手架工具默认集成。Jest主打开箱即用快照功能、独立并行测试以及良好的文档和Api.

Jest使用介绍

安装

// 初始化一个项目
mkdir jest-test && cd jest-test
npm init -y

npm install -D jest

//#在package.json的scripts中添加
 "scripts": {
    "test": "jest"
  }

利用babel将代码转译为es5

npm install -D babel-jest @babel/core @babel/preset-env

编辑babel配置文件 .babelrc

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

编写测试

// 新建一个目标文件 index.js
export function sum(a, b) {
      return a + b;
}
//新建测试文件 index.test.js
import {sum} from './target'

test('测试sum函数 1 加 2 等于 3',()=>{
    expect(sum(1,2)).toBe(3)
})

运行 npm run test

PASS  ./index.spec.js
   测试sum方法 1  2 等于 3 (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.544 s
Ran all test suites.

主要功能介绍

1、Matchers 匹配器

普通匹配器

// toBe 测试是否精确匹配
test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

// toEqual检查对象的值

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

Truthiness

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefinedtoBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假

数字

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual

字符串

可以检查对具有 toMatch 正则表达式的字符串

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

Arrays and iterables

通过 toContain来检查一个数组或可迭代对象是否包含某个特定项

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'beer',
];

test('the shopping list has beer on it', () => {
  expect(shoppingList).toContain('beer');
  expect(new Set(shoppingList)).toContain('beer');
});

异常

想要测试的特定函数抛出一个错误,在它调用时,使用 toThrow

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(Error);

  // You can also use the exact error message or a regexp
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  expect(compileAndroidCode).toThrow(/JDK/);
});

2、测试异步代码

一、如果异步模式是回调函数

比如fetchData(callback) 函数,获取一些数据并在完成时调用 callback(data)。 你期望返回的数据是一个字符串 good

使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。

test('the data is good', done => {
  function callback(data) {
    try {
      expect(data).toBe('good');
      done();
    } catch (error) {
      done(error);
    }
  }

  fetchData(callback);
});

二、Promises

 fetchData 不使用回调函数,而是返回一个 Promise,其resolve值为字符串 good

test('the data is good', () => {
  return fetchData().then(data => {
    expect(data).toBe('good');
  });
});

一定不要忘记把 promise 作为返回值,如果你忘了 return 语句的话,在 fetchData 返回的这个 promise 被 resolve、then() 有机会执行之前,测试就已经被视为已经完成了

如果fetchData() 返回额 Promise是rejected的,请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则一个fulfilled态的 Promise 不会让测试失败

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

三、.resolves / .rejects

可以在 expect 语句中使用 .resolves 匹配器,Jest 将等待此 Promise 解决。 如果承诺被拒绝,则测试将自动失败。

test('the data is good', () => {
  return expect(fetchData()).resolves.toBe('good');
});

一定不要忘记把整个断言作为返回值返回⸺如果你忘了return语句的话,在 fetchData 返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了。

如果 Promise 被拒绝,则使用.resolves

test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});

  四、Async / Await

可以在测试中使用 async 和 await

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

可以用 async 和 await 联合.resolves or .reject一起使用

test('the data is peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  await expect(fetchData()).rejects.toThrow('error');
});

3、测试hooks

  Jest提供了`beforeEach``afterEach``beforeAll``afterAll`等钩子,主要用于一些测试之前等状态预设和测试完成后等重置、清理工作. 其中`beforeEach``afterEach`会分别在每个单元测试的运行前和运行结束后执行,`beforeAll``afterAll`则是在所有单元测试的执行前和执行完成后运行。
  此外可以通过 `describe` 块来将测试分组。 当 `before` 和 `after` 的块在 `describe` 块内部时,则其只适用于该 `describe` 块内的测试。
 顶级的 `beforeEach` 在 `describe` 块级的 `beforeEach` 之前被执行
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

4、Mock 函数

   Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 `new` 实例化时捕获构造函数的实例、允许测试时配置返回值。
   Jest 中有三个与 Mock函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()。
   mock函数主要提供了三个功能用于测试

  - 调用捕获
  - 返回值设定
  - 改变函数实现

jest.fn 是最简单的实现一个mock函数的方法

test("mock test", () => {
 const mock = jest.fn(()=> 'jest.fn test');
 expect(mock()).toBe('jest.fn test'); //函数返回结果
 expect(mock).toHaveBeenCalled(); //函数被调用
 expect(mock).toHaveBeenCalledTimes(1); //调用1次
});

test("mock 返回值", () => {
  const mock = jest.fn();
  mock.mockReturnValue("return value"); //mock 返回值
  expect(mock()).toBe("return value");
});

test("mock promise", () => {
  const mock = jest.fn();
  mock.mockResolvedValue("promise resolve"); // mock promise

  expect(mock("promise")).resolves.toBe("promise resolve");
  expect(mock).toHaveBeenCalledWith("promise"); // 调用参数检验
});

//或者使用赋值的形式 
function add(v1,v2){
  return v1 + v2
}

add = jest.fn()

test("mock dependency", () => {
  let result = add(1,2);
  expect(reslut).toBeDefined();
  expect(add).toHaveBeenCalledWith(1,2)
});

**jest.mock mock整个module, **擦除函数的实际实现

//mod.js
export function modTest(v1, v2) {
  return v1 * v2
}
//index.test.js
import {modTest} from './mod'

jest.mock('./mod')

test('jest.mock test', () => {
  let result = modTest(1,2)
  expect(result).toBe(undefined);
  expect(modTest).toHaveBeenCalledTimes(1);
  expect(modTest).toHaveBeenCalledWith(1,2)
})

jest.spyOn

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。

import * as mod from './mod'
    
test('jest.spyOn test', () => {
  const modMock = jest.spyOn(mod,'modTest')
  expect(mod.modTest(1, 2)).toBe(2);

  // and the spy stores the calls to add
  expect(modMock).toHaveBeenCalledWith(1, 2);
})

5、Mock 定时器

原生的定时器函数(如:setTimeoutsetIntervalclearTimeoutclearInterval)并不是很方便测试,因为程序需要等待相应的延时。

我们通过jest.useFakeTimers();来模拟定时器函数

// timerGame.js
'use strict';

function timerGame(callback) {
  console.log('Ready....go!');
  setTimeout(() => {
    console.log("Time's up -- stop!");
    callback && callback();
  }, 1000);
}

module.exports = timerGame;
// __tests__/timerGame-test.js
'use strict';

jest.useFakeTimers();

test('waits 1 second before ending the game', () => {
  const timerGame = require('../timerGame');
  timerGame();

  expect(setTimeout).toHaveBeenCalledTimes(1);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

使用 jest.runAllTimers(), 用于在测试中将时间“快进”到正确的时间点

test('calls the callback after 1 second', () => {
  const timerGame = require('../timerGame');
  const callback = jest.fn();

  timerGame(callback);

  // 在这个时间点,定时器的回调不应该被执行
  expect(callback).not.toBeCalled();

  // “快进”时间使得所有定时器回调被执行
  jest.runAllTimers();

  // 现在回调函数应该被调用了!
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

VS Code编辑器中调试测试用例

可以在VS Code中安装Jest插件,对编写的测试用例调试,具体使用参考Jest插件使用