Есть ли способ правильно имитировать повторный выбор селекторов для модульного тестирования?

#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 рабочих решения:

  1. Макет всего selectors1.js модуля с jest.mock помощью, однако, это не сработает, если мне нужно будет изменить selectors1.baseSelector вывод для некоторых конкретных случаев
  2. Оберните все селекторы зависимостей следующим образом:

 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() in beforeEach должен сделать свое дело

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 помощником from ts-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 здесь является лучшим подходом. Но если у вас нет выбора, когда дело доходит до рефакторинга, тогда мой подход имеет смысл.