下面的实现是我作为一个软件开发者的日常工作中的一个快速摘录。如果我遇到一个问题,并得出一个我认为值得分享的例子,我将把代码的要点放在这个网站上。它可能对其他偶然遇到相同任务的人有用。
连接到Redux的React组件可能会变得相当复杂。因此,大多数人认为,测试这些复杂的组件也会变得非常复杂。但是,如果你在React组件的集成/单元测试中完全控制了Redux存储,那就根本不应该是复杂的。
在这个React Redux组件的简短测试教程中,我将只使用Jest。然而,你可以用React测试库或Enzyme扩展你的测试设置,以渲染和模拟事件。Jest只给你提供了执行这项任务的基本条件。
假设我们有一个连接的React组件,从Redux商店接收状态。我们把这种状态--以道具的形式进入我们的组件--称为myState 。我们的组件也有一个按钮元素,它向我们的Redux商店分派一个动作。我们称这个动作为myAction(payload) ,而`payload 可以是传递给该动作的任何参数。总之,我们的React组件以两种方式与Redux存储相连:它接收状态(例如通过mapStateToProps)和分派行动(例如通过mapDispatchToProps)。
Redux State -> React Component -> Redux Action
想象一下以下场景。React组件在myState ,以便在渲染时填充一个HTML输入字段。用户可以改变输入字段中的值,一旦用户点击按钮,改变的值就会作为有效载荷发送到myAction 。现在,我们可以在一个测试套件中用两个测试用例测试连接的React组件的两端:
describe('My Connected React-Redux Component', () => {
it('should render with given state from Redux store', () => {
});
it('should dispatch an action on button click', () => {
});
});
为了完全控制Redux存储,我们将使用一个Redux特定的测试库,称为Redux Mock Store。如果你还没有安装它,你可以在命令行中这样做。
npm install redux-mock-store --save-dev
由于这种模拟,我们没有充分的信心,我们的组件将在与非模拟的Redux商店的整合中工作,然而,你的其他测试应该确保在实际的Redux商店中的所有行动/还原器/传奇都能按预期工作。这就是为什么我们将在这种情况下模拟Redux商店,只对我们连接的react-redux组件运行单元测试。
Redux State (Mock) -> React Component (Unit Test) -> Redux Action (Mock)
让我们看看我们如何在单元测试中设置Redux模拟商店:
import configureStore from 'redux-mock-store';
const mockStore = configureStore([]);
describe('My Connected React-Redux Component', () => { let store;
beforeEach(() => { store = mockStore({ myState: 'sample text', }); });
it('should render with given state from Redux store', () => {
});
it('should dispatch an action on button click', () => {
});});
你传入mockStore 的一切将是你的Redux商店的初始状态。因此,确保你提供你的连接的React组件所需要的一切,以便顺利渲染。接下来,用你选择的渲染器为你的测试创建React组件:
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import MyConnectedComponent from '.';
const mockStore = configureStore([]);
describe('My Connected React-Redux Component', () => {
let store;
let component;
beforeEach(() => {
store = mockStore({
myState: 'sample text',
});
component = renderer.create(
<Provider store={store}>
<MyConnectedComponent />
</Provider>
);
});
it('should render with given state from Redux store', () => {
});
it('should dispatch an action on button click', () => {
});
});
你可以从实际的react-redux库中看到模拟的Redux商店是如何用于包装Provider的。因此,为了这个测试的目的,模拟的Redux商店是为你的React组件提供的。对于你的第一个单元测试,你可以做的最简单的事情是对渲染的组件进行快照测试。
...
...
describe('My Connected React-Redux Component', () => {
let store;
let component;
beforeEach(() => {
store = mockStore({
myState: 'sample text',
});
component = renderer.create(
<Provider store={store}>
<MyConnectedComponent />
</Provider>
);
});
it('should render with given state from Redux store', () => {
expect(component.toJSON()).toMatchSnapshot();
});
it('should dispatch an action on button click', () => {
});
});
检查快照测试的输出,看是否所有的东西都按照预期的状态从模拟的Redux存储中得到了渲染。当然,你可以在这个测试案例中更加明确,不仅检查渲染的快照,还可以明确地检查某些元素是否已经用Redux商店的给定状态渲染。例如,你可以检查一个给定的HTML输入字段是否接收了Redux商店的状态作为其初始状态。
现在,对于你的第二个单元测试,你将用Jest检查一个HTML按钮的点击是否会派遣一个特定的Redux动作。因此,你将不得不为Redux商店的调度函数引入一个间谍,你将不得不模拟给定按钮的点击事件。如前所述,这取决于你如何监视一个函数和如何模拟一个事件 -- 在我们的例子中,我们将使用Jest来处理这两种情况。
...
...
import MyConnectedComponent from '.';
import { myAction } from './actions'
...
describe('My Connected React-Redux Component', () => {
let store;
let component;
beforeEach(() => {
store = mockStore({
myState: 'sample text',
});
store.dispatch = jest.fn();
component = renderer.create(
<Provider store={store}>
<MyConnectedComponent />
</Provider>
);
});
it('should render with given state from Redux store', () => {
expect(component.toJSON()).toMatchSnapshot();
});
it('should dispatch an action on button click', () => {
renderer.act(() => {
component.root.findByType('button').props.onClick();
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(
myAction({ payload: 'sample text' })
);
});
});
使用Jest,我们模拟按钮上的点击事件,并期望Redux商店的调度函数被调用一次,并从我们所需的Redux动作返回值。
重要提示:一定要确保在测试中清理你的mock,否则另一个测试可能会运行到一个被模拟的函数。你可以在Jest中单独清除mock,就像前面的代码片段所展示的那样,也可以通过在jest.config.json文件中设置clearMocks 标志为true来全局清除。这将在每次测试后清除所有模拟,而不会留下任何僵尸模拟。
奖励如果你需要在两者之间模拟其他事件,例如填写一个表单,你可以简单地这样做。
describe('My Connected React-Redux Component', () => {
...
it('should dispatch an action on button click', () => {
renderer.act(() => {
component.root.findByType('button').props.onClick();
});
renderer.act(() => {
component.root.findByType('input')
.props.onChange({ target: { value: 'some other text' } });
});
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(
myAction({ payload: 'some other text' })
);
});
});
例如,在这种情况下,我们假设输入字段更新组件的内部状态,一旦按钮被点击,这个状态,在这种情况下,"其他一些文本 "的值,被作为调度动作发送到模拟的Redux商店。
最终,这已经是测试连接react-redux组件的第二部分了:
-
- 提供状态 -> React组件(单元测试) => 组件渲染
-
- React组件(单元测试) -> 模拟事件 => 派遣动作触发器
有很多方法可以测试知道Redux商店的连接React组件。使用Jest Mock来测试函数(例如Redux dispatch函数)和Redux Store Mock来伪造接收状态,只是对这类组件进行单元测试的一种方法。其他方法试图将他们的Redux商店完全整合到他们的测试方程中,或者用Jest模拟react-redux连接的高阶组件。无论如何,你现在可以把这个学到的测试方法添加到你的React单元测试最佳实践的工具带中。