Testing React components: Enzyme vs React Testing Library

react testing library javascript testing react enzyme

In the last post we've built a Tic Tac Toe game with React Hooks and Styled components. However, it's missing one crucial part of the development process - testing. In this post we'll fix this omission by adding the tests to the TicTacToe component. Additionally this seems like a good opportunity to compare two of the most popular React testing tools - Enzyme and React Testing Library. As a refresher, the final version of the game can be found here and the code is available on Github.

The point of this comparison is not to try to decide which framework is the best, but to illustrate the differences in their approach. First let's install the packages.

npm i -D enzyme enzyme-adapter-react-16 @testing-library/react @testing-library/jest-dom

Next we'll create __tests__ folder in the root of the src directory. We'll be using Jest to run the tests, which comes pre-installed with create-react-app, used for the Tic Tact Toe game. In there let's add two files, one for each testing framework: TicTacToe.enzyme.test.js and TicTacToe.rtl.test.js.

React Testing Library

Starting with React Testing Library, in TicTacToe.rtl.test.js we'll introduce a basic setup and write the first test. But before that, we need to go back to the TicTacToe.js and make a small modification, namely add data-testid for each square.

// TicTacToe.js

// ...

{grid.map((value, index) => {
  const isActive = value !== null;

  return (
    <Square
      data-testid={`square_${index}`} // Add testid
      key={index}
      onClick={() => humanMove(index)}
    >
      {isActive && <Marker>{value === PLAYER_X ? "X" : "O"}</Marker>}
    </Square>
  );
})}
// ...

This testid is a special attribute React Testing Library uses for querying DOM elements.

import React from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TicTacToe from "../TicTacToe";

afterEach(cleanup);

it("should render board with correct number of squares", () => {
  // Render the game component
  const { getAllByTestId, getByText } = render(<TicTacToe />);

  // Click 'X' to start game as player X
  fireEvent.click(getByText("X"));

  // Check that the correct number of squares is rendered
  expect(getAllByTestId(/square/).length).toEqual(9);
});

If you remember from the previous tutorial, when the game starts, the player sees Choose your player screen. We make our choice here by selecting and verify that the grid is rendered with the correct number of squares. Note that we can also get items by partial match, using regex syntax - getAllByTestId(/square/) - returns all the items which include square in their testid attribute. The library has an extensive documentation about the types of queries available.

Testing async actions

Next, let's verify that when we click on an empty square, a move for that player is actually made. Additionally we can test that computer makes its move next.

it("should register and display result of human player's move", async () => {
  const { getByTestId, getByText } = render(<TicTacToe />);
  fireEvent.click(getByText("X"));

  // Click the first square
  fireEvent.click(getByTestId("square_1"));

  // Validate that it has 'X' rendered
  expect(getByTestId("square_1")).toHaveTextContent("X");

  // Wait for computer move
  await waitForElement(() => getByText("O"));

  // Check that we have 'O' in the DOM
  expect(getByText("O")).toBeInTheDocument();
});

After triggering the click on the first square, we successfully verify that text content of the square is X. In order to use toHaveTextContent and a few other useful Jest matchers, we need to install and import Jest-dom package.

After the player has made the move we are testing that computer's move is made as well. In the game component, computer moves with a slight delay, created by setTimeout, so we need to use special async utilities from the testing library. In this case we'll use waitForElement function to wait for computer move to be rendered. Also since we're using await, our test function has to be made async

Note that although the tests pass, you might still get a warning in the console, along the lines of Warning: An update to TicTacToe inside a test was not wrapped in act(...). This is because act testing utility only supported synchronous functions up until React 16.9.0. So in order to get rid of the warning simply update your React to the latest version. If you're curious about the issue itself, there's a lengthy discussion on Github

