Jest 是 Facebook 推出的一款开源的前端测试框架,或者我更愿意称其为包含了断言库 / 测试框架 / Mock 框架 / CLI 的开箱即用的前端测试套件。
我们常说的前端测试框架比如 Jasmine 或者 Mocha 其实某种程度上并不能独立的运行起来并且集成在我们日常的工作流之中。比如最常见的 Jasmine,Jasmine 基本上很难去脱离 Karma 去使用,并且 Jasmine 自带的断言和 Mock 有的时候也无法完全满足我们的需求,可能还需要搭配 Sinon.js 或者 chai,最后配合 Istanbul 进行覆盖率分析。在实际的工作中,当你想基于 Jasmine 去构建项目的测试框架时,实际上你会发现,你的绝大多数时间都花在了搭建这些工作链上。
我们创建了这样一个 math.js 的测试文件。其中 describe 类似 Jasmine 中的 describe, 属于 test suite 的描述,而每个 test 或者 it 则描述了每个 test case。
}}
当然 test suite 可以进行嵌套
describe ( ' foo ' , () => {
describe ( ' bar ' , () => {
it ( ' foo bar ' , () => {
//test code
});
});
});
test case 也可以脱离 test suite 独立运行
test ( ' adds 1 + 2 to equal 3 ' , () => {
expect ( sum ( 1 , 2 )). toBe ( 3 );
});
断言
断言是一个测试用例中不可缺少的部分。
Jest 内置了很多断言 ,基本可以满足常用的需求。
// 内置断言
test ( ' two plus two is four ' , () => {
expect ( 2 + 2 ). toBe ( 4 );
});
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 );
}
}
});
//
当你觉得 Jest 的内置断言不够灵活时,你也可以创建自己的断言。
// Custom断言
expect. extend ({
toBeDivisibleBy ( received , argument ) {
const pass = received % argument == 0 ;
if (pass) {
return {
message : () => `expected ${ received } not to be divisible by ${ argument } ` ,
pass : true ,
};
} else {
return {
message : () => `expected ${ received } to be divisible by ${ argument } ` ,
pass : false ,
};
}
},
});
test ( ' even and odd numbers ' , () => {
expect ( 100 ). toBeDivisibleBy ( 2 );
expect ( 101 ).not. toBeDivisibleBy ( 2 );
expect ({ apples : 6 , bananas : 3 }). toEqual ({
apples : expect. toBeDivisibleBy ( 2 ),
bananas : expect.not. toBeDivisibleBy ( 2 ),
});
});
Mock
Mock 是单元测试中经常使用的一种技术。单元测试,顾名思义测试的重点是某个具体单元。但是在实际代码中,代码与代码之间,模块与模块之间总是会存在着相互引用。这个时候,剥离出这种单元的依赖,让测试更加独立,使用到的技术就是 Mock。
{{
}}
比如在实际项目中,经常会有这种代码之间的引用存在。
// fooBar.js
export const getFooResult = () => {
// foo logic here
};
export const getBarResult = () => {
// bar logic here
};
// caculate.js
import { getFooResult, getBarResult } from ' ./math ' ;
export const getFooBarResult = () => getFooResult () + getBarResult ();
此时,getFooResult() 和 getBarResult() 就是 getFooBarResult 这个函数的依赖。如果我们关注的点是 getFooBarResult 这个函数,我们就应该把 getFooResult 和 getBarResult Mock 掉,剥离这种依赖。下面是一个使用 Jest 进行 Mock 的例子。
// calculate.test.js
import { getFooBarResult } from ' ./calculate ' ;
import * as fooBar from ' ./math ' ;
test ( ' getResult should return result getFooResult() + getBarResult() ' , () => {
// mock add方法和multiple方法
fooBar.getFooBarResult = jest. fn (() => 10 );
fooBar.getBarResult = jest. fn (() => 5 );
const result = getResult ();
expect (result). toEqual ( 15 );
});
在上面的例子中,我们关注的逻辑是 getFooBarResult()这个函数中 getFooResult() + getBarResult()这段相加的逻辑,进行 Mock 之后我们能很好的进行独立的单元测试。
当然,Jest 也支持 Mock 技术中的 Spy。
// calculate.test.js
import { getFooBarResult } from ' ./calculate ' ;
import * as fooBar from ' ./math ' ;
test ( ' getResult should return result getFooResult() + getBarResult() ' , () => {
// mock add方法和multiple方法
fooBar.getFooBarResult = jest. fn (() => 10 );
fooBar.getBarResult = jest. fn (() => 5 );
const result = getResult ();
// 监控getFooResult和getBarResult的调用情况.
expect (fooBar.getFooResult). toHaveBeenCalled ( 1 );
expect (fooBar.getBarResult). toHaveBeenCalled ( 1 );
});
对于使用 export default foo
或者 module.exports
语法的模块导出,我们可以使用mockImplementation
进行 mock。
// foo.js
module . exports = function () {
// some implementation;
};
// test.js
jest. mock ( ' ../foo ' ); // this happens automatically with automocking
const foo = require ( ' ../foo ' );
// foo is a mock function
foo. mockImplementation (() => 42 );
foo ();
// > 42
Jest 中关于 Mock 还有很多用法,感兴趣的可以仔细阅读一下官方文档。
jest-Mock-Functions
异步代码
在实际应用中, js 有很大一部分的交互是异步的交互,因此,如何方便的测试异步代码也是 很需要考量的一个地方。
// request.js
const http = require ( ' http ' );
export default function request ( url ) {
return new Promise (( resolve ) => {
// This is an example of an http request, for example to fetch
// user data from an API.
// This module is being mocked in __mocks__/request.js
http. get ({ path : url }, ( response ) => {
let data = '' ;
response. on ( ' data ' , ( _data ) => (data += _data));
response. on ( ' end ' , () => resolve (data));
});
});
}
在测试 request 这个函数时,我们实际上并不想去真正的发送网络请求,我们需要做的只是验证我们的 get call 发出去了,并且在我们做出了对于对应 response 正确的响应。
// __mocks__/request.js
const users = {
4 : { name : ' Mark ' },
5 : { name : ' Paul ' },
};
export default function request ( url ) {
return new Promise (( resolve , reject ) => {
const userID = parseInt (url. substr ( ' /users/ ' .length), 10 );
process. nextTick (() =>
users[userID]
? resolve (users[userID])
: reject ({
error : ' User with ' + userID + ' not found. ' ,
})
);
});
}
我们创建了这样一个 mock 文件,该文件指明了 request 的 mock。
// __tests__/user-test.js
jest. mock ( ' ../request ' );
import * as user from ' ../user ' ;
// The assertion for a promise must be returned.
it ( ' works with promises ' , () => {
expect. assertions ( 1 );
return user. getUserName ( 4 ). then (( data ) => expect (data). toEqual ( ' Mark ' ));
});
it ( ' works with resolves ' , () => {
expect. assertions ( 1 );
return expect (user. getUserName ( 5 )).resolves. toEqual ( ' Paul ' );
});
在 测试文件中,我们手动调用jest.mock
来告诉 Jest 我们不需要 automock, 而是使用我们创建的 mock 文件。
关于更多异步代码的测试,请查阅官方文档
Async-code testing
总结
Jest 是一套很强大的开箱即用的测试套件,其中的用法丰富多变,本文涉及的只是很基础很简单的一部分,如果有更多的需求,我也鼓励大家更多去阅读官方的文档。
参考
Jest 官方文档
https://juejin.im/post/597aa518f265da3e345f3262