DEV Community

David Moore
David Moore

Posted on • Originally published at dmohq.com on

Using fixtures for testing a React/Redux app (with Jest & Enzyme)

I love testing because it helps me understand better the code I write. One particular problem it solves is how I expect data that I’m fetching to render. Since the same data often gets passed around to multiple functions, I find using fixtures a to be a really useful way to confirm that everything is working as expected. I’ve put together what I think is a practical demonstration below.

Let’s assume the following

  • We have an endpoint GET /users
  • We want to render a list of users with a response from the endpoint
  • We are going to use redux to manage the state of our app
  • We want to test all the things (reducer, actions, components and containers) with jest and enzyme

You’ll need to have some familiarity with redux including async actions and thunk.

If you have trouble with the portions of this article that involve redux, the docs are really well written.

Step 1 – Setup

For this post you can either create your own project from scratch or refer to the Github repo

  1. Install yarn
  2. Install create-react-app
  3. Use create-react-app to create your app
  4. Change to the root of your new project and install dependenices yarn add axios redux redux-thunk yarn add -D axios-mock-adapter enzyme enzyme-adapter-react-16 react-test-renderer redux-mock-store
  5. Create a global setup file src/setupTests.js and the following enzyme configuration:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
Enter fullscreen mode Exit fullscreen mode
  1. Last we’ll add a .env file in the root of the project and add a couple of environment variables.
    • NODE_PATH – Makes importing files easier.
    • REACT_APP_BASE_URL – Since we often use different servers for different environments we want to set the base url to whatever server we use for development. I’ll be using http://localhost:3001 NODE_PATH=src/ REACT_APP_BASE_URL=http://localhost:3001

Step 2 – Generate a snapshot with static data

In order to fetch and render data in our app we need to answer a couple of questions:

– What data do we get from our endpoint

– How is that data being rendered in our app?

Our endpoint GET /users returns an array of users.

[
  {
    "id": 1,
    "first_name": "Diana",
    "last_name": "Prince",
    "email": "dianaprince@flatley.com",
    "nickname": "Wonder Woman",
    "created_at": "2018-07-25T22:18:13.337Z",
    "updated_at": "2018-07-25T22:18:13.337Z"
  },
  {
    "id": 2,
    "first_name": "Bruce",
    "last_name": "Wayne",
    "email": "brucewayne@cummerata.com",
    "nickname": "Batman",
    "created_at": "2018-07-25T22:18:13.340Z",
    "updated_at": "2018-07-25T22:18:13.340Z"
  }
]
Enter fullscreen mode Exit fullscreen mode

Let’s create component with static values we want to render based on some of the data in the response:

// src/components/UserList.jsx

import React from "react";

const UserList = () => (
    <table>
      <thead>
        <tr>
          <td>Full Name</td>
          <td>Email</td>
          <td>Nickname</td>
        </tr>
      </thead>
      <tbody>
        <tr className="User">
          <td>Diana Prince</td>
          <td>dianaprince@flatley.com</td>
          <td>Wonder Woman</td>
        </tr>
        <tr className="User">
          <td>Bruce Wayne</td>
          <td>brucewayne@cummerata.com</td>
          <td>Batman</td>
        </tr>
      </tbody>
    </table>
  );

export default UserList
Enter fullscreen mode Exit fullscreen mode

Let’s create a a couple of tests. One tells us how many user rows we expect and the second is a snapshot test. Having these test in place early helps guide the refactoring and catches us from making any unwanted changes to the “markup” in our component.

// src/ __tests__ /UserList.test.jsx
import React from "react";
import UserList from "components/UserList";

import renderer from "react-test-renderer";

describe("UserList", () => {
  it("displays a list of users", () => {        
    const tree = renderer.create(<UserList/>).toJSON();

    expect(tree).toMatchSnapshot();
  });

  it("renders a list of rows with users", () => {
    const componentWrapper = shallow(<UserList />);
    const numberOfUserRows = componentWrapper.find("tr.User").length;

    expect(numberOfUserRows).toEqual(2);
    });
});
Enter fullscreen mode Exit fullscreen mode

Step 3 – Create our reducer

Let’s take a step back and conceptualize the data flow and how things will come together.

  • We’ll fetch some users by dispatching an action. It would be named fetchUsers or something similiar
  • When we receive the users we’ll pass those to a users reducer
  • The users reducer will transform the data from the action into an array of users that is “shaped” like the array of users we used in our test
  • That array of users will eventually get passed to a UsersList component to be rendered.

Let’s build a test to define our reducers behavior.

// __tests__ /usersReducer.test.js
Enter fullscreen mode Exit fullscreen mode

We have two important pieces of data to help us test further:

  • Our example response
  • A users array based on that response we pass to our UserList component

