0
votes

I am learning taking this testing course to test connected components by setting up a store factory test helper that 'creates a store for testing that matches the configuration of our store'. Below, you can see my connected sample component as well as the code used to setup tests, in which I create a connected, shallow enzyme wrapper of my sample component. However, it seems like the initial state I am passing to the sample component, in this case {jotto: 'foo'} is not getting passed to my sample component when creating this shallow wrapper. Am I doing something wrong and how can I correctly recreate the necessary store configuration when running enzyme tests? Thanks!

Sample Component:

import React from 'react';
import {connect} from 'react-redux';

const SampleComponent = (props) => {
  console.log(props);
  return (
    <div>This is a sample component!</div>
  );
};

const mapStateToProps = (state) => ({
  jotto: state.jotto,
});

export default connect(mapStateToProps)(SampleComponent);

reducer:

import * as jottoActionTypes from 'actionTypes/jottoActionTypes';

export const initialState = {
  isSuccess: false,
};

const jotto = (state = initialState, action) => {
  switch (action.type) {
    case jottoActionTypes.CORRECT_GUESS:
      return {
        ...state,
        isSuccess: true,
      };
    default:
      return state;
  }
};

export default jotto;

root reducer:

import {combineReducers} from 'redux';
import {connectRouter} from 'connected-react-router';
import jotto from 'reducers/jottoReducer';

export default (historyObject) => combineReducers({
  jotto,
  router: connectRouter(historyObject),
});

Setting up test:

import React from 'react';
import {shallow} from 'enzyme';
import {createStore} from 'redux';
import rootReducer from 'reducers/rootReducer';
import SampleComponent from './sampleComponent';

export const storeFactory = (initialState) => createStore(rootReducer, initialState);

const store = storeFactory({jotto: 'foo'});
const wrapper = shallow(<SampleComponent store={store} />).dive();
console.log(wrapper.debug());

// Result:
      { store:
         { dispatch: [Function: dispatch],
           subscribe: [Function: subscribe],
           getState: [Function: getState],
           replaceReducer: [Function: replaceReducer],
           [Symbol(observable)]: [Function: observable] },
        jotto: undefined,
        dispatch: [Function: dispatch],
        storeSubscription:
         Subscription {
           store:
            { dispatch: [Function: dispatch],
              subscribe: [Function: subscribe],
              getState: [Function: getState],
              replaceReducer: [Function: replaceReducer],
              [Symbol(observable)]: [Function: observable] },
           parentSub: undefined,
           onStateChange: [Function: bound onStateChange],
           unsubscribe: [Function: unsubscribe],
           listeners:
            { clear: [Function: clear],
              notify: [Function: notify],
              get: [Function: get],
              subscribe: [Function: subscribe] } } }
2

2 Answers

2
votes

Just a heads up about that Udemy course... it's not the greatest learning tool. The instructor approaches testing using data attributes which are unnecessary for jest and enzyme testing (they also crowd up the DOM with unused attributes).

In addition, her code experience is around a beginner level and she makes quite a few mistakes and odd code choices. That said, learn what you can from it and start looking into tests created by those who maintain popular npm packages (most well-documented and popular packages will contain tests that'll teach you a more practical approach of unit and integration testing).

Anyway, I digress, you have two options for testing a container:

  1. export the class/pure function, shallow or mount wrap it, and update it with fake props (very easy, less of a headache, and more common to do)
  2. Wrap your component in a redux <Provider> and react-router-dom's <MemoryRouter>, and then mount it (can become very complex as it requires a semi-deep understanding of: enzyme and how it interprets the DOM when a component is mounted, redux's action/reducer flow, how to create mock implementations and/or mock files, and how to properly handle promise based actions).

Working examples (click the Tests tab to run the tests; locate the .tests.js in the directories mentioned below):

Edit Testing Redux Component


Note: Codesandbox currently has some testing limitations as noted below, so please adjust for your local project.

containers/Dashboard/__tests__/UnconnectedDashboard.test.js (you can just as easily mount wrap this unconnected component to assert against its deeply nested children nodes)

import { Dashboard } from "../index.js";

/* 
   codesandbox doesn't currently support mocking, so it's making real
   calls to the API; as a result, the lifecycle methods have been
   disabled to prevent this, and that's why I'm manually calling
   componentDidMount.
*/

const getCurrentProfile = jest.fn();

const fakeUser = {
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "[email protected]",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets"
  }
};

const initialProps = {
  getCurrentProfile,
  currentUser: {},
  isLoading: true
};

describe("Unconnected Dashboard Component", () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallow(<Dashboard {...initialProps} />);
    wrapper.instance().componentDidMount();
  });

  afterEach(() => wrapper.unmount());

  it("initially renders a spinnner", () => {
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("Spinner")).toHaveLength(1);
  });

  it("displays the current user", () => {
    wrapper.setProps({ currentUser: fakeUser, isLoading: false });
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("DisplayUser")).toHaveLength(1);
  });

  it("displays a signup message if no users exist", () => {
    wrapper.setProps({ isLoading: false });
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
  });
});

containers/Dashboard/__tests__/ConnectedDashboard.test.js

import Dashboard from "../index";
// import { getCurrentProfile } from "../../../actions/profileActions";
import * as types from "../../../types";

/* 
  codesandbox doesn't currently support mocking, so it's making real
  calls to the API; however, actions like getCurrentProfile, should be
  mocked as shown below -- in your case, you wouldn't need to use
  a promise, but instead just mock the "guessedWord" action and return
  store.dispatch({ ... })
*/

const fakeUser = {
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "[email protected]",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets"
  }
};

const flushPromises = () => new Promise(resolve => setImmediate(resolve));

describe("Connected Dashboard Component", () => {
  let store;
  let wrapper;
  beforeEach(() => {
    store = createStoreFactory();
    wrapper = mount(
      <Provider store={store}>
        <MemoryRouter>
          <Dashboard />
        </MemoryRouter>
      </Provider>
    );
  });

  afterEach(() => wrapper.unmount());

  it("initially displays a spinner", () => {
    expect(wrapper.find("Spinner")).toHaveLength(1);
  });

  it("displays the current user after a successful API call", async () => {
    /* 
      getCurrentProfile.mockImplementationOnce(() => new Promise(resolve => {
        resolve(
          store.dispatch({
            type: types.SET_SIGNEDIN_USER,
            payload: fakeUser
          })
        );
      });

      await flushPromises();
      wrapper.update();

      expect(wrapper.find("DisplayUser")).toHaveLength(1);
    */

    store.dispatch({
      type: types.SET_SIGNEDIN_USER,
      payload: fakeUser
    });

    wrapper.update();

    expect(wrapper.find("DisplayUser")).toHaveLength(1);
  });

  it("displays a signup message if no users exist", async () => {
    /* 
      getCurrentProfile.mockImplementationOnce(() => new Promise((resolve,reject) => {
        reject(
          store.dispatch({
            type: types.FAILED_SIGNEDIN_USER
          })
        );
      });

      await flushPromises();
      wrapper.update();

      expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
    */

    store.dispatch({
      type: types.FAILED_SIGNEDIN_USER
    });

    wrapper.update();

    expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
  });
});
1
votes

Solution: I forgot the browser parameter for my root reducer, given I was using connected-react-router.

import rootReducer from 'reducers/rootReducer';
import {createBrowserHistory} from 'history';


export const storeFactory = (initialState) => createStore(rootReducer(createBrowserHistory()), initialState);