单元测试主要是为了验证代码运行是否符合预期
所以单测的常用语句就是expect(xxx).toXXX()
用例示例
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
用例组成
- 引用待测函数
- 用例描述
test或it - 调用指定方法
- 判断结果是否符合预期
匹配器
判断相等
.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来处理异步代码。
使用then和catch来判断返回
使用 jest 的resolves和rejects的方法得到返回
then 和 catch
// 假设 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'));
});
resolves 和 rejects
// 假设 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()
});