如何使用Enzyme在React Router / Redux应用程序中对React组件方法的输出进行单元测试

时间:2019-03-01 17:12:13

标签: reactjs redux react-router jestjs enzyme

我有一个“容器组件”,DashboardContainer,它连接到redux商店。

在index.js中上一层,商店Provider包装了一个AppRouter组件,如下所示:

<Provider store={store}>
  <AppRouter />
</Provider>

AppRouter的设置如下:

const AppRouter = () => (
  <Router history={history}>
    <Switch>
      <PublicRoute exact path={ROUTES.LANDING} component={SignUpPage} />
      <PrivateRoute path={ROUTES.DASHBOARD} component={DashboardContainer} />
      <Route component={NotFoundPage} />
    </Switch>
  </Router>
);

因此,简单来说,我要测试的方法位于redux和router包装器下。

这是该方法的组成部分,我正在尝试测试:

import React, { Component } from "react";
import { connect } from "react-redux";
import Dashboard from "./Dashboard";
import PropTypes from "prop-types";
import moment from "moment";

class DashboardContainer extends Component {
  static propTypes = {
    dashboardDate: PropTypes.string.isRequired,
    exerciseLog: PropTypes.array.isRequired
  };

  componentDidMount() {
    this.props.setDashboardDate(moment().format());
  }
  getExerciseCalsForDay = () => {
    const { dashboardDate, exerciseLog } = this.props;
    const totalCals = exerciseLog
      .filter(entry => {
        return moment(dashboardDate).isSame(entry.date, "day");
      })
      .map(entry => {
        return entry.workouts
          .map(item => item.nf_calories || 0)
          .reduce((acc, curr) => acc + curr, 0);
      });
    return totalCals[0] || 0;
  };
  render() {
    return (
      <Dashboard
        exerciseCalsToday={this.getExerciseCalsForDay()}
        exerciseLog={this.props.exerciseLog}
      />
    );
  }
}

const mapStateToProps = state => ({
  dashboardDate: state.dashboard.dashboardDate,
  exerciseLog: state.exerciseLog
});

export default connect(mapStateToProps)(DashboardContainer);

一些注意事项:

  1. 我不是要测试React Router或Redux。
  2. 我没有在任何HOC中使用{withRouter}。
  3. 我不是要测试是否或如何调用该方法。
  4. 所有我要尝试的是查看方法是否返回正确的值(给定通过测试提供的props数据集)。
  5. 我在这里和Github上的Enzyme(例如 link),并认为我需要使用dive()。
  6. DashboardContainer实际上不渲染任何东西,除了 是孩子它根据从 redux存储,并将处理后的数据向下传递给子级 在那里呈现的“代表性”组件。
  7. 在子组件中进行测试无济于事,因为它们正在接收 计算出的值作为道具,可以正确呈现。

这是我要进行的测试:

import React from "react";
import { shallow } from "enzyme";
import DashboardContainer from "../../../components/Dashboard/DashboardContainer";
import data from "../../fixtures/ExerciseLogSeedData";

const props = {
  dashboardDate: "2019-03-01T19:07:17+07:00",
  foodLog: data
};

const wrapper = shallow(<DashboardContainer {...props} />);
const instance = wrapper.instance();

test("should correctly calculate exercise calories for the day", () => {
  expect(instance.getExerciseCalsForDay()).toBe(1501);
});

此测试的结果是:

TypeError: instance.getExerciseCalsForDay is not a function

如果我将实例的定义更改为:

const instance = wrapper.instance().dive();

我得到:

TypeError: wrapper.instance(...).dive is not a function

如果我将实例更改为:

const instance = wrapper.dive().instance();

我得到:

TypeError: ShallowWrapper::dive() can only be called on components

如果我尝试与此运行例外,则:

expect(instance.getExerciseCalsForDay).toBe(1501);

toBe()收到“未定义”。

如果我尝试使用mount而不是浅表,那么所有地狱都将丢失,因为我没有实现模拟存储,等等。

问题:缺少将方法直接复制到测试中(并使其成为函数)的方法,如何正确地针对这样的方法,以便能够针对它?在哪里潜水?还是我错过了这件事的一些基本方面?

2 个答案:

答案 0 :(得分:2)

Redux doc on writing tests建议以下内容:

  

为了能够测试App组件本身而不必处理装饰器,我们建议您还导出未装饰的组件。

将连接的组件导出为default导出以在应用程序中使用,并将组件本身导出为名为命名的导出进行测试:

export class DashboardContainer extends Component {  // named export
  ...
}

export default connect(mapStateToProps)(DashboardContainer);  // default export

然后在测试中导入以命名的导出(组件本身):

...
import { DashboardContainer } from "../../../components/Dashboard/DashboardContainer";
...

这使在单元测试中测试组件本身容易得多,在这种情况下,这似乎是测试工作所需的唯一更改。

答案 1 :(得分:1)

鉴于您的目标是对getExerciseCalsForDay方法进行单元测试,而不要使用Redux或React路由器,我强烈建议将getExerciseCalsForDay中的逻辑提取到纯JavaScript函数中< / p>

一旦将其提取出来,您就可以自己对其进行测试,而无需通过React。

然后,您可以将getExerciseCalsForDay导入到组件的index.js文件中,并从组件的方法中调用它:

import React, { Component } from "react";
import { connect } from "react-redux";
import Dashboard from "./Dashboard";
import PropTypes from "prop-types";
import moment from "moment";
import calculateExerciseCalsForDay from "calculateExerciseCalsForDay";

class DashboardContainer extends Component {
  static propTypes = {
    dashboardDate: PropTypes.string.isRequired,
    exerciseLog: PropTypes.array.isRequired
  };

  componentDidMount() {
    this.props.setDashboardDate(moment().format());
  }
  getExerciseCalsForDay = () => {
    const { dashboardDate, exerciseLog } = this.props;
    return calculateExerciseCalsForDay(dashboardDate, exerciseLog);
  };
  render() {
    return (
      <Dashboard
        exerciseCalsToday={this.getExerciseCalsForDay()}
        exerciseLog={this.props.exerciseLog}
      />
    );
  }
}

const mapStateToProps = state => ({
  dashboardDate: state.dashboard.dashboardDate,
  exerciseLog: state.exerciseLog
});

export default connect(mapStateToProps)(DashboardContainer);

calculateExerciseCalsForDay.js将包含:

export default function calculateExerciseCalsForDay(date, exerciseLog) {
  const totalCals = exerciseLog
    .filter(entry => {
      return moment(date).isSame(entry.date, "day");
    })
    .map(entry => {
      return entry.workouts
        .map(item => item.nf_calories || 0)
        .reduce((acc, curr) => acc + curr, 0);
    });
  return totalCals[0] || 0;
}

您的测试非常简单:

import calculateExerciseCalsForDay from "calculateExerciseCalsForDay";
import data from "../../fixtures/ExerciseLogSeedData";

const dashboardDate = "2019-03-01T19:07:17+07:00";
const foodLog = data;
};

test("should correctly calculate exercise calories for the day", () => {
  expect(
    calculateExerciseCalsForDay(dashboardDate, foodLog)
  ).toBe(1501);
});