我有一个用于学习测试的模块,看起来像这样:
api.js
import axios from "axios";
const BASE_URL = "https://jsonplaceholder.typicode.com/";
const URI_USERS = 'users/';
export async function makeApiCall(uri) {
try {
const response = await axios(BASE_URL + uri);
return response.data;
} catch (err) {
throw err.message;
}
}
export async function fetchUsers() {
return makeApiCall(URI_USERS);
}
export async function fetchUser(id) {
return makeApiCall(URI_USERS + id);
}
export async function fetchUserStrings(...ids) {
const users = await Promise.all(ids.map(id => fetchUser(id)));
return users.map(user => parseUser(user));
}
export function parseUser(user) {
return `${user.name}:${user.username}`;
}
很简单的东西。
现在,我想测试该fetchUserStrings
方法,并想对fetchUser
和parseUser
进行模拟/间谍。同时-我不希望parseUser
的行为受到嘲笑-在我实际进行测试时。
我遇到了一个问题,即似乎不可能对同一模块中的函数进行模拟/监视。
以下是我已阅读的资源:
How to mock a specific module function? Jest github issue.(超过100个大拇指)。
我们被告知的地方:
在JavaScript中,在需要模块之后无法通过模拟函数来支持上述功能–几乎没有办法检索foo所引用的绑定并对其进行修改。
jest-mock的工作方式是隔离运行模块代码,然后检索模块的元数据并创建模拟函数。同样,在这种情况下,它将无法修改foo的本地绑定。
他提出的解决方案是ES5-但此博客文章中描述了现代的等效方法:
https://luetkemj.github.io/170421/mocking-modules-in-jest/
在这里,不是直接调用我的函数,而是通过类似这样的对象来引用它们:
api.js
async function makeApiCall(uri) {
try {
const response = await axios(BASE_URL + uri);
return response.data;
} catch (err) {
throw err.message;
}
}
async function fetchUsers() {
return lib.makeApiCall(URI_USERS);
}
async function fetchUser(id) {
return lib.makeApiCall(URI_USERS + id);
}
async function fetchUserStrings(...ids) {
const users = await Promise.all(ids.map(id => lib.fetchUser(id)));
return users.map(user => lib.parseUser(user));
}
function parseUser(user) {
return `${user.name}:${user.username}`;
}
const lib = {
makeApiCall,
fetchUsers,
fetchUser,
fetchUserStrings,
parseUser
};
export default lib;
建议该解决方案的其他帖子:
https://groups.google.com/forum/#!topic/sinonjs/bPZYl6jjMdg https://stackoverflow.com/a/45288360/1068446
这似乎是同一想法的一种变体: https://stackoverflow.com/a/47976589/1068446
另一种选择是,我将模块分解,这样我就不会在彼此之间直接调用函数。
例如
api.js
import axios from "axios";
const BASE_URL = "https://jsonplaceholder.typicode.com/";
export async function makeApiCall(uri) {
try {
const response = await axios(BASE_URL + uri);
return response.data;
} catch (err) {
throw err.message;
}
}
user-api.js
import {makeApiCall} from "./api";
export async function fetchUsers() {
return makeApiCall(URI_USERS);
}
export async function fetchUser(id) {
return makeApiCall(URI_USERS + id);
}
user-service.js
import {fetchUser} from "./user-api.js";
import {parseUser} from "./user-parser.js";
export async function fetchUserStrings(...ids) {
const users = await Promise.all(ids.map(id => lib.fetchUser(id)));
return ids.map(user => lib.parseUser(user));
}
user-parser.js
export function parseUser(user) {
return `${user.name}:${user.username}`;
}
这样,我可以在测试依赖模块时模拟依赖模块,不用担心。
但是我不确定分解这样的模块是否可行-我想在某些情况下您可能需要循环依赖。
还有其他选择:
https://stackoverflow.com/a/47804180/1068446
这个看起来丑陋,imo。
https://stackoverflow.com/a/52725067/1068446
我必须承认-我没看那么多。
我正在对此进行调查。
我的问题:这都是一种令人沮丧且愚蠢的测试方式-是否存在人们在2018年编写单元测试的标准,简便易行的方式来专门解决此问题? < / p>
答案 0 :(得分:1)
您已经发现尝试直接测试ES6模块非常痛苦。在您的情况下,听起来好像是在编译ES6模块而不是直接对其进行测试,这可能会生成看起来像这样的代码:
async function makeApiCall(uri) {
...
}
module.exports.makeApiCall = makeApiCall;
由于其他方法直接调用makeApiCall
而不是导出,因此即使您尝试模拟导出也不会发生任何事情。从目前的角度来看,ES6模块的导出是一成不变的,因此,即使您没有移植模块,您仍然可能会遇到问题。
将所有内容附加到“ lib”对象可能是最简单的方法,但感觉就像是破解,而不是解决方案。另外,使用可以重新连接模块的库是一个潜在的解决方案,但它的功能非常强大,我认为它闻起来很香。通常,当您遇到这种类型的代码时,就会遇到设计问题。
将模块拆分成很小的部分感觉就像是穷人的依赖注入,正如您所说的那样,您可能会很快遇到问题。真正的依赖注入可能是最可靠的解决方案,但这是您需要从头开始构建的,不是您可以将其插入现有项目并期望立即进行工作的东西。
我的建议?创建类并将其用于测试,然后仅使模块成为该类实例的瘦包装。由于您使用的是类,因此将始终使用集中化对象(this
对象)来引用方法调用,这将使您能够模拟所需的东西。使用类也将使您有机会在构造类时注入数据,从而在测试中提供极为精细的控制。
让我们重构您的api
模块以使用一个类:
import axios from 'axios';
export class ApiClient {
constructor({baseUrl, client}) {
this.baseUrl = baseUrl;
this.client = client;
}
async makeApiCall(uri) {
try {
const response = await this.client(`${this.baseUrl}${uri}`);
return response.data;
} catch (err) {
throw err.message;
}
}
async fetchUsers() {
return this.makeApiCall('/users');
}
async fetchUser(id) {
return this.makeApiCall(`/users/${id}`);
}
async fetchUserStrings(...ids) {
const users = await Promise.all(ids.map(id => this.fetchUser(id)));
return users.map(user => this.parseUser(user));
}
parseUser(user) {
return `${user.name}:${user.username}`;
}
}
export default new ApiClient({
url: "https://jsonplaceholder.typicode.com/",
client: axios
});
现在让我们为ApiClient
类创建一些测试:
import {ApiClient} from './api';
describe('api tests', () => {
let api;
beforeEach(() => {
api = new ApiClient({
baseUrl: 'http://test.com',
client: jest.fn()
});
});
it('makeApiCall should use client', async () => {
const response = {data: []};
api.client.mockResolvedValue(response);
const value = await api.makeApiCall('/foo');
expect(api.client).toHaveBeenCalledWith('http://test.com/foo');
expect(value).toBe(response.data);
});
it('fetchUsers should call makeApiCall', async () => {
const value = [];
jest.spyOn(api, 'makeApiCall').mockResolvedValue(value);
const users = await api.fetchUsers();
expect(api.makeApiCall).toHaveBeenCalledWith('/users');
expect(users).toBe(value);
});
});
我应该注意,我尚未测试所提供的代码是否有效,但是希望这个概念很清楚。