Eugene Tsakh
Eugene Tsakh

Reputation: 2879

Is there a way to properly mock Reselect selectors for unit testing?

I'm having a pretty complex selectors structure in my project (some selectors may have up to 5 levels of nesting) so some of them are very hard to test with passing input state and I would like to mock input selectors instead. However I found that this is not really possible.

Here is the most simple example:

// selectors1.js
export const baseSelector = createSelector(...);

-

// selectors2.js
export const targetSelector = createSelector([selectors1.baseSelector], () => {...});

What I would like to have in my test suite:

beforeEach(() => {
  jest.spyOn(selectors1, 'baseSelector').mockReturnValue('some value');
});

test('My test', () => {
  expect(selectors2.targetSelector()).toEqual('some value');
});

But, this approach won't work as targetSelector is getting reference to selectors1.baseSelector during initialization of selectors2.js and mock is assigned to selectors1.baseSelector after it.

There are 2 working solutions I see now:

  1. Mock entire selectors1.js module with jest.mock, however, it won't work if I'll need to change selectors1.baseSelector output for some specific cases
  2. Wrap every dependency selectors like this:

export const targetSelector = createSelector([(state) => selectors1.baseSelector(state)], () => {...});

But I don't like this approach a lot for obvious reasons.

So, the question is next: is there any chance to mock Reselect selectors properly for unit testing?

Upvotes: 28

Views: 25565

Answers (5)

Nagibaba
Nagibaba

Reputation: 5368

In addition to the answer of @nate-peters I would like to share an even easier version:

import { baseSelector } from "../store/selectors";         

jest.mock("../store/selectors", () => ({
  baseSelector: jest.fn(),
}));

Then in every it(...) block you can implement

it('returns ...', () => {
    baseSelector.mockReturnValueOnce(values);
    //...rest of you code

Upvotes: 0

Nate Peters
Nate Peters

Reputation: 31

For anyone trying to solve this using Typescript, this post is what finally worked for me: https://dev.to/terabaud/testing-with-jest-and-typescript-the-tricky-parts-1gnc

My problem was that I was testing a module that called several different selectors in the process of creating a request, and the redux-mock-store state that I created wasn't visible to the selectors when the tests executed. I ended up skipping the mock store altogether, and instead I mocked the return data for the specific selectors that were called.

The process is this:

  • Import the selectors and register them as jest functions:
    import { inputsSelector, outputsSelector } from "../store/selectors";         
    import { mockInputsData, mockOutputsData } from "../utils/test-data";

    jest.mock("../store/selectors", () => ({
      inputsSelector: jest.fn(),
      outputsSelector: jest.fn(),
    }));
  • Then use .mockImplementation along with he mocked helper from ts-jest\utils, which lets you wrap each selector and give custom return data to each one.
    beforeEach(() => {
        mocked(inputsSelector).mockImplementation(() => {
            return mockInputsData;
        });
        mocked(outputsSelector).mockImplementation(() => {
            return mockOutputsData;
        });
    });
  • If you need to override the default return value for a selector in a specific test, you can do that inside the test() definition like this:
    test("returns empty list when output data is missing", () => {
        mocked(outputsSelector).mockClear();
        mocked(outputsSelector).mockImplementationOnce(() => {
            return [];
        });
        // ... rest of your test code follows ...
    });

Upvotes: 3

Sy Le
Sy Le

Reputation: 89

I ran into the same problem. I ended up mocking the reselect createSelector with jest.mock to ignore all but the last argument (which is the core function you want to test) when it comes to testing. Overall this approach has served me well.

Problem I had

I had a circular dependency within my selector modules. Our code base is too large and we don't have the bandwidth to refactor them accordingly.

Why I used this approach?

Our code base has a lot of circular dependencies in the Selectors. And trying to rewrite and refactor them to not have circular dependencies are too much work. Therefore I chose to mock createSelector so that I don't have to spend time refactoring.

If your code base for selectors are clean and free of dependencies, definitely look into using reselect resultFunc. More documentation here: https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc

Code I used to mock the createSelector

// Mock the reselect
// This mocking call will be hoisted to the top (before import)
jest.mock('reselect', () => ({
  createSelector: (...params) => params[params.length - 1]
}));

Then to access the created selector, I had something like this

const myFunc = TargetSelector.IsCurrentPhaseDraft;

The entire test suite code in action

// 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);
  });
});

Upvotes: -1

Maciej Sikora
Maciej Sikora

Reputation: 20162

The thing is that Reselect is based on the composition concept. So you create one selector from many others. What really you need to test is not the whole selector, but the last function which do the job. If not, the tests will duplicate each other, as if you have tests for selector1, and selector1 is used in selector2, then automatically you test both of them in selector2 tests.

In order to achieve:

  • less mocks
  • no need to specially mock result of composed selectors
  • no test duplication

test only the result function of the selector. It is accessible by selector.resultFunc.

So for example:

const selector2 = createSelector(selector1, (data) => ...);

// tests

const actual = selector2.resultFunc([returnOfSelector1Mock]);
const expected = [what we expect];
expect(actual).toEqual(expected)

Summary

Instead of testing the whole composition, and duplicating the same assertion, or mocking specific selectors outputs, we test the function which defines our selector, so the last argument in createSelector, accessible by resultFunc key.

Upvotes: 45

Mohamed Shaaban
Mohamed Shaaban

Reputation: 1169

You could achieve this by mocking the entire selectors1.js module, but also importing is inside test, in order to have access over the mocked functionality

Assuming your selectors1.js looks like

import { createSelector } from 'reselect';

// selector
const getFoo = state => state.foo;

// reselect function
export const baseSelector = createSelector(
  [getFoo],
  foo => foo
);

and selectors2.js looks like

import { createSelector } from 'reselect';
import selectors1 from './selectors1';

export const targetSelector = createSelector(
  [selectors1.baseSelector],
  foo => {
    return foo.a;
  }
);

Then you could write some test like

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 function call will be hoisted to the top of the module to mock the selectors1.js module When you import/require selectors1.js, you will get the mocked version of baseSelector which you can control its behaviour before running the test

Upvotes: 3

Related Questions