原文: Unit Testing Beginners Guide - Part 3 - Mock Http And Files access
作者:jstweetster
在本文结束时,您将能够使用Jest正确的测试包含http请求,文件访问方法,数据库调用或者任何其他类型的辅助作用的生产代码。此外,您将学习如何处理其他类似的问题,而且您需要避免实际调用的方法活模块(例如数据库调用)。
译者注:接下来的测试涉及到Jest需要使用Babel的情况,所以需要安装一些依赖,可以查看官方文档说明 https://jestjs.io/docs/zh-Hans/getting-started ,也可以直接查看上面给出的示例代码。
1)安装依赖
yarn add --dev babel-jest babel-core regenerator-runtime babel-preset-env # 也可以使用 npm 安装
2)在项目根目录新建 .babelrc
文件,添加内容如下:
{
"presets": ["env"]
}
让我们假设我们有一段代码使用 XMLHttpRequest
方法执行网络请求,如下:
const API_ROOT = 'http://jsonplaceholder.typicode.com';
class API {
getPosts() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `${API_ROOT}/posts`);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText);
if (resp.error) {
reject(resp.error);
} else {
resolve(resp);
}
}
}
xhr.send();
})
}
}
export default new API();
首先,让我们定义这个方法是干什么的。
getPosts()
使用API调用来检索博客中的帖子列表,并且返回一个解析帖子列表的Promise
我们想要避免的是在我们的测试中实际进行API调用。
为什么?
正如你所看到的,我们有很多理由希望避免直接在具有实际网络请求单元测试中工作。
解决方法是回溯到使用 XMLHttpRequest
对象的正确模拟,拦截对它的调用并伪造行为。
如果你不记得究竟模拟什么和怎样使用,你可以查看“使用Jest中的spies和fake timers” 和Jest官方文档对mocks的说明。
让我们尝试创建第一个测试(假设我们有一个 api.spec.js
文件):
import API from '../src/api.js';
const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify(
[
{
title: 'test post'
},
{
tile: 'second test post'
}
]
)
};
const oldXMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = jest.fn(() => mockXHR);
describe('API integration test suite', function () {
test('Should retrieve the list of posts from the server when calling getPosts method', function (done) {
const reqPromise = API.getPosts();
mockXHR.onreadystatechange();
reqPromise.then((posts) => {
expect(posts.length).toBe(2);
expect(posts[0].title).toBe('test post');
expect(posts[1].title).toBe('second test post');
done();
});
});
});
让我们一步一步的来了解发生了什么
const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify(
[
{
title: 'test post'
},
{
title: 'second test post'
}
]
)
};
我们开始创建了一个假的XHR对象,实际的 open
和 send
方法是不做任何事情的函数。我们还设置 readyState
的值为4(通常用于检测请求是否已完成)和 responseText
的值为适合我们要测试的内容。
我们可以通过为 responseText
提供正确的文本值来模拟我们想要的任何API响应。
const oldXMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = jest.fn(() => mockXHR);
接下来,我们将备份内置的XMLHttpRequest
对象,将其替换为返回我们的模拟对象的函数。备份真正的 XMLHttpRequest
对象是一个好主意,因为在测试结束时我们应该清理环境,让环境处于使用它之前的初始状态。
因此,每当调用 new XMLHttpRequest
时,将会返回 mockXHR
对象。
而且,最后我们使用这个设置,对 getPosts
函数进行单元测试更加容易啦。
const reqPromise = API.getPosts();
mockXHR.onreadystatechange();
调用这个 API ,然后通过调用 onreadystatechange
函数来模拟响应到达。当 onreadystatechange
被调用后,我们实际上正在调用在 api.js
中设置的状态更改的回调函数:
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText);
if (resp.error) {
reject(resp.error);
} else {
resolve(resp);
}
}
}
因为模拟的 XHR 对象的 readyState
的值设置为 4
,Promise 对象将会立即被解析(resolved)或拒绝(rejected),具体取决于响应。
根据断言,我们验证从 getPosts()
函数返回的 Promise
对象是否具有实际的 JSON 响应值作为被解析的值。
我们通过检查文章数量是否正确以及每篇文章应该是什么值来验证测试的正确性。
expect(posts.length).toBe(2);
expect(posts[0].title).toBe('test post');
expect(posts[1].title).toBe('second test post');
我们可以将该方法进行一些改进,使其更灵活和易于使用。
首先,让我们创建一个可以模拟 XHR 对象的工厂函数,它能够让我们更加容易的创建新的模拟 XHR,并且,可选择的指定响应的对象。
const createMockXHR = (responseJSON) => {
const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify(
responseJSON || {}
)
};
return mockXHR;
}
接下来,让我们为每一个单元测试创建一个新的 XHR 对象。在单元测试时,我们最不希望的是在单元测试中具有共享状态,这会导致不可预测且难以调试的测试。实际的测试单元如下:
describe('API integration test suite', function() {
const oldXMLHttpRequest = window.XMLHttpRequest;
let mockXHR = null;
beforeEach(() => {
mockXHR = createMockXHR();
window.XMLHttpRequest = jest.fn(() => mockXHR);
});
afterEach(() => {
window.XMLHttpRequest = oldXMLHttpRequest;
});
test('Should retrieve the list of posts from the server when calling getPosts method', function(done) {
const reqPromise = API.getPosts();
mockXHR.responseText = JSON.stringify([
{ title: 'test post' },
{ title: 'second test post' }
]);
mockXHR.onreadystatechange();
reqPromise.then((posts) => {
expect(posts.length).toBe(2);
expect(posts[0].title).toBe('test post');
expect(posts[1].title).toBe('second test post');
done();
});
});
});
此外,在每次测试之后,我们通过在 beforeEach
中模拟XMLHttpRequest
并在 afterEach
中恢复原本的 XMLHttpRequest
对象来清理环境。
beforeEach(() => {
mockXHR = createMockXHR();
window.XMLHttpRequest = jest.fn(() => mockXHR);
});
afterEach(() => {
window.XMLHttpRequest = oldXMLHttpRequest;
});
我们获得的另一个好处是我们可以非常轻松的测试不同的场景。假设我们想要添加另一个测试,模拟 API 返回错误:
test('Should return a failed promise with the error message when the API returns an error', function(done) {
const reqPromise = API.getPosts();
mockXHR.responseText = JSON.stringify({
error: 'Failed to GET posts'
});
mockXHR.onreadystatechange();
reqPromise.catch((err) => {
expect(err).toBe('Failed to GET posts');
done();
});
});
你是否注意到模拟API不同的响应变得更加容易啦?
我们可以非常轻松地扩展我们应用于HTTP请求的技术,以涵盖其他类型的副作用。
假设我们有一个 FileSystem
组件,它有一个读取文件并将其解析为 JSON 的方法:
import fs from 'fs';
export default class FileSystem {
parseJSONFile(file) {
const content = String(fs.readFileSync(file));
return JSON.parse(content);
}
}
我们想要测试 parseJSONFile()
方法,但是我们也想要避免从磁盘中创建文件和读取它的内容。
我们的测试单元如下:
jest.mock('fs', () => ({
readFileSync: jest.fn()
}));
import FileSystem from './FileSystem.js';
import fs from 'fs';
describe('FileSystem test suite', function() {
test('Should return the parsed JSON from a file specified as param', function(done) {
const fileReader = new FileSystem();
fs.readFileSync.mockReturnValue('{ "test": 1 }');
const result = fileReader.parseJSONFile('test.json');
expect(result).toEqual({ "test": 1 });
done();
});
});
让我们一步一步的来看:
jest.mock('fs', () => ({
readFileSync: jest.fn()
})})
jest.mock
允许我们模拟我们可能拥有的任何模块,包括在 NodeJS 中构建的并有工厂函数的模块作为第二个参数 (arg),返回模拟的返回值。
在我们的示例中,每当我们的代码里有 const fs = require('fs');
或者 import fs from 'fs'
,导入的值实际上是我们从工厂函数返回的对象:
{
readFileSync: jest.fn()
}
在使用 fs.readFileSync
之前调用 jest.mock
很重要。
接下来,我们实例化我们要测试的组件 const fileReader = new FileSystem();
,并指示 readFileSync
spy 返回某个预先制作的字符串:
fs.readFileSync.mockRetrunValue('{ "test": 1 }');
如果你想要了解更多的操作信息,请在 Jest 文档中查看 mockReturnValue。
最后,我们验证 parseJSONFile
的结果是解析的 JSON 值:
expect(result).toEqual({ "test": 1 });
以上是对模拟http调用和文件系统调用的介绍。
扫码关注w3ctech微信公众号
共收到0条回复