Каков современный уровень тестирования / проверки функций в модуле в 2018 году?
У меня есть модуль, для целей обучения тестированию, который выглядит следующим образом:
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
чтобы насмехаться - когда я проверяю это.
Я сталкиваюсь с проблемой, что кажется, что невозможно посмеяться над шпионскими функциями в одном модуле.
Вот ресурсы, которые я читал об этом:
Как смоделировать конкретную функцию модуля? Шутка GitHub вопроса. (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/ /questions/46141060/kak-maketirovat-funktsii-v-tom-zhe-module-ispolzuya-shutku/46141132#46141132
И этот, кажется, вариант той же идеи: /questions/46141060/kak-maketirovat-funktsii-v-tom-zhe-module-ispolzuya-shutku/46141097#46141097
Разбейте объект на модули
Альтернативой является то, что я бы разбил свой модуль так, чтобы я никогда не вызывал функции непосредственно друг в друге.
например.
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;
}
}
пользовательский api.js
import {makeApiCall} from "./api";
export async function fetchUsers() {
return makeApiCall(URI_USERS);
}
export async function fetchUser(id) {
return makeApiCall(URI_USERS + id);
}
пользователя 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));
}
пользовательский parser.js
export function parseUser(user) {
return `${user.name}:${user.username}`;
}
И таким образом я могу смоделировать модули зависимостей при тестировании зависимого модуля, не беспокойтесь.
Но я не уверен, что такое разделение модулей даже возможно - я предполагаю, что может возникнуть ситуация, когда у вас есть циклические зависимости.
Есть несколько альтернатив:
Зависимость внедрения в функцию:
/questions/46141060/kak-maketirovat-funktsii-v-tom-zhe-module-ispolzuya-shutku/46141120#46141120
Это выглядит ужасно, как будто, IMO.
Используйте плагин babel-rewire
/questions/46141060/kak-maketirovat-funktsii-v-tom-zhe-module-ispolzuya-shutku/46141072#46141072
Я должен признать - я не смотрел на это много.
Разделите ваш тест на несколько файлов
Сейчас расследую это.
Мой вопрос: это все довольно разочаровывающий и сложный способ тестирования - есть ли стандартный, приятный и простой способ, которым люди пишут юнит-тесты в 2018 году, которые конкретно решают эту проблему?
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);
});
});
Я должен отметить, что я не проверял, работает ли предоставленный код, но, надеюсь, концепция достаточно ясна.