Jest学习笔记
前端测试发展近况
对于前端来说,像后端一样写测试用例的同学并不多。毕竟前端应用是围绕浏览器进行应用开发的,多了UI这一层,输入和输出都是在UI上而不是像后端一样的纯数据,这造成了测试的问题。目前有调查称近半的前端开发者从来没有接触过编写前端测试用例。
针对这种情况,目前前端的测试应该分为单元测试和集成测试。
- 单元测试:类似后端,测试某个代码片段,一般来说不带UI,检测其输入输出是否符合预期。这一类测试往往比较精确,运行速度也快。流行的有Jest、Mocha
- 集成测试(UI测试):使用自动化UI测试工具,配合浏览器对实际UI进行测试,产生真实的UI事件、截图对比。因为涉及真实的UI,这一类测试可能天生带有失败率,运行速度也比较慢。流行的有Puppeteer、PhantomJS
同样为了满足测试条件,前端开发者应该尽量降低视图层和逻辑层的耦合,便于进行精准的单元测试,同时也是一种良好的设计模式。
今天看的Jest就是属于单元测试。
Jest
Jest是Facebook推出的Javscript单元测试框架,运行在浏览器和Node平台上。有以下特点:
- 零配置开箱即用
- 实时测试(其实就是改了测试文件自动跑一下)
- 对象快照,便于监控对象变化
官方入门示例
安装jest:
$ npm install --save-dev jest
sum.js
function sum(a, b) {
return a + b;
}
module.exports=sum
sum.test.js
const sum = require('./sum')
describe('my test suite',()=>{
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
})
package.json
{
"scripts": {
"test": "jest"
}
}
执行测试:
$ npm run test
PASS src/sum.test.js
✓ adds 1 + 2 to equal 3 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.08s
Ran all test suites.
这个入门示例虽然很短也很简单,但为我们展示了jest很重要的基本概念:
- 测试用例可以由测试套件(test suite)在逻辑上分组,创建测试套件对应
describe
方法,创建测试用例对应test
方法。 - 单元测试由断言方法(assert)驱动,如例子中的
expect
方法和tobe
相等断言。如果断言不报错,则认为测试通过。 - 测试用例文件名符合
*.test.js
的形式,jest就会自动去找到项目下的测试用例文件去运行。也不需要显式用js引入测试相关方法。所以jest自称零配置。 - 由npm指令
jest
去启动执行测试用例。在控制台打印出测试结果。
断言
和大部分单元测试框架一样,Jest单元测试是断言驱动的。一个精确的断言驱动,至少应该包括:
- 测试执行的程序过程。
- 程序过程的输入。
- 程序过程的预期输出。
- 断言是否成功的结果。
对我们最简单的Jest例子来说:
test('two plus two is four', () => {
expect(sum(2,2)).toBe(4);
});
expect
接受一个过程sum
和过程的输入2,2
,tobe
定义一个预期输出是当且仅当输出为4,断言是否成功的结果是执行是否抛出JS Error。所以Jest满足成为一个精确的断言工具的能力。
Jest提供的断言匹配方法有:
tobe
,用Object.is
来判断的“完全相等”toEqual
,用于对对象或数组递归比较判断相等toBeTruthy
和toBeFalsy
,用于判断真假值- 针对
number
类型的有:toBeGreaterThan
toBeGreaterThanOrEqual
toBeLessThan
toBeLessThanOrEqual
- 针对
string
类型的有:toMatch
匹配一个正则规则
- 针对可迭代类型(Array, Set, Map, 自定义迭代器)
toContain
检查是否包含
- 针对Exception的有:
toThrow
异步测试
JavaScript是一门天生充满异步的语言,JavaScript测试框架也就必须要有对异步的支持,否则只能算一个残废。
对于Jest来说,他不知道什么时候异步调用会结束,如果不做任何处理,当测试用例走完了Jest就认为测试结束了,显然是不适用于异步的。那么Jest需要知道有多少回调会被完成,或者有完成的标记。
回调函数callbacks
对于回调函数形式的异步。Jest提供了一个无参数的函数done()
来标记测试完成。如果done()
没有被调用,最终测试用例会被标记为失败。
test('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
Promise
如果是Promise
类型的异步,同样也可以使用done
参数来标记异步结束。或者也可以return
这个Promise
,测试框架会自动等待这个Promise
变为Resolve或Reject
done
形式:
function promisedSum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b);
}, 2000);
});
}
test("adds 1 + 2 to equal 3", (done) => {
function cb(data) {
expect(data).toBe(3);
done()
}
promisedSum(1,2).then(cb)
});
return
形式:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
catch
错误也是一样的处理形式:
function promisedReject() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("err");
}, 2000);
});
}
test("adds 1 + 2 to equal 3", (done) => {
function cb(data) {
expect(data).toMatch('err');
done()
}
promisedReject(1,2).catch(cb)
});
async
Jest还可以在测试用例中直接接收async
函数语法,对于测试async
函数或者是返回值为promise
的函数更加好用,就不需要调用其他api了,推荐这种。
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');
}
});
测试初始化(setup)和拆除(teardown)
有时候测试的单元是需要程序环境的,比如需要数据库、需要其他模块数据等。这时候就需要在单元测试启动前先初始化环境,在结束时拆除环境。
Jest提供beforeEach
和afterEach
来做这两件事。
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
Mock函数
对于JS这种灵活的语言来说,经常会有把函数传来传去的做法。要测试函数被调用的情况,可以很大程度覆盖一些测试场景。
mock函数可以用来测试函数被调用时的参数、返回值、调用次数。用jest.fn
来创建,也是一个实用的测试方法。
// 待测试的方法
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 断言mock函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
// 断言mock函数第二次被调用的第一个参数是0
expect(mockCallback.mock.calls[1][0]).toBe(1);
// 断言mock函数第一次被调用的返回值的42
expect(mockCallback.mock.results[0].value).toBe(42);