有没有办法正确模拟 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');
});
但是,这种方法不会起作用,因为 targetSelector
在 selectors2.js
的初始化期间获取对 selectors1.baseSelector
的引用,并且在它之后将 mock 分配给 selectors1.baseSelector
。
我现在看到了 2 个可行的解决方案:
- 用
jest.mock
模拟整个 selectors1.js
模块,但是,如果我需要为某些特定情况更改 selectors1.baseSelector
输出,它将无法工作
- 像这样包装每个依赖选择器:
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\utils
的 mocked
助手,它可以让您包装每个选择器并为每个选择器提供自定义 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 ...
});
我的项目中有一个非常复杂的选择器结构(一些选择器可能有多达 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
在 selectors2.js
的初始化期间获取对 selectors1.baseSelector
的引用,并且在它之后将 mock 分配给 selectors1.baseSelector
。
我现在看到了 2 个可行的解决方案:
- 用
jest.mock
模拟整个selectors1.js
模块,但是,如果我需要为某些特定情况更改selectors1.baseSelector
输出,它将无法工作 - 像这样包装每个依赖选择器:
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\utils
的mocked
助手,它可以让您包装每个选择器并为每个选择器提供自定义 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 ...
});