JEST - 单元测试框架

571 阅读5分钟

JEST -- 单元测试框架

由 Facebook 推出,目前 Babel,TypeScript,Node,React,Angular,Vue 等都在使用

JEST 官方文档

安装

yarn add --dev jest

npm install --save-dev jest

运行

全局安装 jest 时,使用“jest 文件”运行 没有全局安装 jest(环境变量没有 jest)时在 package.json 中添加命令“jest 文件”,使用 npm 运行

使用 babel

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

快照

第一次执行时将表达式的结果存储到.snap 文件中,之后每次执行将第一次结果与当前进行比较,相同则通过

// index.test.js
test('snap',()=>{
  expect(1+2).toMatchSnapshot('desc')
})

//index.test.js.snap
// snap测试块中的第一条测试首次执行的结果
exports[`snap: desc 1`] = `3`;

执行测试的命令中加上--updateSnapshot更新 snap

自定义快照

// index.test.js
const {toMatchSnapshot} = require('jest-snapshot')
expect.extend({
  newSnap(received, l) {
    return toMatchSnapshot.call(this, l, 'desc')
  }
})

test('snap',()=>{
    expect('hhh').newSnap(65)
})


//index.test.js.snap
exports[`snap: desc 1`] = `65`;

mock 函数

测试函数或模块的行为是否正确

创建 mock 函数

function add(a, b) {
  return a + b
}
test('mock', () => {
  const mockFun = jest.fn(add) // 创建
  const arr = [[1, 2], [3, 4]]
  arr.forEach(item => mockFun(...item))
  console.log(mockFun.mock)
  // 此 mock 函数被调用了两次
  expect(mockFun.mock.calls.length).toBe(2);

  // 第一次调用函数时的第一个参数是 0
  expect(mockFun.mock.calls[0][0]).toBe(1);

  // 第二次调用函数时的第一个参数是 1
  expect(mockFun.mock.calls[1][0]).toBe(3);

  // 第一次调用函数时的结果是 3
  expect(mockFun.mock.results[0].value).toBe(3);
})

// console结果
// {
//   calls: [ [ 1, 2 ], [ 3, 4 ] ],
//   instances: [ undefined, undefined ],
//   invocationCallOrder: [ 1, 2 ],
//   results: [ { type: 'return', value: 3 }, { type: 'return', value: 7 } ]
// }

创建 mock 模块

import axios from 'axios';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp); // 代替axios.get('/user.json').then的回调参数

  return axios.get('/user.json').then(resp =>expect(resp.data).toEqual(users))
});

期望 api

普通匹配器

  • toBe 比较值的一致性
  • toEqual 比较对象的一致性
  • not 匹配期望之外的值
  • toBeGreaterThan 数字大于
  • toBeGreaterThanOrEqual 数字大于等于
  • toBeLessThan 数字小于
  • toBeLessThanOrEqual 数字小于等于
  • toBeCloseTo 浮点数等于(浮点数比较时代替 toBe)
  • toContain 数组等可迭代对象包含某个值
expect(1 + 2).not.toBe(4)
expect([1, 2, 3]).toContain(2)
  • .toBeDefined() 值不是 undefined
  • .toBeUndefined() undefined
  • .toBeFalsy() 假值
  • .toBeTruthy() 真值
  • .toBeNaN() NaN
  • .toBeNull() null
expect(0).toBeFalsy()
  • .toMatch(string | regexp) 匹配正则
  • .toMatchObject(Object) 目标对象是否为接受到的对象的子集,对应属性值相同,深度比较
expect('str').toMatch(/^s/)
expect({a: 1, b: 2}).toMatchObject({a: 1})

代替值的匹配器

在 toEqual 和 toBeCalledWith 中替代

  • expect.anything() 匹配除 null 和 undefined 之外的值
  • expect.any(constrocutor) 匹配给定构造类型的值
  • expect.stringMatching(string | regexp) 匹配字符串或正则
expect(2).toEqual(expect.anything())
expect(2).toEqual(expect.any(Number))
expect('2').toEqual(expect.stringMatching(/2/))

自定义匹配器

使用 expect.extend 在执行测试之前定义

自定义的匹配器必须返回一个对象,该对象中包含 pass 和 message。其中 pass 是 Boolean 类型,表示是否通过,message 是 Function,返回报错信息

匹配器函数可以传多个参数,第一个为 expect 的值,其它为自定义传参

如果没有在全部的代码执行路径设置返回值,对应测试结果会报错

在自定义匹配器中通过 this 对象中的 isNot、promise、equals 等访问 jest 提供的函数

expect.extend({
  yourMatcher(x, y, z) {
    return {
      pass: true,
      message: () => '',
    };
  },
  yourMatcher2(x, y, z) {
    return {
      pass: true,
      message: () => '',
    };
  },
});

异步匹配

在测试块中直接使用异步函数,测试不会等待异步函数的处理结果

以下断言解决不能测试异步函数的问题(并非只用于处理异步函数)

  • expect.assertions(number) 测试期间调用一定数量的断言

  • expect.hasAssertions() 测试期间至少调用一次断言

  • 回调中执行测试

    该方法中如果没有在代码执行路径上调用 done,最后会报超时(等待时间为 test 中的第三个参数 timeout)

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

  fetchData(callback);
});
  • 使用 promise
// fetchData返回promise
test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
  • 使用 async/await
function get(){
  return new Promise(res=>res('Mark'))
}
test('snap', async ()=>{
  const data = await get()
  expect(data).toEqual('Mark')
})

全局 api

  • test(name, fn, timeout) 执行测试,timeout 可选,指定终止测试前的等待时间

  • test.each(table)(name, fn, timeout) 多组数据执行同一个测试

test.each([  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('.add(%i, %i)', (a, b, expected) => {
  expect(a + b).toBe(expected);
});
  • describe(name, fn) 将多个 test 包装成一个(非必须),内部是一个独立作用域
describe('all', () => {
  test('test1', () => {
    expect(1).toBeTruthy()
  });

  test('test2', () => {
    expect(0).toBeFalsy()
  });
});
  • describe.each(table)(name, fn, timeout) 多组数据执行多个测试
describe.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('.add(%i, %i)', (a, b, expected) => {
  test(`returns ${expected}`, () => {
    expect(a + b).toBe(expected);
  });

  test(`returned value not be greater than ${expected}`, () => {
    expect(a + b).not.toBeGreaterThan(expected);
  });

  test(`returned value not be less than ${expected}`, () => {
    expect(a + b).not.toBeLessThan(expected);
  });

});

不能在 test 中内嵌 test 或 describe

describe 中可以嵌套 describe

有多个 describe 和 test 时的执行顺序:先执行 describe 中除 test 之外的代码,全部执行完之后,按照 test 在代码上出现的顺序执行。

同一个 test 中如果有测试用例出错,不会执行该 test 剩余的测试用例,但仍会执行其他 test。

  • afterAll(fn, timeout) 同个测试块(describe)中所有 test执行结束之后调用
// 以下console顺序为
// 1
// 5
// 6
let a = 1
describe('all', () => {
  test('inner', () => {
    console.log(a)
  });
  afterAll(()=>{
    a = 5
  })
});
test('outer', ()=>{
  console.log(a)
  a = 6
})
afterAll(()=>{
  console.log(a)
})
  • afterEach(fn, timeout) 同个测试块(describe)中每个 test执行结束之后调用
  • beforeAll(fn, timeout) 同个测试块(describe)中所有 test执行开始之前调用
  • beforeEach(fn, timeout) 同个测试块(describe)中每个 test执行开始之前调用

本文使用 mdnice 排版