Next we are going to test that when player clicks on a non-empty square that move won't have any effect. At this point it's getting clear that we need to write some of the same code to make human player's move, then wait for computer move. What happens when we want to test the end game? Are we going to code all the moves to fill the board? That doesn't sound like a productive way to spend our time. Instead let's modify the TicTacToe component to accept an optional grid, that we can use for testing to fast-forward the game to any state. We'll call it squares (I'm running out of names here, since grid and board are already taken) and it will default to the arr we declared earlier. 

// TicTacToe.js

// ...

const arr = new Array(DIMS ** 2).fill(null);

// ...

const TicTacToe = ({ squares = arr }) => {
  const [grid, setGrid] = useState(squares);
  // ...
}

Now when rendering the component for testing we can provide a grid with prefilled values, so we don't need to set them up manually. With this setup we can easily test that it's not possible to make a move to the same square and change its value.

// TicTacToe.rtl.test

it("should not make a move if the square is not empty", () => {
  const { getByTestId, getByText } = render(
    <TicTacToe
      squares={[PLAYER_X, null, PLAYER_O, null, null, null, null, null, null]}
    />
  );
  fireEvent.click(getByText("X"));

  // Click non-empty square
  fireEvent.click(getByTestId("square_2"));
  
  // Should have initial value
  expect(getByTestId("square_2")).toHaveTextContent("O");
});

We have two more things to test to make this test suite comprehensive: 

  1. When there's a winning combination or a draw, the modal with the result is shown.
  2. Pressing Start over button starts a new game and shows initial screen.

For the first scenario we'll provide the grid state one move away from endgame, and then by making that move we test that the game is finished properly.

// TicTacToe.rtl.test

it("should correctly show Player X as a winner", async () => {
  // prettier-ignore
  const grid = [
    PLAYER_X, PLAYER_X, null,
    PLAYER_O, PLAYER_O, null,
    PLAYER_X, null,     PLAYER_O
  ];
  const { getByTestId, getByText } = render(<TicTacToe squares={grid} />);
  fireEvent.click(getByText("X"));

  // Make the winning move
  fireEvent.click(getByTestId("square_2"));

  // Wait for result modal to appear
  await waitForElement(() => getByText("Player X wins!"));

  // Check that result is declared properly
  expect(getByText("Player X wins!")).toBeInTheDocument();
});

it("should correctly display the draw result", async () => {
  // prettier-ignore
  const grid = [
    PLAYER_X, PLAYER_X, PLAYER_O,
    PLAYER_O, PLAYER_O, null,
    PLAYER_X, PLAYER_X, PLAYER_O
  ];
  const { getByTestId, getByText } = render(<TicTacToe squares={grid} />);
  fireEvent.click(getByText("X"));

  // Make the final move
  fireEvent.click(getByTestId("square_5"));

  // Wait for result modal to appear
  await waitForElement(() => getByText("It's a draw"));

  // Check that result is declared properly
  expect(getByText("It's a draw")).toBeInTheDocument();
});

it("should correctly show Player O as a winner", async () => {
  // prettier-ignore
  const grid = [
    PLAYER_O, null,     PLAYER_O,
    PLAYER_X, PLAYER_O, PLAYER_X,
    null,     PLAYER_X, null
  ];
  const { getByTestId, getByText } = render(<TicTacToe squares={grid} />);
  fireEvent.click(getByText("X"));

  // Make the move
  fireEvent.click(getByTestId("square_6"));

  // Wait for result modal to appear
  await waitForElement(() => getByText("Player O wins!"));

  // Check that result is declared properly
  expect(getByText("Player O wins!")).toBeInTheDocument();
});

For completeness sake we're testing all 3 possible endgame scenarios. Note that the grid is formatted in the same way as the game's grid, so it's easier to see the state of the game. If you're using prettier for code formatting, you can disable it for this line with // prettier-ignore, to keep the custom formatting.

Note that in the last test we setup a board so after human player moves, both of the options left for computer's move will make it a winner. We don't have to explicitly wait for computer's turn, we wait instead for the modal to appear, which should happen after the last move. 

As a final test we confirm that the game gets reset after Start over button is pressed.

// TicTacToe.rtl.test

it("should start a new game after 'Start over' button is pressed", async () => {
  // prettier-ignore
  const grid = [
    PLAYER_O, null,     PLAYER_O,
    PLAYER_X, PLAYER_O, null,
    null,     PLAYER_X, PLAYER_X
  ];
  const { getByTestId, getByText } = render(<TicTacToe squares={grid} />);
  fireEvent.click(getByText("X"));

  // Make the winning move
  fireEvent.click(getByTestId("square_6"));

  await waitForElement(() => getByText("Start over"));
  fireEvent.click(getByText("Start over"));

  await waitForElement(() => getByText("Choose your player"));
  expect(getByText("Choose your player")).toBeInTheDocument();
});

With that done, we have a nice comprehensive test suite where we used React Testing Library and tested the game in the same way as the end user would interact with it.

Enzyme

Now we'll test the game from the end user's point of view with Enzyme. We'll start by adding TicTacToe.enzyme.test.js file to the __tests__ folder. Before writing the actual tests we need to do a bit of setup, namely configure the Enzyme's adapter for React.

// TicTacToe.enzyme.test.js 

import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

configure({ adapter: new Adapter() });

Make sure to use Adapter of the same version as your current version of React. After the initial setup we can start writing the tests. Let's follow the same path as with React Testing Library and verify that the game starts with the grid of correct size after choosing the player. 

// TicTacToe.enzyme.test

import React from "react";
import { mount } from "enzyme";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import TicTacToe from "../TicTacToe";
import { PLAYER_O, PLAYER_X } from "../constants";

configure({ adapter: new Adapter() });

it("should render board with correct number of squares", () => {
  // Render the game component
  const wrapper = mount(<TicTacToe />);

  // Find the 'X' button
  const buttonX = wrapper.findWhere(
    component => component.name() === "button" && component.text() === "X"
  );

  // Press it
  buttonX.simulate("click");

  // Check that board is rendered
  expect(wrapper.find("Square").length).toBe(9);
});

From the first tests it gets obvious that testing components with Enzyme the same way as we did with React Testing Library will be a bit more challenging. Firstly we need to use powerful findWhere method to find the item with specific text. Also need to check that it's actually a button so we don't catch any wrapper components. Then, in order to get the Square components, we need to first override their displayName method.

// TicTacToe.js

const Square = styled.div`
 // ...
`;

Square.displayName = "Square";

We could also find them by component reference but in that case we'd have to export Square component and directly import it into the tests. One more option could be to use query like wrapper.find('div[data-testid^="square"], to match test ids that start with "square",  where ^= is used to match partial attributes, however that does not look pretty at all. 

We are also using mount here instead of shallow, that does full DOM render of component and its children, useful in case we need to investigate our Styled components.  

Following the same test structure as when using React Testing Library we'll verify now that player's move is rendered correctly.

// TicTacToe.enzyme.test

it("should register and display result of human player's move", () => {
  // Render the game component
  const wrapper = mount(<TicTacToe />);
  const buttonX = wrapper.findWhere(
    component => component.name() === "button" && component.text() === "X"
  );
  buttonX.simulate("click");

  const firstSquare = wrapper.find("Square").at(0);

  // Click the first square
  firstSquare.simulate("click");

  // Validate that it has 'X' rendered
  expect(firstSquare.text()).toBe("X");
});

Now that it's possible to select styled components by their display name, it's easy to get a component at particular index using at selector. After that we can assert that it's text content is correct using text() method.

One more thing: it seems like we'll be using our verbose button find method in quite a few places, so let's convert it to an utility function.

// TicTacToe.enzyme.test.js

// Helper function to get button by a text
const findButtonByText = (wrapper, text) => {
  return wrapper.findWhere(
    component => component.name() === "button" && component.text() === text
  );
};

After this we can get buttons by specific text with less amount of code. Let's follow up by checking that player cannot make a move to the taken square.

// TicTacToe.enzyme.test

it("should not make a move if the square is not empty", () => {
  const wrapper = mount(
    <TicTacToe
      squares={[PLAYER_X, null, PLAYER_O, null, null, null, null, null, null]}
    />
  );
  const buttonX = findButtonByText(wrapper, "X");
  buttonX.simulate("click");

  // Get non-empty square
  const nonEmptySquare = wrapper.find("Square").at(2);

  // Click it
  nonEmptySquare.simulate("click");

  // Check that text content stays the same
  expect(nonEmptySquare.text()).toBe("O");
});
Testing async actions

The tests are passing so we're all good. Next we'll check that all the endgame combinations are handled correctly. 

// TicTacToe.enzyme.test

import { act } from "react-dom/test-utils";

// ...

jest.useFakeTimers();

it("should correctly show Player X as a winner", () => {
  // prettier-ignore
  const grid = [
    PLAYER_X, PLAYER_X, null,
    PLAYER_O, PLAYER_O, null,
    PLAYER_X, null,     PLAYER_O
  ];
  const wrapper = mount(<TicTacToe squares={grid} />);
  const buttonX = findButtonByText(wrapper, "X");
  buttonX.simulate("click");

  // Make the winning move
  wrapper
    .find("Square")
    .at(2)
    .simulate("click");

  // Wait for result modal to appear
  act(() => {
    jest.runAllTimers();
  });
  wrapper.update();

  // Check that result is declared properly
  expect(wrapper.find("ModalContent").text()).toBe("Player X wins!");
});

it("should correctly display the draw result", () => {
  // prettier-ignore
  const grid = [
    PLAYER_X, PLAYER_X, PLAYER_O,
    PLAYER_O, PLAYER_O, null,
    PLAYER_X, PLAYER_X, PLAYER_O
  ];
  const wrapper = mount(<TicTacToe squares={grid} />);
  const buttonX = findButtonByText(wrapper, "X");
  buttonX.simulate("click");

  // Make the final move
  wrapper
    .find("Square")
    .at(5)
    .simulate("click");

  // Wait for result modal to appear
  act(() => {
    jest.runAllTimers();
  });
  wrapper.update();

  // Check that result is declared properly
  expect(wrapper.find("ModalContent").text()).toBe("It's a draw");
});

it("should correctly show Player O as a winner", () => {
  // prettier-ignore
  const grid = [
    PLAYER_O, null,     PLAYER_O,
    PLAYER_X, PLAYER_O, PLAYER_X,
    null,     PLAYER_X, null
  ];
  const wrapper = mount(<TicTacToe squares={grid} />);
  const buttonX = findButtonByText(wrapper, "X");
  buttonX.simulate("click");

  // Make the move
  wrapper
    .find("Square")
    .at(6)
    .simulate("click");

  // Wait for the computer move
  act(() => {
    jest.runAllTimers();

    // Run timers again for the result modal to appear
    jest.runAllTimers();
  });

  wrapper.update();

  // Check that result is declared properly
  expect(wrapper.find("ModalContent").text()).toBe("Player O wins!");
});

Testing async component actions with Enzyme proved to be quite a challenge. First, we need to add display name prop to the modal content component: ModalContent.displayName = "ModalContent"; Because we're not only testing that the state has updated correctly, but also the state itself is set after a timeout, we need to leverage Jest's useFakeTimers() method to mock the timers used in the component. To manually run those timers, we'll use runAllTimers(), wrapped in act function from React TestUtils. Additionally we need to trigger the timers once more to account for computer's move and finally call Enzyme's update method which will force component's re-render, ensuring that the state was updated. 

Tip: If you stuck at some point wondering why the tests are failing when you're sure they shouldn't, Enzyme's wrapper has a handy debug() method, which prints the rendered component as it would appear in the DOM. It can be used like so console.log(wrapper.debug()).

The final test is to assert that the game restarts correctly.

// TicTacToe.enzyme.test.js

it("should start a new game after 'Start over' button is pressed", () => {
  // prettier-ignore
  const grid = [
    PLAYER_O, null,     PLAYER_O,
    PLAYER_X, PLAYER_O, null,
    null,     PLAYER_X, PLAYER_X
  ];
  const wrapper = mount(<TicTacToe squares={grid} />);
  const buttonX = findButtonByText(wrapper, "X");
  buttonX.simulate("click");
  // Make the winning move
  wrapper
    .find("Square")
    .at(6)
    .simulate("click");

  act(() => {
    jest.runAllTimers();
  });
  
  // Re-render component
  wrapper.update();

  // Get restart button and click it
  const restartButton = findButtonByText(wrapper, "Start over");
  restartButton.simulate("click");

  // Verify that new game screen is shown
  const choosePlayer = wrapper.findWhere(
    component =>
      component.name() === "p" && component.text() === "Choose your player"
  );
  expect(choosePlayer.length).toBe(1);
});

Conclusion

We saw that it is possible to test React components without getting much into implementation details with both Enzyme and React Testing Library. Due to its design, it is more challenging to do it with Enzyme. With Enzyme, we're still getting components by their names, and if those names change in the future or the components get removed, our tests will break. Additionally with the developers moving away from Class based components, a lot of Enzyme's methods for testing class instances are no longer useful since they do not apply to functional components. 

However, it's still possible to have a comprehensive test suite with Enzyme. I have personally started testing React components with Enzyme, however nowadays I am shifting more to React Testing Library due the reasons mentioned above. Ultimately your choice will depend on personal preferences and structure of the tested components. 

Hopefully this article made the task of choosing a framework for testing React components easier by illustrating the application of the two of most popular ones.

Got any questions/comments or other kinds of feedback about this post? Let me know on Twitter.