jest
jest:
Jest是由Facebook发布的开源的、基于Jasmine的JavaScript单元测试框架。Jest源于Facebook两年前的构想,用于快速、可靠地测试Web聊天应用。它吸引了公司内部的兴趣,Facebook的一名软件工程师Jeff Morrison半年前又重拾这个项目,改善它的性能,并将其开源。Jest的目标是减少开始测试一个项目所要花费的时间和认知负荷,因此它提供了大部分你需要的现成工具:快速的命令行接口、Mock工具集以及它的自动模块Mock系统。此外,如果你在寻找隔离工具例如Mock库,大部分其它工具将让你在测试中(甚至经常在你的主代码中)写一些不尽如人意的样板代码,以使其生效。Jest与Jasmine框架的区别是在后者之上增加了一些层。最值得注意的是,运行测试时,Jest会自动模拟依赖。Jest自动为每个依赖的模块生成Mock,并默认提供这些Mock,这样就可以很容易地隔离模块的依赖。
通过npm 下载 jest 指令是:
npm install --save-dev jest
也可以通过yan 下载jest 指令是:
yarn add --dev jest
模拟功能
Mock函数允许您通过擦除函数的实际实现,捕获对该函数的调用(以及在这些调用中传递的参数),捕获用实例化的构造函数的实例new
以及允许对它们进行测试时配置来测试代码之间的链接。返回值。
有两种模拟函数的方法:通过创建要在测试代码中使用的模拟函数,或编写manual mock
来重写模块依赖性。
使用模拟功能
假设我们正在测试一个函数的实现,该函数forEach
为提供的数组中的每个项目调用一个回调。
1 function forEach(items, callback) { 2 for (let index = 0; index < items.length; index++) { 3 callback(items[index]); 4 } 5 }
为了测试此功能,我们可以使用模拟功能,并检查模拟的状态以确保按预期方式调用回调。
1 const mockCallback = jest.fn(x => 42 + x); 2 forEach([0, 1], mockCallback); 3 4 // The mock function is called twice 5 expect(mockCallback.mock.calls.length).toBe(2); 6 7 // The first argument of the first call to the function was 0 8 expect(mockCallback.mock.calls[0][0]).toBe(0); 9 10 // The first argument of the second call to the function was 1 11 expect(mockCallback.mock.calls[1][0]).toBe(1); 12 13 // The return value of the first call to the function was 42 14 expect(mockCallback.mock.results[0].value).toBe(42);
.mock
属性
所有模拟函数都具有此特殊.mock
属性,该属性用于保存有关函数调用方式和函数返回内容的数据。该.mock
属性还跟踪this
每个调用的值,因此也
1 const myMock = jest.fn(); 2 3 const a = new myMock(); 4 const b = {}; 5 const bound = myMock.bind(b); 6 bound(); 7 8 console.log(myMock.mock.instances); 9 // > [
1 const myMock = jest.fn(); 2 3 const a = new myMock(); 4 const b = {}; 5 const bound = myMock.bind(b); 6 bound(); 7 8 console.log(myMock.mock.instances); 9 // > [ <a>, <b> ]
这些模拟成员在断言这些函数如何被调用,实例化或它们返回什么的测试中非常有用:
1 // The function was called exactly once 2 expect(someMockFunction.mock.calls.length).toBe(1); 3 4 // The first arg of the first call to the function was 'first arg' 5 expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); 6 7 // The second arg of the first call to the function was 'second arg' 8 expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); 9 10 // The return value of the first call to the function was 'return value' 11 expect(someMockFunction.mock.results[0].value).toBe('return value'); 12 13 // This function was instantiated exactly twice 14 expect(someMockFunction.mock.instances.length).toBe(2); 15 16 // The object returned by the first instantiation of this function 17 // had a `name` property whose value was set to 'test' 18 expect(someMockFunction.mock.instances[0].name).toEqual('test')
模拟返回值
模拟功能还可用于在测试期间将测试值注入代码中:
1 const myMock = jest.fn(); 2 console.log(myMock()); 3 // > undefined 4 5 myMock 6 .mockReturnValueOnce(10) 7 .mockReturnValueOnce('x') 8 .mockReturnValue(true); 9 10 console.log(myMock(), myMock(), myMock(), myMock()); 11 // > 10, 'x', true, true
模拟函数在使用函数连续传递样式的代码中也非常有效。用这种风格编写的代码有助于避免使用复杂的存根来重新创建其所代表的实际组件的行为,从而有利于在使用它们之前将值直接注入测试中。
1 const filterTestFn = jest.fn(); 2 3 // Make the mock return `true` for the first call, 4 // and `false` for the second call 5 filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); 6 7 const result = [11, 12].filter(num => filterTestFn(num)); 8 9 console.log(result); 10 // > [11] 11 console.log(filterTestFn.mock.calls); 12 // > [ [11], [12] ]
实际上,大多数实际示例都涉及在依赖组件上获取模拟功能并对其进行配置,但是技术是相同的。在这种情况下,请尝试避免在任何未经直接测试的功能内实现逻辑的诱惑。
模拟模块
假设我们有一个从API获取用户的类。该类使用axios调用API,然后返回data
包含所有用户的属性:
1 // users.js 2 import axios from 'axios'; 3 4 class Users { 5 static all() { 6 return axios.get('/users.json').then(resp => resp.data); 7 } 8 } 9 10 export default Users;
现在,为了在不实际访问API的情况下测试该方法(从而创建缓慢而脆弱的测试),我们可以使用该jest.mock(...)
函数自动模拟axios模块。
一旦对模块进行了模拟,我们就可以提供一个mockResolvedValue
for .get
,以返回我们要针对测试进行断言的数据。实际上,我们说的是我们希望axios.get('/ users.json')返回假响应。
1 // users.test.js 2 import axios from 'axios'; 3 import Users from './users'; 4 5 jest.mock('axios'); 6 7 test('should fetch users', () => { 8 const users = [{name: 'Bob'}]; 9 const resp = {data: users}; 10 axios.get.mockResolvedValue(resp); 11 12 // or you could use the following depending on your use case: 13 // axios.get.mockImplementation(() => Promise.resolve(resp)) 14 15 return Users.all().then(data => expect(data).toEqual(users)); 16 });
模拟实现
但是,在某些情况下,超越指定返回值的功能并完全替换模拟功能的实现是有用的。这可以通过模拟函数jest.fn
或mockImplementationOnce
方法来完成。
1 const myMockFn = jest.fn(cb => cb(null, true)); 2 3 myMockFn((err, val) => console.log(val)); 4 // > true
mockImplementation
当您需要定义从另一个模块创建的模拟函数的默认实现时,该方法很有用:
1 // foo.js 2 module.exports = function() { 3 // some implementation; 4 }; 5 6 // test.js 7 jest.mock('../foo'); // this happens automatically with automocking 8 const foo = require('../foo'); 9 10 // foo is a mock function 11 foo.mockImplementation(() => 42); 12 foo(); 13 // > 42
当您需要重新创建模拟函数的复杂行为以使多个函数调用产生不同的结果时,请使用以下mockImplementationOnce
方法:
1 const myMockFn = jest 2 .fn() 3 .mockImplementationOnce(cb => cb(null, true)) 4 .mockImplementationOnce(cb => cb(null, false)); 5 6 myMockFn((err, val) => console.log(val)); 7 // > true 8 9 myMockFn((err, val) => console.log(val)); 10 // > false
当模拟功能用尽了用定义的实现时mockImplementationOnce
,它将执行的默认实现集jest.fn
(如果已定义):
1 const myMockFn = jest 2 .fn(() => 'default') 3 .mockImplementationOnce(() => 'first call') 4 .mockImplementationOnce(() => 'second call'); 5 6 console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); 7 // > 'first call', 'second call', 'default', 'default'
对于通常具有链式方法(因此总是需要返回this
)的情况,我们提供了一个含糖的API以简化.mockReturnThis()
函数的形式简化此过程,该函数也位于所有模拟中:
1 const myObj = { 2 myMethod: jest.fn().mockReturnThis(), 3 }; 4 5 // is the same as 6 7 const otherObj = { 8 myMethod: jest.fn(function() { 9 return this; 10 }), 11 };
模拟名称
您可以选择为模拟函数提供一个名称,该名称将在测试错误输出中显示,而不是显示“ jest.fn()”。如果您希望能够快速识别在测试输出中报告错误的模拟功能,请使用此功能。
1 const myMockFn = jest 2 .fn() 3 .mockReturnValue('default') 4 .mockImplementation(scalar => 42 + scalar) 5 .mockName('add42');
自定义匹配器
最后,为了减少对如何调用模拟函数的要求,我们为您添加了一些自定义匹配器函数:
1 // The mock function was called at least once 2 expect(mockFunc).toHaveBeenCalled(); 3 4 // The mock function was called at least once with the specified args 5 expect(mockFunc).toHaveBeenCalledWith(arg1, arg2); 6 7 // The last call to the mock function was called with the specified args 8 expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2); 9 10 // All calls and the name of the mock is written as a snapshot 11 expect(mockFunc).toMatchSnapshot();
这些匹配器是用于检查.mock
财产的常见形式的糖。如果这更符合您的口味,或者您需要执行更具体的操作,则始终可以自己手动执行此操作:
1 // The mock function was called at least once 2 expect(mockFunc.mock.calls.length).toBeGreaterThan(0); 3 4 // The mock function was called at least once with the specified args 5 expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]); 6 7 // The last call to the mock function was called with the specified args 8 expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([ 9 arg1, 10 arg2, 11 ]); 12 13 // The first arg of the last call to the mock function was `42` 14 // (note that there is no sugar helper for this specific of an assertion) 15 expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42); 16 17 // A snapshot will check that a mock was invoked the same number of times, 18 // in the same order, with the same arguments. It will also assert on the name. 19 expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]); 20 expect(mockFunc.getMockName()).toBe('a mock name');
Dom 操作:
通常认为很难测试的另一类功能是直接操作DOM的代码。让我们看看如何测试下面的jQuery代码片段,以侦听click事件,异步获取一些数据并设置span的内容。
1 // displayUser.js 2 'use strict'; 3 4 const $ = require('jquery'); 5 const fetchCurrentUser = require('./fetchCurrentUser.js'); 6 7 $('#button').click(() => { 8 fetchCurrentUser(user => { 9 const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out'); 10 $('#username').text(user.fullName + ' - ' + loggedText); 11 }); 12 });
同样,我们在
__tests__/
文件夹中创建一个测试文件:1 // __tests__/displayUser-test.js 2 'use strict'; 3 4 jest.mock('../fetchCurrentUser'); 5 6 test('displays a user after a click', () => { 7 // Set up our document body 8 document.body.innerHTML = 9 '<div>' + 10 ' <span id="username" />' + 11 ' <button id="button" />' + 12 '</div>'; 13 14 // This module has a side-effect 15 require('../displayUser'); 16 17 const $ = require('jquery'); 18 const fetchCurrentUser = require('../fetchCurrentUser'); 19 20 // Tell the fetchCurrentUser mock function to automatically invoke 21 // its callback with some data 22 fetchCurrentUser.mockImplementation(cb => { 23 cb({ 24 fullName: 'Johnny Cash', 25 loggedIn: true, 26 }); 27 }); 28 29 // Use jquery to emulate a click on our button 30 $('#button').click(); 31 32 // Assert that the fetchCurrentUser function was called, and that the 33 // #username span's inner text was updated as we'd expect it to. 34 expect(fetchCurrentUser).toBeCalled(); 35 expect($('#username').text()).toEqual('Johnny Cash - Logged In'); 36 });