Как макетировать функции в том же модуле, используя шутку
Какой лучший способ правильно высмеивать следующий пример?
Проблема в том, что после импорта, foo
сохраняет ссылку на оригинал без изменений bar
,
module.js:
export function bar () {
return 'bar';
}
export function foo () {
return `I am foo. bar is ${bar()}`;
}
module.test.js:
import * as module from '../src/module';
describe('module', () => {
let barSpy;
beforeEach(() => {
barSpy = jest.spyOn(
module,
'bar'
).mockImplementation(jest.fn());
});
afterEach(() => {
barSpy.mockRestore();
});
it('foo', () => {
console.log(jest.isMockFunction(module.bar)); // outputs true
module.bar.mockReturnValue('fake bar');
console.log(module.bar()); // outputs 'fake bar';
expect(module.foo()).toEqual('I am foo. bar is fake bar');
/**
* does not work! we get the following:
*
* Expected value to equal:
* "I am foo. bar is fake bar"
* Received:
* "I am foo. bar is bar"
*/
});
});
Спасибо!
РЕДАКТИРОВАТЬ: я мог бы изменить:
export function foo () {
return `I am foo. bar is ${bar()}`;
}
в
export function foo () {
return `I am foo. bar is ${exports.bar()}`;
}
но это р. некрасиво на мой взгляд делать везде:/
11 ответов
Между прочим, я решил использовать инъекцию зависимостей, задав аргумент по умолчанию.
Так что я бы изменил
export function bar () {
return 'bar';
}
export function foo () {
return `I am foo. bar is ${bar()}`;
}
в
export function bar () {
return 'bar';
}
export function foo (_bar = bar) {
return `I am foo. bar is ${_bar()}`;
}
Это не является принципиальным изменением API моего компонента, и я могу легко переопределить панель в моем тесте, выполнив следующие действия.
import { foo, bar } from '../src/module';
describe('module', () => {
it('foo', () => {
const dummyBar = jest.fn().mockReturnValue('fake bar');
expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
});
});
Это имеет то преимущество, что приводит к немного более хорошему тестовому коду:)
Альтернативным решением может быть импорт модуля в его собственный файл кода и использование импортированного экземпляра всех экспортируемых объектов. Как это:
import * as thisModule from './module';
export function bar () {
return 'bar';
}
export function foo () {
return `I am foo. bar is ${thisModule.bar()}`;
}
Сейчас издевается bar
это действительно легко, потому что foo
также использует экспортированный экземпляр bar
:
import * as module from '../src/module';
describe('module', () => {
it('foo', () => {
spyOn(module, 'bar').and.returnValue('fake bar');
expect(module.foo()).toEqual('I am foo. bar is fake bar');
});
});
Импорт модуля в его собственный код выглядит странно, но благодаря поддержке ES6 циклического импорта он работает очень гладко.
Похоже, проблема связана с тем, как вы ожидаете, что область действия бара будет решена.
С одной стороны, в module.js
Вы экспортируете две функции (вместо объекта, содержащего эти две функции). Из-за способа экспорта модулей ссылка на контейнер экспортируемых вещей exports
как вы упомянули об этом.
С другой стороны, вы обрабатываете свой экспорт (что вы дали module
) как объект, содержащий эти функции и пытающийся заменить одну из своих функций (панель функций).
Если вы внимательно посмотрите на свою реализацию foo, вы фактически держите фиксированную ссылку на функцию bar.
Когда вы думаете, что заменили функцию bar новой, вы просто заменили эталонную копию в области действия вашего module.test.js
Чтобы заставить foo фактически использовать другую версию bar, у вас есть две возможности:
В файле module.js экспортируйте класс или экземпляр, содержащий метод foo и bar:
Module.js:
export class MyModule { function bar () { return 'bar'; } function foo () { return `I am foo. bar is ${this.bar()}`; } }
Обратите внимание на использование этого ключевого слова в методе foo.
Module.test.js:
import { MyModule } from '../src/module' describe('MyModule', () => { //System under test : const sut:MyModule = new MyModule(); let barSpy; beforeEach(() => { barSpy = jest.spyOn( sut, 'bar' ).mockImplementation(jest.fn()); }); afterEach(() => { barSpy.mockRestore(); }); it('foo', () => { sut.bar.mockReturnValue('fake bar'); expect(sut.foo()).toEqual('I am foo. bar is fake bar'); }); });
Как вы сказали, переписать глобальную ссылку в глобальном
exports
контейнер. Это не рекомендуемый путь, так как вы, возможно, введете странные поведения в других тестах, если не вернете экспорт в исходное состояние.
У меня была такая же проблема, и из-за стандартов проекта, определения класса или переписывания ссылок в exports
не были одобрены варианты проверки кода, даже если они не были предотвращены определениями linting. То, на что я наткнулся в качестве жизнеспособного варианта, - это использовать babel-rewire-plugin, который намного чище, по крайней мере, по внешнему виду. Хотя я обнаружил, что это используется в другом проекте, к которому у меня был доступ, я заметил, что он уже был в ответе на аналогичный вопрос, который я связал здесь. Это фрагмент, скорректированный для этого вопроса (и без использования шпионов), предоставленный из связанного ответа для справки (я также добавил точки с запятой в дополнение к удалению шпионов, потому что я не язычник):
import __RewireAPI__, * as module from '../module';
describe('foo', () => {
it('calls bar', () => {
const barMock = jest.fn();
__RewireAPI__.__Rewire__('bar', barMock);
module.foo();
expect(bar).toHaveBeenCalledTimes(1);
});
});
/questions/12013041/kak-shpionit-za-importirovannoj-funktsiej-s-pomoschyu-sinon/12013042#12013042
Работает для меня:
cat moduleWithFunc.ts
export function funcA() {
return export.funcB();
}
export function funcB() {
return false;
}
cat moduleWithFunc.test.ts
import * as module from './moduleWithFunc';
describe('testFunc', () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
module.funcB.mockRestore();
});
it.only('testCase', () => {
// arrange
jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));
// act
const result = module.funcA();
// assert
expect(result).toEqual(true);
expect(module.funcB).toHaveBeenCalledTimes(1);
});
});
Из этой темы :
Попробуйте использовать функциональное выражение
export const bar = () => {
return "bar"
}
Это должно позволить вам шпионить за
bar
даже если он используется другой функцией в том же модуле.
Если вы определяете свои экспорты, то можете ссылаться на свои функции как часть объекта экспорта. Затем вы можете переписать функции в ваших макетах по отдельности. Это связано с тем, что импорт работает как ссылка, а не как копия.
module.js:
exports.bar () => {
return 'bar';
}
exports.foo () => {
return `I am foo. bar is ${exports.bar()}`;
}
module.test.js:
describe('MyModule', () => {
it('foo', () => {
let module = require('./module')
module.bar = jest.fn(()=>{return 'fake bar'})
expect(module.foo()).toEqual('I am foo. bar is fake bar');
});
})
Здесь доступны различные хаки, чтобы заставить это работать, но реальный ответ, который должен использовать большинство людей, таков: не делайте этого. Взяв пример модуля OP:
export function bar () {
return 'bar';
}
export function foo () {
return `I am foo. bar is ${bar()}`;
}
и проверяя фактическое поведение , вы должны написать:
import { bar, foo } from "path/to/module";
describe("module", () => {
it("foo returns 'bar'", () => {
expect(bar()).toBe('bar');
});
it("foo returns 'I am foo. bar is bar'", () => {
expect(foo()).toBe('I am foo. bar is bar');
});
});
Почему? Потому что тогда вы можете провести рефакторинг внутри границ модуля, не меняя тесты, что дает вам уверенность в улучшении качества вашего кода, зная, что он по-прежнему делает то, что должен.
Представьте, что вы извлекли создание
'bar'
from в неэкспортированную функцию, например:
function rawBar() {
return 'bar';
}
export function bar () {
return rawBar();
}
export function foo () {
return `I am foo. bar is ${rawBar()}`;
}
Тест, который я предлагаю выше, пройдет. Если бы вы утверждали, что призвание
foo
имел ввиду
bar
был вызван, этот тест начал давать сбой, даже если рефакторинг сохранил поведение модуля (тот же API, те же выходные данные). Это деталь реализации .
Тестовые двойники предназначены для соавторов , если что-то действительно нужно здесь помокать, это следует извлечь в отдельный модуль (тогда помокать намного проще, что говорит о том, что вы движетесь в правильном направлении). Попытка имитировать функции в том же модуле похожа на издевательство над частями класса, который вы пытаетесь протестировать, что я аналогично иллюстрирую здесь: /questions/57001222/kakie-funktsii-ya-dolzhen-imitirovat-vo-vremya-modulnogo-testirovaniya/57010820#57010820.
Мне потребовалось слишком много времени, чтобы заметить комментарий от @Nickofthyme под вопросом, поэтому я делюсь им в качестве ответа.
Он ссылается на к этот комментарийпроблеме GitHub, где он дал, безусловно, лучший ответ, imho. Это не требует каких-либо странных ссылок на себя в коде, просто требуется, чтобы они были написаны как функциональные выражения, что мне кажется намного менее болезненным и может быть реализовано с помощью
func-style
eslint rule, как он упомянул.
Я не могу поверить в этот ответ, я просто хотел сделать его более заметным. Если Nickofthyme захочет поделиться этим в качестве ответа, я полностью за это и с радостью проголосую за него!
Если вы используете Babel (т.е.
@babel/parser
) для обработки вашего кода, babel-plugin-explicit-exports-references
Пакет 1 npm решает эту проблему довольно элегантно, делая "уродливым"
module.exports
замены для вас прозрачно в транспильское время . См. Исходную ветку проблемы для получения дополнительной информации.
1 Примечание: я написал этот плагин!
Для пользователей модулей CommonJS предположим, что файл выглядит примерно так:
/* myModule.js */
function bar() {
return "bar";
}
function foo() {
return `I am foo. bar is ${bar()}`;
}
module.exports = { bar, foo };
Вам необходимо изменить файл на:
/* myModule.js */
function bar() {
return "bar";
}
function foo() {
return `I am foo. bar is ${myModule.bar()}`; // Change `bar()` to `myModule.bar()`
}
const myModule = { bar, foo }; // Items you wish to export
module.exports = myModule; // Export the object
Ваш исходный набор тестов (
myModule.test.js
) теперь должно пройти:
const myModule = require("./myModule");
describe("myModule", () => {
test("foo", () => {
jest.spyOn(myModule, "bar").mockReturnValueOnce("bar-mock");
const result = myModule.foo();
expect(result).toBe("I am foo. bar is bar-mock");
});
});
Подробнее: Mock / Spy экспортированные функции в одном модуле Jest