Jest简单入门

555 阅读6分钟

单元测试主要是为了验证代码运行是否符合预期 所以单测的常用语句就是expect(xxx).toXXX()

用例示例

const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

用例组成

  1. 引用待测函数
  2. 用例描述 testit
  3. 调用指定方法
  4. 判断结果是否符合预期

匹配器

判断相等

 .toBe(2)   // 值相等
 .toEqual({one: 1, two: 2}); // object
 .toBeNull()  // null
 .toBeUndefined()  // undefiend
 .toBeDefined()  // 与上面相反
 .toBeTruthy()   // true
 .toBeFalsy()    // false

not

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).not.toBeNaN();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

数字

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);
  
   const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});

正则匹配

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

包含

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

test('the shopping list has milk on it', () => {
  // 字符串、数组
  expect(shoppingList[0]).toContain('a');
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
})

toThrow

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

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

promise

test('resolves to lemon', () => {
  // make sure to add a return statement
  expect(Promise.resolve('lemon')).resolves.toBe('lemon');
  expect(Promise.reject('apple')).rejects.toBe('apple');
});

instance

class A {}

expect(new A()).toBeInstanceOf(A);
expect(() => {}).toBeInstanceOf(Function);

常见的处理案例

异步

正常情况下测试代码是同步执行的,但当我们待测的代码是异步的时候,就会有问题了,会导致test case已经结束了,但是我们的异步代码并没有执行,从而导致异步代码没有被测到。我们可以调用指定方法,来告知jest我们的异步执行完毕

done

如果test函数传入了done,jest就会等到done被调用才会结束当前的test case,如果done没有被调用,则该test自动不通过测试。

it('Test async code with done', (done) => {
  setTimeout(() => {
    // expect something
    done();
  }, 1000)
});

promise

如果代码中使用了Promise,则可以通过返回Promise来处理异步代码。
使用 thencatch 来判断返回
使用 jest 的 resolvesrejects 的方法得到返回

thencatch

// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', () => {
  expect.assertions(1);
  return doAsync().then((data) => {
    expect(data).toBe('example');
  });
});

it('Test promise with an error', () => {
  expect.assertions(1);
  return doAsync().catch(e => expect(e).toMatch('error'));
});

resolvesrejects

// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', () => {
  expect.assertions(1);
  return expect(doAsync()).resolves.toBe('example');
  });
});

it('Test promise with an error', () => {
  expect.assertions(1);
  return expect(doAsync()).rejects.toMatch('error'));
});

expect.assertions(n) 用来确保expect的执行次数

async/await

async/await 使用的是 promise 语法糖

// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', async () => {
  expect.assertions(1);
  const data = await doAsync();
  expect(data).toBe('example');
  });
});

async/await也可以和resolves/rejects一起使用

// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', async () => {
  expect.assertions(1);
  await expect(doAsync()).resolves.toBe('example');
  });
});

mock

在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。

Mock函数提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

jest.fn()

jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

我们可以判断 fn 函数的

  • 调用返回值
  • 是否被调用 (常用于判断对回调)
  • 调用次数
  • 调用时接收的参数
test('测试jest.fn()调用', () => {
  let mockFn = jest.fn();
  let result = mockFn(1, 2, 3);

  // 断言mockFn的执行后返回undefined
  expect(result).toBeUndefined();
  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1, 2, 3
  expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})

jest.fn() 还可以

  • 设置返回值
  • 定义内部实现
  • 返回Promise对象。
test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('default');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为default
  expect(result).toBe('default');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

jest.mock()

我们在做单元测试的时候,被测文件可能会引用别的模块。
但是我们通常只想测试单个文件的执行情况,这个时候我们就可以使用 mock整个文件

比如我们需要测试 sum 文件,sum 中调用了 utils 这个时候,init里面可能使用了一些东西我们在单个文件测试的时候没提供,有时候就会报错
所以这个时候我门不需要管init的逻辑是什么,只需要他调用就行了

const init = require('./utils')
function sum(a, b) {
    init();
    return a+b;
}
module.exports = sum;

这个时候我们的测试用例可以这么写
直接 mock 整个模块然后 验证它被调用就好了

const init = require('./utils')
const sum = require('./sum');

jest.mock('./utils.js')
it('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
  expect(init).toHaveBeenCalled();
});

有的时候我代码可能是这样,调用了函数使用了返回值
这个时候我们还可以 mock 来指定返回

比如这个 init 返回的是一个 Promise

function init(num) {
   // 执行一些列复杂操作
   return Promise.resolve(num)
}
module.exports = init;

并且 sum 方法使用了这个返回值

const init = require('./utils')
function sum(a, b) {
    return init(a + b) 
}
module.exports = sum;

这个时候可以通过 mock 指定返回内容

const init = require('./utils')
const sum = require('./sum');

jest.mock('./utils.js',()=>{

    return ()=> {
      return Promise.resolve(666);
    }
})
it('adds 1 + 2 to equal 3', () => {
  const result = init(sum(1, 2))
  result.then(r=>{
    expect(r).toBe(666);
  })
});

jest.spyOn()

有的时候我们 mock 之后 希望mock的函数内部代码被执行
并且我们希望验证这个函数的调用情况
这个时候我们就可以使用 spyOn

const video = {
  play() {
    return true;
  },
};

module.exports = video;
const video = require('./video');

test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});

可以看出,spyOn 并不会影响被mock函数的功能
spyOn 创建了一个和原函数又相同功能的 mock 函数
这样就可以保留原有功能,并且可以使用 mock 函数的方法去验证调用情况了

before/after 系列

beforeAll

在此文件中的任何测试运行之前运行一个函数。

比如在测试数据时先连接数据库

const DB = require('./db.js');

beforeAll(() => {
  // 在开始测试前连接数据
  // Jest将等待 promise 返回 resolve 后运行测试。
  return DB.clear().connect()
});

// 初始化数据后,进行操作
test('can find things', () => {
  return DB.find('thing', {}, results => {
    expect(results.length).toBeGreaterThan(0);
  });
});

// 省略一堆数据测试

afterAll

在此文件中的所有测试完成后运行一个函数。

比如在测试完毕后,关闭连接

const DB = require('./db.js');

beforeAll(() => {
  // 在开始测试前连接数据
  // Jest将等待 promise 返回 resolve 后运行测试。
  return DB.connect()
});

// 初始化数据后,进行操作
test('can find things', () => {
  return DB.find('thing', {}, results => {
    expect(results.length).toBeGreaterThan(0);
  });
});

// 省略一堆数据测试

afterAll(() => {
  // 在测试都结束之后关闭连接
  // Jest将等待 promise 返回 resolve 后运行测试。
  return DB.close()
});

beforeEach

在每个测试运行之前运行一个函数。

比如 每次测试都想拿到一个新的实例

const Event = require('./event')
let instance

beforeEach(() => {
  instance  = new Event()
});

test('on', () => {
  let fn = jest.fn()
  instance.on('click',fn)
  instance.emit('click')
  expect(fn).toHaveBeenCalled()
});