Our test wUserListContainer like this:

import users from "reducers/users";

describe("users reducer", () => {
  it("handles a RECEIVED_USERS action", () => {
    const action = {
      type: "RECEIVED_USERS",
      data: [
        {
          id: 1,
          first_name: "Diana",
          last_name: "Prince",
          email: "dianaprince@flatley.com",
          nickname: "Wonder Woman",
          created_at: "2018-07-25T22:18:13.337Z",
          updated_at: "2018-07-25T22:18:13.337Z"
        },
        {
          id: 2,
          first_name: "Bruce",
          last_name: "Wayne",
          email: "brucewayne@cummerata.com",
          nickname: "Batman",
          created_at: "2018-07-25T22:18:13.340Z",
          updated_at: "2018-07-25T22:18:13.340Z"
        }
      ]
    };

    const result = users(null, action);

    expect(result.users).toEqual([
      {
        id: 1,
        first_name: "Diana",
        last_name: "Prince",
        email: "dianaprince@flatley.com",
        nickname: "Wonder Woman"
      },
      {
        id: 2,
        first_name: "Bruce",
        last_name: "Wayne",
        email: "brucewayne@cummerata.com",
        nickname: "Batman"
      }
    ]);
  });
});
Enter fullscreen mode Exit fullscreen mode

And our reducer

// src/reducers/user.js
const initialState = {
  users: []
};

const receivedUsers = (state, data) => {
  const users = data.map(user => {
    const { id, first_name, last_name, email, nickname } = user;
    return { id, first_name, last_name, email, nickname };
  });
  return { ...state, users };
};

const users = (state = initialState, action) => {
  switch (action.type) {
    case "RECEIVED_USERS":
      return receivedUsers(state, action.data);
    default:
      return state;  
  }
};

export default users;
Enter fullscreen mode Exit fullscreen mode

Let’s also update our index.js file to use redux

// src/index.js

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import thunkMiddleware from "redux-thunk";
import { applyMiddleware, combineReducers, createStore } from "redux";

import users from "reducers/users";
import "./index.css";
import App from "./components/App";
import registerServiceWorker from "./registerServiceWorker";

const appReducer = combineReducers({
  users
});

let store = createStore(appReducer, applyMiddleware(thunkMiddleware));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
registerServiceWorker();
Enter fullscreen mode Exit fullscreen mode

Step 4 Extract fixtures

You might have noticed we are repeating ourselves in our tests

– The UserList component gets a users array

– The same users array is the result of our reducer test.

Let’s extract the users array to a fixture.

You can put your fixtures wherever you want, I use a folder like src/ __fixtures__.

// src/ __fixtures__ /reducedUsers.js
const reducedUsers = [
  {
    id: 1,
    first_name: "Diana",
    last_name: "Prince",
    email: "dianaprince@flatley.com",
    nickname: "Wonder Woman"
  },
  {
    id: 2,
    first_name: "Bruce",
    last_name: "Wayne",
    email: "brucewayne@cummerata.com",
    nickname: "Batman"
  }
];

export default reducedUsers;
Enter fullscreen mode Exit fullscreen mode

We are using the response data in our reducer test and we’ll use it in our user actions test later as well. So we should make a fixture for it too.

// src/ __fixtures__ /getUsersResponse.js

const getUsersResponse = [
  {
    id: 1,
    first_name: "Diana",
    last_name: "Prince",
    email: "dianaprince@flatley.com",
    nickname: "Wonder Woman",
    created_at: "2018-07-25T22:18:13.337Z",
    updated_at: "2018-07-25T22:18:13.337Z"
  },
  {
    id: 2,
    first_name: "Bruce",
    last_name: "Wayne",
    email: "brucewayne@cummerata.com",
    nickname: "Batman",
    created_at: "2018-07-25T22:18:13.340Z",
    updated_at: "2018-07-25T22:18:13.340Z"
  }
];

export default getUsersResponse;
Enter fullscreen mode Exit fullscreen mode
  • Let’s update our reducer test
import users from "reducers/users";
import reducedUsers from " __fixtures__ /reducedUsers";
import getUsersResponse from " __fixtures__ /getUsersResponse";

