【问题标题】:How to unit test the output of a React component method in a React Router / Redux app with Enzyme如何使用 Enzyme 在 React Router/Redux 应用程序中对 React 组件方法的输出进行单元测试
【发布时间】:2019-07-23 18:22:19
【问题描述】:

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

在 index.js 的上一层,store 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 和路由器包装器之下。

这是带有方法的组件,我正在尝试测试:

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. 所有我想做的是看看方法是否返回正确的值,给定一个通过道具提供的数据集,这些道具是在测试中提供的。
  5. 我在这里和 Github repo 上阅读了几篇关于 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

如果我尝试用这个来运行 except:

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

toBe() 接收“未定义”。

如果我尝试使用 mount 而不是 shallow ,所有的地狱都会失败,因为我还没有实现模拟存储等。

问题:没有将方法直接复制到测试中(并使其成为函数),如何正确定位这样的方法以便能够运行期望/toBe它?是潜水的地方吗?还是我错过了整个事情的一些基本方面?

【问题讨论】:

    标签: reactjs redux react-router jestjs enzyme


    【解决方案1】:

    Reduxdoc 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";
    ...
    

    这使得在您的单元测试中测试组件本身变得更加容易,在这种情况下,看起来这是您的测试工作所需的唯一更改。

    【讨论】:

    • 嗨,布赖恩。感谢您的深思熟虑,创造性的回应。虽然我选择将这些功能移到它们自己的。我认为这个解决方案在某些情况下可能会派上用场,我会将这个新技巧保留在我的工具包中。感谢您在这里分享您的智慧。
    • @Kim,仅供参考,我将link 的答案更新为Redux 文档,这是为连接组件编写测试的推荐方法。
    • 谢谢。是的,在做这个的时候,我找到了那篇确切的文章,当我找到的时候,我想,“这就是布赖恩说的!”。所以,是的,在有和没有 Redux 的情况下导出类/函数允许从两个角度进行测试,这非常有帮助。感谢您的跟进。
    • @Kim 太棒了,很高兴听到它有帮助!
    【解决方案2】:

    鉴于您的目标是单元测试 getExerciseCalsForDay 方法而不是 Redux 或 React 路由器,我强烈建议将 getExerciseCalsForDay 中的逻辑提取到纯 JavaScript 函数中

    一旦它被提取出来,你就可以自己测试它,而无需通过 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);
    });
    

    【讨论】:

    • 嗨,Fito,感谢您做出如此出色、清晰的回复。我一直打算将这些作为独立功能拉出来,您的回复是我需要这样做的动力。这样做也暴露了一些之前似乎漏掉的缺陷,例如没有为来自 state 的值设置默认值,这会在过滤器、映射或任何期望数组工作的东西时导致崩溃。无论如何,我很感谢你在这方面的帮助。
    • 太棒了,很高兴你发现我的建议有帮助
    猜你喜欢
    • 2018-10-19
    • 1970-01-01
    • 2016-10-14
    • 2020-05-12
    • 2016-05-09
    • 2016-02-27
    • 1970-01-01
    • 2022-06-15
    • 2019-08-01
    相关资源
    最近更新 更多