1
votes

I've got a "container component", DashboardContainer, that is connected to a redux store.

One level up in index.js, the store Provider wraps an AppRouter component, like this:

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

The AppRouter setup is like this:

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

So, in brief, the method I'm trying to test is nestled down under the redux and router wrappers.

Here's the component with the method, I'm trying to test:

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);

A few of notes:

  1. I'm not trying to test React Router or Redux.
  2. I'm not using {withRouter} in any HOCs.
  3. I'm not trying to test if or how the method has been called.
  4. All I'm trying to do is see if the method returns the correct value, given a data set via props, which are provided inside the test.
  5. I've read several posts here and on the Github repo for Enzyme (e.g link), and believe I need to use dive().
  6. The DashboardContainer doesn't actually render anything except for it's children. It performs calculations on data received from the redux store, and passes this processed data down to the child "presentational" components for rendering there.
  7. Testing in the child components won't help, as they are receiving the calculated values as props, which render correctly.

Here's the test I'm battling:

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);
});

The result of this this test is:

TypeError: instance.getExerciseCalsForDay is not a function

If I change the definition of instance to:

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

I get:

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

If I change the instance to:

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

I get:

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

If I try to run the except with this:

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

toBe() receives "undefined".

If I try to try to use mount, instead of shallow, all hell breaks lose, as I've not implemented a mock store, etc.

QUESTION: Short of copying the method directly into the test (and making it a function), how does one properly target a method like this so as to be able to run an expect/toBe against it? Is it where to dive? Or have I missed some fundamental aspect of this whole thing?

2

2 Answers

2
votes

The Redux doc on writing tests recommends the following:

In order to be able to test the App component itself without having to deal with the decorator, we recommend you to also export the undecorated component.

Export the connected component as the default export for use in the app, and the component itself as a named export for testing:

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

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

Then import the named export (the component itself) in your test:

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

That makes it much easier to test the component itself in your unit tests, and in this case it looks like that is the only change needed for your test to work.

2
votes

Given that your goal is to unit test the getExerciseCalsForDay method and NOT Redux or React router, I highly recommend extracting the logic inside getExerciseCalsForDay into a pure JavaScript function

Once it's been extracted you can test it on its own, without having to go through React.

You can then import getExerciseCalsForDay into your component's index.js file, and call it from within the component's method:

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);

And calculateExerciseCalsForDay.js would contain:

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;
}

Your test then is very simple:

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);
});