describe("users reducer", () => {
  it("handles a RECEIVED_USERS action", () => {
    const action = {
      type: "RECEIVED_USERS",
      data: getUsersResponse
    };

    const result = users(null, action);

    expect(result.users).toEqual(reducedUsers);
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Let’s also update our UserList test. Again this should not require any change to our snapshot test. Simply refactoring shouldn’t render things differently.
import React from "react";
import { shallow } from "enzyme";
import renderer from "react-test-renderer";

import UserList from "components/UserList";
import reducedUsers from " __fixtures__ /reducedUsers";

describe("UserList", () => {
  it("renders correctly", () => {
    const tree = renderer.create(<UserList users={reducedUsers} />).toJSON();

    expect(tree).toMatchSnapshot();
  });

  it("renders a list of rows with users", () => {
    const componentWrapper = shallow(<UserList users={reducedUsers} />);
    const numberOfUserRows = componentWrapper.find("tr.User").length;

    expect(numberOfUserRows).toEqual(2);
  });
});
Enter fullscreen mode Exit fullscreen mode

You might be thinking, “but if I change the fixture now I have to update every test that uses it”. That is exactly the point.

If what is returned from the reducer changes it would affect our UserList component. Our tests might break which informs us we may need to handle changes in the data.

Step 5 Add redux actions

Our user actions test will make user of our getUsersResponse fixture

import axios from "axios";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import MockAdapter from "axios-mock-adapter";

import { fetchUsers } from "actions/users";
import getUsersResponse from " __fixtures__ /getUsersResponse";

const axiosMock = new MockAdapter(axios);
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe("actions", () => {
  afterEach(() => {
    axiosMock.reset();
  });

  describe("fetchUsers", () => {
    it("should make an http request for users", () => {
      const uri = "http://localhost/users.json";
      axiosMock.onGet(uri).reply(200, getUsersResponse);

      const receiveUsersAction = {
        type: "RECEIVED_USERS",
        data: getUsersResponse
      };

      const store = mockStore({ users: [] });

      store.dispatch(fetchUsers(uri)).then(() => {
        const result = store.getActions();

        expect(result).toMatchObject([receiveUsersAction]);
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

And our users actions

// actions/users.js
import axios from "axios";

const fetchUsers = uri => {
  return dispatch =>
    axios.get(uri).then(response => dispatch(receivedUsers(response.data)));
};

const receivedUsers = data => {
  return {
    type: "RECEIVED_USERS",
    data
  };
};

export { fetchUsers };
Enter fullscreen mode Exit fullscreen mode

Step 6 Integrate redux and react

It’s helpful to separate containers for fetching data from components for rendering the fetched data .

So the last major step is to create a UserListContainer to fetch users and pass the result on to the UsersList component.

We’ll import the UserListContainer instead of the default export which is the

UserListContainer wrapped with redux. We’ll also mock out our fetchUsers

function since we don’t want to actually test the endpoint.

Our example tests will define expected behavior for two scenarios.

– When users were successfully fetched and passed on to the UserList component

– When the users array is empty

// __tests__ /UserListContainer.test.js
import React from "react";
import {shallow} from "enzyme";

import {UserListContainer} from "containers/UserListContainer";
import reducedUsers from " __fixtures__ /reducedUsers";

describe("UserListContainer", () => {
  it("displays the UsersList component when it has fetched users", () => {

    const props = {
      fetchUsers: jest.fn(),
      users: reducedUsers
    };

    const container = shallow(<UserListContainer {...props} />);
    const userListComponent = container.find('UserList').length;

    expect(userListComponent).toEqual(1)
  });

  it("does not display the UserList when ther are no users", () => {
    const props = {
      fetchUsers: jest.fn(),
      users: []
    };

    const container = shallow(<UserListContainer {...props} />);
    const userListComponentLength = container.find('UserList').length;

    expect(userListComponentLength).toEqual(0)
  });
});
Enter fullscreen mode Exit fullscreen mode

Finally our UserListContainer

// src/containers/UserListContainer.jsx

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

import UserList from "components/UserList";
import * as userActions from "actions/users";

// REACT_APP_BASE_URL stored in our .env file
const GET_USERS_URL = `${process.env.REACT_APP_BASE_URL}/users.json`;

export class UserListContainer extends React.Component {
  componentDidMount() {
    const { fetchUsers } = this.props;

    fetchUsers(GET_USERS_URL);
  }

  render() {
    const { users } = this.props;
    return users && users.length > 0 ? (
      <UserList users={users} />
    ) : (
      <div>No Users!</div>
    );
  }
}

const mapStateToProps = ({ users }) => ({ ...users });

export default connect(
  mapStateToProps,
  userActions
)(UserListContainer);
Enter fullscreen mode Exit fullscreen mode

Let’s render everything in the App component

import React, { Component } from 'react';

import logo from 'logo.svg';
import UserListContainer from "containers/UserListContainer";

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <div>
          <UserListContainer />
        </div>
      </div>
    );
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Summary

There are solid arguments for and against using fixtures in tests. They can become unwieldily and too numerous if overused. I believe there is a place for fixtures in addition to functions that generate data more dynamically, like factories. In a follow up article I’ll continue on with how the same fixtures can be used with storybook.

Top comments (0)