Jest 是 Facebook 的一套开源的 JavaScript 测试框架。相比其他的测试框架,Jest具有如下的一些特点:
在软件的测试领域,测试主要分为:单元测试、集成测试和功能测试。
单元测试:又称为模块测试,就是对每个单元进行的测试,一般针对的是函数、类或单个组件,不涉及系统和集成。
集成测试,也叫组装测试或联合测试,在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。
功能测试,就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。
使用yarnyarn add --dev jest
或者npmnpm install --save-dev jest
进行安装。
Jest将根据项目提出一系列问题,并且将创建一个基础配置文件jest.config.js。
npx jest --init
Jest中可以使用console,不能使用debugger。
function sum(a, b) {
return a + b;
}
module.exports = sum;
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
{
"scripts": {
"test": "jest"
}
}
yarn test
或npm run test
命令,Jest会自动找到当前目录下所有的*.test.js
或 *.spec.js
文件并执行。Jest将打印下面这个消息:PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
Jest提供了expect函数用来包装被测试的方法并返回一个对象,该对象中包含一系列的匹配器来更方便的进行断言。
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
如果要检查对象的值,要使用toEqual()代替,toEqual()递归检查对象或数组的每个字段。test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
});
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
expect(value).toBeCloseTo(0.3);
});
在测试中,有时需要精准区分 undefined 、null 和 false,有时又不需要区分,jest 都满足。
test('null', () => {
const n = null;
expect(n).toBeNull();
});
可以通过 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 ConfigError('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow('you are using the wrong JDK');
});
jest 测试提供了一些测试的生命周期 API,可以辅助在每个 case 的开始和结束做一些处理。
test()函数来描述一个测试用例。
test('should get "Hello world"', () => {
expect(hello()).toBe('Hello world')
})
有时想在测试开始之前进行下环境的检查,或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。
对测试文件中所有的用例进行统一的预处理,可以使用 beforeAll() 函数。后处理有对应的 afterAll()函数。
而如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。后处理有对应的afterEach() 函数。
如果只是想对某几个用例进行同样的预处理或后处理,可以将先将这几个用例归为一组。使用 describe() 函数即可表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理。
describe('test testObject', () => {
beforeAll(() => {
// 预处理操作
})
test('is foo', () => {
expect(testObject.foo).toBeTruthy()
})
test('is not bar', () => {
expect(testObject.bar).toBeFalsy(
})
afterAll(() => {
// 后处理操作
})
})
测试异步代码的三种实现方式:回调函数、Promise、Async/Await。
例如:有一个fetchData(callback)的函数用来获取数据,并且在获取完成的时候调用callback 函数,希望测试返回的数据是“peanut butter” 。
// 错误
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
上面代码的问题在于:一旦fetchData完成,Jest测试也就执行完成,然后才会调用回调,这并不是按所期望的那样的去运行。
Jest提供了一种用于测试异步代码的实现方式, done() 被执行则意味着callback函数被调用。
test('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
如果代码中使用了Promise的方式,处理异步测试将会变得更加简单。Jest从测试中返回一个Promise,然后等待Promise被实现,如果没有实现,那么就判断测试是失败的。
test('the data is peanut butter', () => {
expect.assertions(1); //assertions(1)代表的是在当前的测试中至少有一个断言是被调用的,否则判定为失败。
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
如果断言的承诺并没有被实现,那么可以添加 .catch 方法。
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
test('the data is peanut butter', async () => {
expect.assertions(1);
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');
}
});
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便继续进行测试的测试方法。
Mock函数提供的以下三种特性:
在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。
jest.fn() 是创建Mock函数最简单的方式。
jest.fn()所创建的Mock函数可以设置返回值。
// functions.test.js
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
还定义内部实现或返回Promise对象。如果没有定义函数内部的实现,jest.fn() 会返回undefined作为返回值。
// functions.test.js
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]");
})
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);
})
例如:在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口,并通过传入的回调函数返回处理过的返回值。如果想测试该接口能够被正常请求,只需要捕获到传入的回调函数能够被正常的调用即可。
// fetch.js
import axios from 'axios';
export default {
async fetchPostsList(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
return callback(res.data);
})
}
}
// functions.test.js
import fetch from '../src/fetch.js'
test('fetchPostsList中的回调函数应该能够被调用', async () => {
expect.assertions(1);
let mockFn = jest.fn();
await fetch.fetchPostsList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
})
jest.mock()可以mock整个模块。
通过 jest.mock() 后,模块内的方法是不会被 jest 实际执行的。
例如:fetch.js文件夹中封装的请求方法在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。
// events.js
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!');
});
}
}
// functions.test.js
import events from '../src/events';
import fetch from '../src/fetch';
jest.mock('../src/fetch.js');
test('mock 整个 fetch.js模块', async () => {
expect.assertions(2);
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalled();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});
jest.spyOn() 是 jest.fn() 的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被 spy 的函数。
通过 jest.spyOn() 后,模块内的方法是被 jest 实际执行。
// functions.test.js
import events from '../src/events';
import fetch from '../src/fetch';
test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
expect.assertions(2);
const spyFn = jest.spyOn(fetch, 'fetchPostsList');
await events.getPostList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
})