有没有办法正确模拟 Reselect 选择器以进行单元测试?

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

我的项目中有一个非常复杂的选择器结构(一些选择器可能有多达 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');
});

但是,这种方法不会起作用,因为 targetSelectorselectors2.js 的初始化期间获取对 selectors1.baseSelector 的引用,并且在它之后将 mock 分配给 selectors1.baseSelector

我现在看到了 2 个可行的解决方案:

  1. jest.mock 模拟整个 selectors1.js 模块,但是,如果我需要为某些特定情况更改 selectors1.baseSelector 输出,它将无法工作
  2. 像这样包装每个依赖选择器:

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

但出于显而易见的原因,我不太喜欢这种方法。

那么,接下来的问题是:是否有机会为单元测试正确地模拟 Reselect 选择器?

您可以通过模拟整个 selectors1.js 模块来实现这一点,但导入也在测试中,以便访问模拟功能

假设你的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 的模拟版本,您可以在 运行 测试

之前控制其行为

关键是Reselect是基于组合概念的。因此,您可以从许多其他选择器中创建一个选择器。你真正需要测试的不是整个选择器,而是完成这项工作的最后一个函数。否则,测试将相互重复,就好像您对 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 键访问。

我运行陷入同样的​​问题。在测试时,我最终用 jest.mock 模拟了 reselect createSelector 以忽略除最后一个参数(这是您要测试的核心功能)之外的所有参数。总的来说,这种方法对我很有帮助。

我遇到的问题

我的选择器模块中存在循环依赖。我们的代码库太大,我们没有足够的带宽来相应地重构它们。

为什么我使用这种方法?

我们的代码库在选择器中有很多循环依赖。并试图重写和重构它们以使其不具有循环依赖性是太多的工作。因此我选择了 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);
  });
});

对于任何尝试使用 Typescript 解决此问题的人来说,这个 post 最终对我有用: https://dev.to/terabaud/testing-with-jest-and-typescript-the-tricky-parts-1gnc

我的问题是我正在测试一个模块,该模块在创建请求的过程中调用了多个不同的选择器,而我创建的 redux-mock-store 状态在测试执行时对选择器不可见。我最终完全跳过了模拟存储,而是模拟了被调用的特定选择器的 return 数据。

过程是这样的:

  • 导入选择器并将它们注册为 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 和来自 ts-jest\utilsmocked 助手,它可以让您包装每个选择器并为每个选择器提供自定义 return 数据。
    beforeEach(() => {
        mocked(inputsSelector).mockImplementation(() => {
            return mockInputsData;
        });
        mocked(outputsSelector).mockImplementation(() => {
            return mockOutputsData;
        });
    });
  • 如果您需要在特定测试中覆盖选择器的默认 return 值,您可以在 test() 定义中这样做:
    test("returns empty list when output data is missing", () => {
        mocked(outputsSelector).mockClear();
        mocked(outputsSelector).mockImplementationOnce(() => {
            return [];
        });
        // ... rest of your test code follows ...
    });