近年来,随着前端工程化的发展,前端发生了翻天覆地的变化。jQuery已经慢慢淡出了咱们的视野,React、Vue和anglur三驾马车急速驶来。今后,前端进入了数据驱动的时代,也有了清晰的模块化开发的方式。随之而来的就是如何去保证本身的代码的正确性。html
编写测试代码要在正是写代码前进行的,它就至关于具体明确的需求文档。以后咱们写的代码若是能经过测试代码就证实是符合预期的。前端
除此以外,因为一个项目须要多人维护,也许别人不当心改动了你的代码就会致使新的问题。因此提交代码前须要跑一遍测试用例,确保本身没有改动别人的逻辑。若是有改动别人的代码,必定要弄清楚这样改动会不会产生新的问题,最后记得把测试用例代码也要改下。ios
前端测试工具也和前端的框架同样纷繁复杂,其中常见的测试工具,大体可分为测试框架、断言库、测试覆盖率工具等几类。在正式开始本文以前,咱们先来大体了解下它们:git
测试框架的做用是提供一些方便的语法来描述测试用例,以及对用例进行分组。github
测试框架可分为两种: TDD (测试驱动开发)和 BDD (行为驱动开发),我理解二者间的区别主要是一些语法上的不一样,其中 BDD 提供了提供了可读性更好的用例语法,至于详细的区别可参见 The Difference Between TDD and BDD 一文。npm
常见的测试框架有 Jasmine, Mocha 以及本文要介绍的 Jest 。json
断言库主要提供语义化方法,用于对参与测试的值作各类各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。常见的断言库有 Should.js, Chai.js 等。axios
用于统计测试用例对代码的测试状况,生成相应的报表,好比 istanbul 。前端工程化
Jest 是 Facebook 出品的一个测试框架,相对其余测试框架,其一大特色就是就是内置了经常使用的测试工具,好比自带断言、测试覆盖率工具,实现了开箱即用。数组
而做为一个面向前端的测试框架, Jest 能够利用其特有的快照测试功能,经过比对 UI 代码生成的快照文件,实现对 React 等常见框架的自动测试。
此外, Jest 的测试用例是并行执行的,并且只执行发生改变的文件所对应的测试,提高了测试速度。目前在 Github 上其 star 数已经破两万;而除了 Facebook 外,业内其余公司也开始从其它测试框架转向 Jest ,好比 Airbnb 的尝试 ,相信将来 Jest 的发展趋势仍会比较迅猛。
Jest 能够经过 npm 或 yarn 进行安装。以 npm 为例,既可用npm install -g jest
进行全局安装;也能够只局部安装、并在 package.json 中指定 test 脚本:
{ "scripts": { "test": "jest" } }
Jest 的测试脚本名形如*.test.js
,不论 Jest 是全局运行仍是经过npm run test
运行,它都会执行当前目录下全部的*.test.js
或 *.spec.js
文件、完成测试。
具体用法参考JEST官网,咱们这里只是简单介绍几个常规用法。
表示测试用例是一个测试框架提供的最基本的 API , Jest 内部使用了 Jasmine 2 来进行测试,故其用例语法与 Jasmine 相同。test()
函数来描述一个测试用例,举个简单的例子:
// hello.js module.exports = () => 'Hello world'
// hello.test.js let hello = require('hello.js') test('should get "Hello world"', () => { expect(hello()).toBe('Hello world') // 测试成功 // expect(hello()).toBe('Hello') // 测试失败 })
其中toBe('Hello world')
即是一句断言( Jest 管它叫 “matcher” ,想了解更多 matcher 请参考文档)。写完了用例,运行在项目目录下执行npm test
,便可看到测试结果。
有时咱们想在测试开始以前进行下环境的检查、或者在测试结束以后做一些清理操做,这就须要对用例进行预处理或后处理。对测试文件中全部的用例进行统一的预处理,可使用 beforeAll()
函数;而若是想在每一个用例开始前进行都预处理,则可以使用 beforeEach()
函数。至于后处理,也有对应的 afterAll()
和 afterEach()
函数。
若是只是想对某几个用例进行一样的预处理或后处理,能够将先将这几个用例归为一组。使用 describe()
函数便可表示一组用例,再将上面提到的四个处理函数置于 describe()
的处理回调内,就实现了对一组用例的预处理或后处理:
describe('test testObject', () => { beforeAll(() => { // 预处理操做 }) test('is foo', () => { expect(testObject.foo).toBeTruthy() }) test('is not bar', () => { expect(testObject.bar).toBeFalsy() }) afterAll(() => { // 后处理操做 }) })
异步代码的测试,关键点在于告知测试框架测试什么时候完成,让其在恰当的时机进行断言。随着Babel的盛行,前端的异步写法不少都是用 Promise 的形式了,这使得咱们能够用 async/await 相似同步的方式写异步。下面看下如何针对这种写法测试:
// promiseHello.js module.exports = (name) => { return new Promise((resolve) => { setTimeout(() => resolve(`Hello ${name}`), 1000) }) }
// promiseHello.test.js let promiseHello = require('promiseHello.js') test('should get "Hello World"', async () => { const data = await promiseHello('World'); expect(data).toBe('Hello World'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { const data = await promiseHello('World'); expect(data).toBe('Hello World'); } catch (e) { expect(e).toMatch('error'); } });
Mock 函数容许你测试代码之间的链接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new
实例化时捕获构造函数的实例、容许测试时配置返回值。
使用 mock 函数
假设咱们要测试函数 forEach
的内部实现,这个函数为传入的数组中的每一个元素调用一次回调函数。
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } }
为了测试此函数,咱们可使用一个 mock 函数,而后检查 mock 函数的状态来确保回调函数如期调用。
const mockCallback = jest.fn(x => 42 + x); forEach([0, 1], mockCallback); // 此 mock 函数被调用了两次 expect(mockCallback.mock.calls.length).toBe(2); // 第一次调用函数时的第一个参数是 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // 第二次调用函数时的第一个参数是 1 expect(mockCallback.mock.calls[1][0]).toBe(1); // 第一次函数调用的返回值是 42 expect(mockCallback.mock.results[0].value).toBe(42);
.mock
属性
全部的 mock 函数都有这个特殊的 .mock
属性,它保存了关于此函数如何被调用、调用时的返回值的信息。
// The function was called exactly once expect(someMockFunction.mock.calls.length).toBe(1); // The first arg of the first call to the function was 'first arg' expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // The second arg of the first call to the function was 'second arg' expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); // The return value of the first call to the function was 'return value' expect(someMockFunction.mock.results[0].value).toBe('return value'); // This function was instantiated exactly twice expect(someMockFunction.mock.instances.length).toBe(2); // The object returned by the first instantiation of this function // had a `name` property whose value was set to 'test' expect(someMockFunction.mock.instances[0].name).toEqual('test');
Mock 的返回值
Mock 函数也能够用于在测试期间将测试值注入代码︰
const myMock = jest.fn(); console.log(myMock()); // > undefined myMock .mockReturnValueOnce(10) .mockReturnValueOnce('x') .mockReturnValue(true); console.log(myMock(), myMock(), myMock(), myMock()); // > 10, 'x', true, true
在函数连续传递风格(functional continuation-passing style)的代码中时,Mock 函数也很是有效。 以这种代码风格有助于避免复杂的中间操做,便于直观表现组件的真实意图,这有利于在它们被调用以前,将值直接注入到测试中。
const filterTestFn = jest.fn(); // Make the mock return `true` for the first call, // and `false` for the second call filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); const result = [11, 12].filter(num => filterTestFn(num)); console.log(result); // > [11] console.log(filterTestFn.mock.calls); // > [ [11], [12] ]
大多数现实世界例子中,实际是在依赖的组件上配一个模拟函数并配置它,但手法是相同的。 在这些状况下,尽可能避免在非真正想要进行测试的任何函数内实现逻辑。
假定有个从 API 获取用户的类。 该类用 axios 调用 API 而后返回 data
,其中包含全部用户的属性:
// users.js import axios from 'axios'; class Users { static all() { return axios.get('/users.json').then(resp => resp.data); } } export default Users;
如今,为测试该方法而不调用实际 API (使测试变的缓慢与不稳定),咱们能够用 jest.mock(...)
函数自动模拟 axios 模块。
一旦模拟axios模块,axios的返回结果就能够被咱们随意模拟值。咱们可为 .get
提供一个 mockResolvedValue
,它会返回假数据用于测试。 实际上,咱们想让 axios.get('/users.json') 有个假的 response。
// users.test.js import axios from 'axios'; import Users from './users'; jest.mock('axios'); // mock模拟模块 test('should fetch users', () => { const users = [{name: 'Bob'}]; const resp = {data: users}; axios.get.mockResolvedValue(resp); // 模拟实际调用axios后的返回值 // or you could use the following depending on your use case: // axios.get.mockImplementation(() => Promise.resolve(resp)) return Users.all().then(data => expect(data).toEqual(users)); });
近几年前端工程化的发展风起云涌,可是前端自动化测试这块内容你们却彷佛不过重视。虽然项目迭代过程当中会有专门的测试人员进行测试,但等他们来进行测试时,代码已经开发完成的状态。与之相比,若是咱们在开发过程当中就进行了测试,会有以下的好处:
固然,凡事都有两面性,好处虽然明显,却并非全部的项目都值得引入测试框架,毕竟维护测试用例也是须要成本的。对于一些需求频繁变动、复用性较低的内容,好比活动页面,让开发专门抽出人力来写测试用例确实得不偿失。
须要长期维护的项目。它们须要测试来保障代码可维护性、功能的稳定性
较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低
被屡次复用的部分,好比一些通用组件和库函数。由于多处复用,更要保障质量
参考: