#javascript #unit-testing #jestjs #reselect
#javascript #модульное тестирование #jestjs #повторный выбор
Вопрос:
У меня довольно сложная структура селекторов в моем проекте (некоторые селекторы могут иметь до 5 уровней вложенности), поэтому некоторые из них очень сложно протестировать с передачей состояния ввода, и я хотел бы вместо этого имитировать селекторы ввода. Однако я обнаружил, что это на самом деле невозможно.
Вот самый простой пример:
// selectors1.js
export const baseSelector = createSelector(...);
—
// selectors2.js
export const targetSelector = createSelector([selectors1.baseSelector], () => {...});
Что я хотел бы иметь в своем наборе тестов:
beforeEach(() => {
jest.spyOn(selectors1, 'baseSelector').mockReturnValue('some value');
});
test('My test', () => {
expect(selectors2.targetSelector()).toEqual('some value');
});
Но этот подход не будет работать, поскольку targetSelector
получает ссылку selectors1.baseSelector
во время инициализации selectors2.js
, а макет присваивается selectors1.baseSelector
после него.
Сейчас я вижу 2 рабочих решения:
- Макет всего
selectors1.js
модуля сjest.mock
помощью, однако, это не сработает, если мне нужно будет изменитьselectors1.baseSelector
вывод для некоторых конкретных случаев - Оберните все селекторы зависимостей следующим образом:
export const targetSelector = createSelector([(state) => selectors1.baseSelector(state)], () => {...});
Но мне не очень нравится этот подход по очевидным причинам.
Итак, вопрос следующий: есть ли шанс правильно имитировать повторный выбор селекторов для модульного тестирования?
Комментарии:
1. Если я правильно понимаю,
selectors2
создается новый экземплярselectors1
, следовательно, это не тот экземпляр, который вы издевались с помощью метода spy, но вы бы хотели, чтобы это было? Одним из вариантов может быть использование внедрения зависимостей, чтобы вы могли инициализировать его с помощью издевательского экземпляра. Другим может быть имитация целевого селектора selector2 для возврата издевательского экземпляра instance1, но, похоже, вы пытаетесь избежать этого по соображениям масштабируемости. Я чувствую, что, возможно, я не понимаю всю область проблемы или, может быть, именно то, чтоcreateSelector
делаю. Вы уже коснулись использования макета модуля.
Ответ №1:
Дело в том, что повторный выбор основан на концепции композиции. Итак, вы создаете один селектор из многих других. На самом деле вам нужно протестировать не весь селектор, а последнюю функцию, которая выполняет эту работу. Если нет, тесты будут дублировать друг друга, как если бы у вас были тесты для selector1, а selector1 используется в selector2, затем вы автоматически тестируете их оба в тестах selector2.
Для достижения:
- меньше издевательств
- нет необходимости специально имитировать результат составных селекторов
- нет дублирования тестов
протестируйте только функцию результата селектора. Он доступен по selector.resultFunc
.
Так, например:
const selector2 = createSelector(selector1, (data) => ...);
// tests
const actual = selector2.resultFunc([returnOfSelector1Mock]);
const expected = [what we expect];
expect(actual).toEqual(expected)
Краткие сведения
Вместо того, чтобы тестировать всю композицию и дублировать одно и то же утверждение или имитировать конкретные выходные данные селекторов, мы тестируем функцию, которая определяет наш селектор, поэтому последний аргумент в createSelector, доступный по resultFunc
ключу.
Комментарии:
1. Да, это именно то, чего я хотел достичь. Спасибо за вашу помощь!
2. Кажется, что вы должны передавать
resultFunc
столько аргументов, сколько входных селекторов в вашем селекторе, вместо коллекции, в качестве одного аргумента. Поэтому вместоselector2.resultFunc([returnOfSelector1Mock]);
этого должно бытьselector2.resultFunc(returnOfSelector1Mock);
3. @classicalConditioning Я не думаю, что это какая-то проблема с наличием или отсутствием []
Ответ №2:
Вы могли бы добиться этого, издеваясь над всем selectors1.js
модулем, но также импортируя is inside test, чтобы иметь доступ к измененной функциональности
Предполагая, что ваш selectors1.js
выглядит как
import { createSelector } from 'reselect';
// selector
const getFoo = state => state.foo;
// reselect function
export const baseSelector = createSelector(
[getFoo],
foo => foo
);
и selectors2.js
выглядит так
import { createSelector } from 'reselect';
import selectors1 from './selectors1';
export const targetSelector = createSelector(
[selectors1.baseSelector],
foo => {
return foo.a;
}
);
Тогда вы могли бы написать какой-нибудь тест, например
import { baseSelector } from './selectors1';
import { targetSelector } from './selectors2';
// This mocking call will be hoisted to the top (before import)
jest.mock('./selectors1', () => ({
baseSelector: jest.fn()
}));
describe('selectors', () => {
test('foo.a = 1', () => {
const state = {
foo: {
a: 1
}
};
baseSelector.mockReturnValue({ a: 1 });
expect(targetSelector(state)).toBe(1);
});
test('foo.a = 2', () => {
const state = {
foo: {
a: 1
}
};
baseSelector.mockReturnValue({ a: 2 });
expect(targetSelector(state)).toBe(2);
});
});
jest.mock
вызов функции будет перенесен в верхнюю часть модуля, чтобы имитировать selectors1.js
модуль
Когда вы import
/ require
selectors1.js
, вы получите отредактированную версию baseSelector
, поведением которой вы можете управлять перед запуском теста
Комментарии:
1. Хм. Это интересное решение, однако я бы счел ответ Мацея Сикоры лучшим. Единственное, что я бы добавил — поместите запись jest.mock в beforeEach , иначе ваш макет не будет сбрасываться перед каждым тестом.
2. Я рад, что вы нашли ответ, который ищете. Однако вы не можете вставить
jest.mock
записьbeforeEach
, потому что, как я уже упоминал, jest поднимет этот вызов в верхнюю часть модуля. Чтобы сбросить макет перед каждым тестом, вы просто делаетеbaseSelector.mockClear
илиbaseSelector.mockReset
в зависимости от того, чего вы хотите достичь3. Хм. Это тоже не очень хорошее решение, потому что в некоторых файлах могут быть десятки селекторов. Тем не менее,
jest.resetModules()
inbeforeEach
должен сделать свое дело4. Вот почему мне не нравится функциональность jest mocking. Мне больше нравится sinon с proxyquire
5. Я согласен, что могут быть лучшие решения для издевательства, но в данном конкретном случае, как оказалось, есть возможность тестировать селекторы без издевательства. Кроме того, Jest не так уж плох. Это быстрая и довольно гибкая структура, но да, насмешку можно было бы сделать лучше. 🙂
Ответ №3:
Для тех, кто пытается решить эту проблему с помощью Typescript, этот пост — это то, что, наконец, сработало для меня: https://dev.to/terabaud/testing-with-jest-and-typescript-the-tricky-parts-1gnc
Моя проблема заключалась в том, что я тестировал модуль, который вызывал несколько разных селекторов в процессе создания запроса, и redux-mock-store
созданное мной состояние не было видно селекторам при выполнении тестов. В итоге я вообще пропустил макет хранилища, и вместо этого я издевался над возвращаемыми данными для конкретных селекторов, которые были вызваны.
Процесс заключается в следующем:
- Импортируйте селекторы и зарегистрируйте их как функции jest:
import { inputsSelector, outputsSelector } from "../store/selectors";
import { mockInputsData, mockOutputsData } from "../utils/test-data";
jest.mock("../store/selectors", () => ({
inputsSelector: jest.fn(),
outputsSelector: jest.fn(),
}));
- Затем используйте
.mockImplementation
вместе сmocked
помощником fromts-jestutils
, который позволяет вам переносить каждый селектор и предоставлять пользовательские возвращаемые данные для каждого из них.
beforeEach(() => {
mocked(inputsSelector).mockImplementation(() => {
return mockInputsData;
});
mocked(outputsSelector).mockImplementation(() => {
return mockOutputsData;
});
});
- Если вам нужно переопределить возвращаемое значение по умолчанию для селектора в конкретном тесте, вы можете сделать это внутри
test()
определения следующим образом:
test("returns empty list when output data is missing", () => {
mocked(outputsSelector).mockClear();
mocked(outputsSelector).mockImplementationOnce(() => {
return [];
});
// ... rest of your test code follows ...
});
Ответ №4:
Я столкнулся с той же проблемой. В итоге я издевался над reselect createSelector
with jest.mock
, чтобы игнорировать все, кроме последнего аргумента (который является основной функцией, которую вы хотите протестировать), когда дело доходит до тестирования. В целом этот подход сослужил мне хорошую службу.
Проблема, с которой я столкнулся
У меня была циклическая зависимость в моих модулях селектора. Наша кодовая база слишком велика, и у нас нет пропускной способности для их соответствующего рефакторинга.
Почему я использовал этот подход?
В нашей кодовой базе много циклических зависимостей в селекторах. И попытка переписать и реорганизовать их, чтобы не иметь циклических зависимостей, — это слишком много работы. Поэтому я решил имитировать createSelector
, чтобы мне не приходилось тратить время на рефакторинг.
Если ваша кодовая база для селекторов чиста и свободна от зависимостей, обязательно рассмотрите возможность использования reselect
resultFunc
. Дополнительная документация здесь: https://github.com/reduxjs/reselect#createselectorinputselectors—inputselectors-resultfunc
Код, который я использовал для моделирования createSelector
// Mock the reselect
// This mocking call will be hoisted to the top (before import)
jest.mock('reselect', () => ({
createSelector: (...params) => params[params.length - 1]
}));
Затем, чтобы получить доступ к созданному селектору, у меня было что-то вроде этого
const myFunc = TargetSelector.IsCurrentPhaseDraft;
Весь код набора тестов в действии
// Mock the reselect
// This mocking call will be hoisted to the top (before import)
jest.mock('reselect', () => ({
createSelector: (...params) => params[params.length - 1]
}));
import * as TargetSelector from './TicketFormStateSelectors';
import { FILTER_VALUES } from '../../AppConstant';
describe('TicketFormStateSelectors.IsCurrentPhaseDraft', () => {
const myFunc = TargetSelector.IsCurrentPhaseDraft;
it('Yes Scenario', () => {
expect(myFunc(FILTER_VALUES.PHASE_DRAFT)).toEqual(true);
});
it('No Scenario', () => {
expect(myFunc(FILTER_VALUES.PHASE_CLOSED)).toEqual(false);
expect(myFunc('')).toEqual(false);
});
});
Комментарии:
1. Зачем вам нужно имитировать createSelector, если вы можете использовать метод resultFunc селектора?
2. В нашей кодовой базе много циклических зависимостей, которые выполняют импорт
null
. Использование описанного выше подхода помогает сохранить код как есть и не трогать его. Я не согласен с тем, что использованиеresultFunc
здесь является лучшим подходом. Но если у вас нет выбора, когда дело доходит до рефакторинга, тогда мой подход имеет смысл.