在单元测试中, 必须排除本函数之外的其它因素对函数自身的影响,换句话说单元测试必须是确定性的,这种情况下就需要用到Mock.
[模拟函数]
参考官方文档: 模拟函数
test('mock function', () => {
const sum = jest.fn();
let val = sum(1, 2);
console.log(val);
});
test('mock function 的返回值', () => {
const sum = jest.fn().mockReturnValue('8');
let val = sum(1, 2);
console.log(val);
});
test('mock function 的模拟实现', () => {
const sum3 = jest.fn().mockImplementation(
(a, b) => {
return a + b;
}
);
let val = sum3(4, 5);
expect( sum3(4, 5)).toBe(9);
});
这些模拟的函数可以检查是否被调用过, 参考文档:
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
// 此模拟函数被调用了两次
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);
[模拟现有module或者不存在的module]
开发中经常会用到第三方库甚至是还没开发完的库, 这时候我们可以对这些库进行模拟. 这时候直接模拟库里的单个函数是不管用的. 参考文档:
http://facebook.github.io/jest/docs/zh-Hans/manual-mocks.html#content
jest.mock(moduleName, factory, options)
参考: 官方API文档
moduleName
模块名, 如果是npm定义的, 路径是单字符串, 否则是模块的相对路径, 主要这个相对路径可能和被测的类并不一样. 例如:
__tests__/a-test.js
jest.mock("../..B");
A.js
import '../B';
这段代码会在请求时自动返回一个mock版, 被mock的模块中的每个函数都会变成mock版. factory
和 options
参数是可选的.
例如: jest.mock('react-native-alert');
factory
第二个参数, 工厂方法, 用来替代默认的Jest mock实现.
jest.mock('../moduleName', () => {
return jest.fn(() => 42);
});
const moduleName = require('../moduleName'); // This runs the function specified as second argument to `jest.mock`.
moduleName(); // Will return '42';
options
第三个参数, 选项, 可以用来创建虚拟的 mock – 模拟在系统中任何地方都不存在的module, 或者是那种没有开发完成的模块.
jest.mock('../moduleName', () => {
/*
* Custom implementation of a module that doesn't exist in JS,
* like a generated module or a native module in react-native.
*/
}, {virtual: true});
下面是个完整的例子, 注意 virtualApi 压根就没开发:
import virtualApi from 'virtualApi';
jest.mock('virtualApi', () => {
return {
sendVerifyVCode:
() => {
return 'abc';
// 也可返回async的Promise方法
// return new Promise((resolve, reject) => {
// process.nextTick(
// () =>
// resolve(JSON.parse('{"success":true,"code":200,"msg":null,"data":null}'))
// )
// });
}
}
}, {virtual: true}
);
it('can fetch', async () => {
expect(virtualApi.sendVerifyVCode()).toEqual('abc');
});
一般情况下我们的项目使用的是 ES 模块 import语法 , 这时候所有的 import
语句都会写在测试文件的开头. 但是通常需要先mock再使用. 因此, Jest 会自动把所有的 jest.mock
调用放到模块的顶层来执行 (在任何 import语句之前生效).
[模拟定时器]
很多测试都用到了定时器操作, 没有必要完全去等待. 具体参考文档: http://facebook.github.io/jest/docs/zh-Hans/timer-mocks.html#content
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log('Times up -- stop!');
callback && callback();
}, 1000);
setTimeout(() => {
console.log('Times up -- stop!');
}, 2000);
}
test('waits 1 second before ending the game', () => {
jest.useFakeTimers();
const callback = jest.fn();
timerGame(callback);
expect(setTimeout.mock.calls.length).toBe(2);// 总共调用了两次
expect(setTimeout.mock.calls[1][1]).toBe(2000);// 第二次调用的参数是2000
jest.runAllTimers();// 立即执行所有函数
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback.mock.calls.length).toBe(1);// callback方法被调用了一次
});
[模拟fetch请求]
使用fetch-mock框架, 主页 文档
参考文章:
Testing JavaScript's Fetch with Jest - Happy Path https://codereviewvideos.com/course/react-redux-and-redux-saga-with-symfony-3/video/testing-javascript-s-fetch-with-jest-happy-path
下面是一个具体的例子:
import fetchMock from 'fetch-mock';
it('can fetch', async () => {
// Mock请求的语句请写到测试方法里面, 这样不会互相干扰
// fetchMock.post('*', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "jest-post": true}'));
fetchMock.get('http://fake.com', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "jest": true}'));
fetchMock.get('http://www.***.cn/api/v1', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "abc": true}'));
// fetchMock.get('http://fake.com', {hello: "world"});
console.log("fetch******=", fetch);
const response = await fetch('http://fake.com');
const result = await response.json();
expect(result.success).toEqual(true);
let return2 = await fetch('http://www.***.cn/api/v1');
let text = await return2.text();
console.log("fetch response ******=", text);
expect(text).toEqual('{"success":true,"code":200,"msg":null,"data":null,"abc":true}');
});
[实际案例]
[案例: 解决React Native中对本地库的依赖]
修复 Invariant Violation: Native module cannot be null.
jest.mock('NetInfo', () => {
return {
isConnected: {
fetch: () => {
return new Promise((accept, resolve) => {
accept(true);
})
},
addEventListener: jest.fn()
}
}
});
[案例: 模拟Fetch返回值]
仅供参考,实际开发中不会这么用 来源: https://www.snip2code.com/Snippet/1682585/Mock-Fetch-with-Jest/
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
'Content-type': 'application/json'
}
});
};
test('fetch something', () => {
window.fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, '{"foo":"bar"}')));
fetch('/foo/bar')
.then(r => r.json())
.then(json => expect(json.foo).toEqual('bar'));
});
[案例: 模拟全局对象]
参考: https://stackoverflow.com/questions/40449434/mocking-globals-in-jest
每个测试在它自己的环境中运行, 因此可以 mock 全局global对象, 直接在单个test中覆盖即可. 所有的全局变量都可以通过 global
命名空间访问.
global.navigator = {
onLine: true
}