Testing Modern Web Applications With Jest + Enzyme

Jest + Enzyme

Modern React Testing Overview

Why Automate Testing?

Automated testing can be used to test extraneous cases that you wouldn’t normally test during development – for example, a form with an agreement checkbox. During development, a programmer may validate the form works when the user has selected this checkbox but may have overlooked if the user proceeds without selecting the agreement checkbox; thus, leaving opportunity open for bugs to sneak by.

 

Automated testing additionally offers developers more confidence to change code because they know that there are tests verifying the basic functionalities of an application. It serves as a safety net to help ensure innocent issues and bugs don’t make it to production.

 

Developing an automated testing solution also helps document the code. A test should document code in the sense that you should be able to read through the tests and understand what behavior an application can be expected to exhibit.

 

Bottom line, automated testing prevents bugs and ensures proper regression testing is performed. For all the above reasons and more, automation testing prevents issues from being introduced into your project, offers more test coverage, and speeds up the time spent on regression testing prior to release.

 

The Testing Pyramid Model

One popular approach to formulating a software testing strategy is The Testing Pyramid Model, introduced by Mike Cohn.

Cohn Pyramid

 

In this model, GUI tests account for the smallest portion of the pyramid because they are slower, require the app to load into a real browser, and they are complex to write. UI tests are also fragile and require more maintenance – adding to their costliness. The Acceptance / Service test layer sits in the middle, facilitating integration testing between units of the API layer independently of UI considerations. The base of the testing pyramid is Unit tests. These tests execute quickly and are cheap to write. The thought process here is that there should be far more unit tests than UI tests, so testing is as efficient as possible from a speed and development perspective.

 

The problem with this model from a frontend perspective is that user interface details often change and if time is not spent to also update your tests it could result in failures. Due to this factor, even though the GUI test layer should account for the smallest portion, it ends up requiring significant development and support efforts.

 

The Testing Trophy Model

A newer strategy becoming popular for frontend testing – which addresses the Testing Pyramid Model’s challenges – is The Testing Trophy Model, introduced by Kent C. Dodds.

Testing Trophy Model

 

In this model E2E testing, for the most part, corresponds to the UI portion of the testing pyramid. The testing trophy strategy emphasizes integration testing on the basis that it is believed to net the highest return on investment. It is for this reason that integration testing makes up the largest piece of the testing trophy. These tests validate large features to full pages, fostering confidence that functionality is working correctly, all without the overhead of a real browser, database, or backend.

 

Below the integration test portion of the trophy is unit testing. Unit testing is reserved for checking the most complex functionalities’ logic. Finally, at the base of the trophy is the static analysis section. This section checks for syntax errors and bad practices, with a combination of code formatters, linters, and type checkers.

 

Two Main Strategies to React Testing  

  • Rendering component trees in a simplified test environment and asserting on their output
  • Running a complete app in a realistic browser environment (also known as “end-to-end” tests)

 

Testing Strategy Considerations

Tests should be deterministic – meaning that they always pass or fail. Non-deterministic tests pass and fail inconsistently for many reasons such as incompatible file systems, different time zones, a database which isn’t prepared prior to testing, or timeouts for testing asynchronous behavior. One solution for this would be mocks which we will discuss in greater detail later.

 

Do not strive for 100% code coverage – Good tests are easy to create and maintain and give you confidence to change your code. It is for this reason that it could be assumed that 100% test coverage would be the goal. However, complete code coverage in a complex application will never occur because not every situation is conducive to testing with automation. Some user behaviors are extremely complex and require extensive steps to replicate the correct behavior. In these cases, the tests will be difficult to maintain as refactors occur. To achieve efficient testing coverage, it is equally as important to identify what NOT to include tests for. If it feels like it’s taking too long to formulate the solution, then it might be worth considering if the test is even a good candidate.

 

Jest + Enzyme

What are they?

Jest is a JavaScript test runner that allows you to assert, traverse, and manipulate React components’ output. It has the benefit of speed, helpful failure messages, mocks and spies, and simple/zero configuration – to name a few. The power of Jest is achieved through DOM access via jsdom. While this is only an approximation of actual browser behavior, it’s enough in most cases for React component testing. These capabilities combined with powerful features like mocking modules and timers gives ample control over code execution.

 

Enzyme is a testing utility for React. It gives a jQuery like API to find elements, trigger events, and so on. Another popular alternative to Enzyme is the React Testing Library.

 

Setup/Teardown

Test setup and teardown are crucial to a solid testing strategy. beforeEach blocks are commonly used for rendering our React tree to a DOM element. afterEach blocks are used for code clean up and unmounting the tree from the document after the test ends. Cleanup should be executed even if a test fails otherwise tests can become “leaky”. This is when one test can change the behavior of another test, making, them difficult to debug.

__________________________________________

import { unmountComponentAtNode } from “react-dom”;

 

let container = null;

 

beforeEach(() => {

  container = document.createElement(“div”);

  document.body.appendChild(container);

});

 

afterEach(() => {

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

__________________________________________

In this example, we set up a DOM element as a render target in our beforeEach block. Our afterEach block is responsible for cleanup on test exit. The afterEach block calls the unmountComponentAtNode method of react-dom and nullifying the container variable.

 

Act

In UI tests, actions like rendering, event triggers, or data fetching are all examples of “units” of interaction within a user interface. Act is a helper method provided by React to make sure all updates related to these units have been processed and applied to the DOM prior to any assertions running. This helps to mimic the experience of real-world users using the application.

__________________________________________

act(() => {

  // render components

});

// make assertions

__________________________________________

Render Methods

Render Methods come into play when you might want to test whether a component renders correctly for given props.

Consider a simple component that renders an h1 tag and a span element to greet a user by name.

__________________________________________

 

// hello.js

 

import React from “react”;

 

export default function Hello(props) {

  if (props.name) {

    return <h1>Hello, {props.name}!</h1>;

  } else {

    return <span>Hey, stranger</span>;

  }

}

 

__________________________________________

We could test this component as such:

__________________________________________

// hello.test.js

 

import React from “react”;

import { render, unmountComponentAtNode } from “react-dom”;

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

 

import Hello from “./hello”;

 

let container = null;

beforeEach(() => {

  container = document.createElement(“div”);

  document.body.appendChild(container);

});

 

afterEach(() => {

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

 

it(“renders with or without a name”, () => {

  act(() => {

    render(<Hello />, container);}

  );

  expect(container.textContent).toBe(“Hey, stranger”);

 

  act(() => {

    render(<Hello name=”Jenny” />, container);

  });

  expect(container.textContent).toBe(“Hello, Jenny!”);

 

  act(() => {

    render(<Hello name=”Margaret” />, container);

  });

  expect(container.textContent).toBe(“Hello, Margaret!”);

});

__________________________________________

In this example we start with a null container variable and then setup the DOM element as a render target. Then a test runs verifying that the expected result renders with or without a name provided. It first renders with no name property passed down and then again with two different test names. After the test runs it will execute unmountComponentAtNode and finish tearing down the test.

Enzyme has three methods of render: shallow(), mount(), and render().

 

Shallow

Simple shallow

Calls:

  • Constructor
  • Render
  • componentDidMount v3

 

Shallow + setProps

Calls:

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate v3

 

Shallow + unmount

Calls:

  • componentWillUnmount

 

Mount

Full rendering including child components. Testing with Mount requires a DOM (jsdom, domino) and is more costly in execution time.

Simple mount

Calls:

  • constructor
  • render
  • ComponentDidMount

 

Mount + setProps

Calls:

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • ComponentDidUpdate

 

Mount + unmount

Calls:

  • componentWillUnmount

 

Render

Only calls render but renders all children.

 

Good rules to follow

  1. Always begin with shallow
  2. If you want to test children behavior, use mount
  3. If you want to test children rendering with less overhead than mount and you are not interested in lifecycle methods, use render

 

Mocks

The line between unit and integration tests can be blurry. Things to consider: when testing a form if you should also test the buttons inside of that form? Or, alternatively, should the button component have its own suite of testing? How will future refactor effect the tests? Will they break easily?

 

Mocking is a technique used to isolate testing subjects by replacing dependencies with objects that you can manipulate and inspect as needed. The main objective here is to use mock functions to replace the things we cannot control with those which we can.

 

Consider a Contact component that embeds a third-party GoogleMap component:

__________________________________________

// map.js

 

import React from “react”;

import { LoadScript, GoogleMap } from “react-google-maps”;

 

export default function Map(props) {

  return (

    <LoadScript id=”script-loader” googleMapsApiKey=”YOUR_API_KEY”>

      <GoogleMap id=”example-map” center={props.center} />

    </LoadScript>

  );

}

 

// contact.js

 

import React from “react”;

import Map from “./map”;

 

function Contact(props) {

  return (

    <div>

      <address>

        Contact {props.name} via{” “}

        <a data-testid=”email” href={“mailto:” + props.email}>

          email

        </a>

        or on their <a data-testid=”site” href={props.site}>

          website

        </a>.

      </address>

      <Map center={props.center} />

    </div>

  );

}

 

 

 

// contact.test.js

 

import React from “react”;

import { render, unmountComponentAtNode } from “react-dom”;

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

import Contact from “./contact”;

import MockedMap from “./map”;

 

jest.mock(“./map”, () => {

  return function DummyMap(props) {

    return (

      <div data-testid=”map”>

        {props.center.lat}:{props.center.long}

      </div>

    );

  };

});

 

let container = null;

 

beforeEach(() => {

  container = document.createElement(“div”);

  document.body.appendChild(container);

});

 

afterEach(() => {

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

 

it(“should render contact information”, () => {

  const center = { lat: 0, long: 0 };

  act(() => {

    render(

      <Contact

        name=”Joni Baez”

        email=”test@example.com”

        site=”http://test.com”

        center={center}

      />,

      container

    );

  });

 

  expect(

    container.querySelector(“[data-testid=’email’]”).getAttribute(“href”)

  ).toEqual(“mailto:test@example.com”);

 

  expect(

    container.querySelector(‘[data-testid=”site”]’).getAttribute(“href”)

  ).toEqual(“http://test.com”);

 

  expect(container.querySelector(‘[data-testid=”map”]’).textContent).toEqual(

    “0:0”);

});

__________________________________________

If we don’t want to go as far as rendering the map component in testing, then the dependences can be mocked out to create a dummy component and then tests will be executed against that.

 

With mock functions, a user is given the power to omit the actual implementation of a function to test its relationship to other modules. Calls to constructors and other functions can be captured along with the parameters passed in. This ultimately achieves our goal of replacing what cannot be controlled with variables that we can.

 

Events

It is recommended to dispatch real DOM events on DOM elements, and then assert on the result.

__________________________________________

// toggle.js

 

import React, { useState } from “react”;

 

export default function Toggle(props) {

  const [state, setState] = useState(false);

  return (

    <button

      onClick={() => {

        setState(previousState => !previousState);

        props.onChange(!state);

      }}

      data-testid=”toggle”

    >

      {state === true ? “Turn off” : “Turn on”}

    </button>

  );

}

__________________________________________

Consider a Toggle component:

__________________________________________

// toggle.test.js

 

import React from “react”;

import { render, unmountComponentAtNode } from “react-dom”;

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

 

import Toggle from “./toggle”;

 

let container = null;

beforeEach(() => {

  container = document.createElement(“div”);

  document.body.appendChild(container);});

 

afterEach(() => {

  // cleanup on exiting

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

 

it(“changes value when clicked”, () => {

  const onChange = jest.fn();

  act(() => {

    render(<Toggle onChange={onChange} />, container);

  });

 

  // get ahold of the button element, and trigger some clicks on it

  const button = document.querySelector(“[data-testid=toggle]”);

  expect(button.innerHTML).toBe(“Turn on”);

 

  act(() => {

    button.dispatchEvent(new MouseEvent(“click”, { bubbles: true }));  });

 

  expect(onChange).toHaveBeenCalledTimes(1);

  expect(button.innerHTML).toBe(“Turn off”);

 

  act(() => {

    for (let i = 0; i < 5; i++) {

      button.dispatchEvent(new MouseEvent(“click”, { bubbles: true }));    }

  });

 

  expect(onChange).toHaveBeenCalledTimes(6);

  expect(button.innerHTML).toBe(“Turn on”);

});

__________________________________________

In our beforeEach we setup a DOM element and attach it to document – attaching the container is crucial for events to work correctly. In our test “changes value when clicked,” we render the component passing in container and get ahold of the button element to trigger some test clicks. Note that it is necessary to pass { bubbles: true } in each event created for it to reach the React listener. This is because React automatically delegates events to the document.

 

Timers

The native timer functions (i.e., setTimeout, setInterval, clearTimeout, clearInterval) are less than ideal for a testing environment since they depend on real time to elapse. Jest can swap out timers with functions that allow you to control the passage of time. Timer-based functions like setTimeout can be used to schedule work in the future.

 

Consider a multiple-choice panel waiting for a decision, and then advancing, or timing out if a decision isn’t received within 5 seconds.

__________________________________________

// card.js

 

import React, { useEffect } from “react”;

 

export default function Card(props) {

  useEffect(() => {

    const timeoutID = setTimeout(() => {

      props.onSelect(null);

    }, 5000);

    return () => {

      clearTimeout(timeoutID);

    };

  }, [props.onSelect]);

 

  return [1, 2, 3, 4].map(choice => (

    <button

      key={choice}

      data-testid={choice}

      onClick={() => props.onSelect(choice)}

    >

      {choice}

    </button>

  ));

}

__________________________________________

When testing this component, we can leverage Jest’s timer mocks to test the different states a component can be in at a given point.

__________________________________________

// card.test.js

 

import React from “react”;

import { render, unmountComponentAtNode } from “react-dom”;

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

 

import Card from “./card”;

jest.useFakeTimers();

 

let container = null;

beforeEach(() => {

  // setup a DOM element as a render target

  container = document.createElement(“div”);

  document.body.appendChild(container);

});

 

afterEach(() => {

  // cleanup on exiting

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

 

it(“should select null after timing out”, () => {

  const onSelect = jest.fn();

  act(() => {

    render(<Card onSelect={onSelect} />, container);

  });

 

  // move ahead in time by 100ms  act(() => {

    jest.advanceTimersByTime(100);

  });

  expect(onSelect).not.toHaveBeenCalled();

 

  // and then move ahead by 5 seconds  act(() => {

    jest.advanceTimersByTime(5000);

  });

  expect(onSelect).toHaveBeenCalledWith(null);

});

 

it(“should cleanup on being removed”, () => {

  const onSelect = jest.fn();

  act(() => {

    render(<Card onSelect={onSelect} />, container);

  });

  act(() => {

    jest.advanceTimersByTime(100);

  });

  expect(onSelect).not.toHaveBeenCalled();

 

  // unmount the app

  act(() => {

    render(null, container);

  });

  act(() => {

    jest.advanceTimersByTime(5000);

  });

  expect(onSelect).not.toHaveBeenCalled();

});

 

it(“should accept selections”, () => {

  const onSelect = jest.fn();

  act(() => {

__________________________________________

In the above test we enable fake timers by calling jest.useFakeTimers(), which mocks setTimeout and other timer functions. The major advantage here is that the test doesn’t have to truly wait 5 seconds to execute, meaning the component logic doesn’t need to be made more complicated just for the sake of testing.

 

Snapshot Testing

Another useful method of testing is “saving” the rendered component output in the form of snapshots using toMatchSnapshot and toMatchInlineSnapshot methods. This ensures that changes made to the component must be explicitly committed as changes to the snapshot.

 

In this example, we render a component and format the rendered HTML with the pretty package. We then save it as an inline snapshot and assert expectations:

__________________________________________

// hello.test.js, again

 

import React from “react”;

import { render, unmountComponentAtNode } from “react-dom”;

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

import pretty from “pretty”;

 

import Hello from “./hello”;

 

let container = null;

beforeEach(() => {

  // setup a DOM element as a render target

  container = document.createElement(“div”);

  document.body.appendChild(container);

});

 

afterEach(() => {

  // cleanup on exiting

  unmountComponentAtNode(container);

  container.remove();

  container = null;

});

 

it(“should render a greeting”, () => {

  act(() => {

    render(<Hello />, container);

  });

 

  expect(

    pretty(container.innerHTML)

  ).toMatchInlineSnapshot();

 /* … gets filled automatically by jest … */

  act(() => {

    render(<Hello name=”Jenny” />, container);

  });

 

  expect(

    pretty(container.innerHTML)

  ).toMatchInlineSnapshot();

 /* … gets filled automatically by jest … */

 

  act(() => {

    render(<Hello name=”Margaret” />, container);

  });

 

  expect(

    pretty(container.innerHTML)

  ).toMatchInlineSnapshot();

 /* … gets filled automatically by jest … */

});

__________________________________________

In practice, it’s usually better to make more specific assertions than using snapshots. Tests of this type includes implementation details which makes them fragile. Teams become desensitized to snapshot breakages as a result of this fragility – contradicting the purpose of testing. Selectively mocking child components can help to reduce the size and complexity of snapshots, making them more readable in code review.

 

Where Snapshot Testing Shines

Snapshot testing like anything else has its strong suits and its pitfalls and with that, snapshot testing isn’t right for everything. One example of snapshot testing put to good use would be when writing a tool for developers. It is common that you might want to write a test to ensure good error or warning messages are logged to the console for the developers using the tool. Another useful instance could be when using CSS-in-JS. In this case you can use snapshot testing to reduce difficulty of testing changes with a tool like jest-glamor-react. This gives you the ability to include the applicable CSS with whatever’s rendered giving confidence that if logic is changed and some styles aren’t applied properly, we’ll know about it.

 

Things to Avoid with Snapshot Testing

When testing with snapshots, it should generally be avoided to test large snapshots. Snapshots longer than a few dozen lines are liable to suffer maintenance challenges and slow down a development team. Tests are about giving confidence in shipping code that is not broken. However, you can’t ensure that with confidence if you’re using huge snapshots that are too long for anyone to effectively review. Smaller more focused snapshots are far more effective in the long run.

 

Making Snapshot Testing More Effective

Jest-glamor-react is a custom serializer mentioned above which can make our snapshot testing much more effective. A good process for writing a custom serializer is to normalize the paths in a snapshot to those relative to the project directory. Another interesting method helpful to test maintainability is making differences stand out when you have similar tests that all look the same. You can do this by splitting reusable code like setup/teardown into helper methods so that tests have more distinction and less commonality to each other. Using snapshot-diff you can serialize the difference between testing states. This process results in no false negatives when asserting before and after user interaction. In this case, the snapshot will simply be the difference between the two – like the structure of a git diff.

 

Conclusion

When it comes to good testing, we should describe our test steps and assertions – this way anyone can come in and understand quickly the function of the testing. Structuring our tests like this helps to establish a mental picture of setup/teardown and testing.

 

Splitting assertions across multiple it blocks and nesting different describes is a method to better organize tests and make them more readable. It is generally beneficial for assertions and it blocks to have a 1:1 relationship to allow for proper documentation of each test step. We can group describes into common functions like describe(‘rendering’), describe(‘callbacks’), etc. This strategy is called RDD (ReadMe Driven Development) and its main takeaway is that describes are meant to depict conditions, while it blocks explain the expected output.

 

With all this information, it can be hard to know where to start but testing react components is not difficult. A test runner like Jest provides a powerful experience for teams to be able to shape the structure of their testing. With a solid testing strategy, developers are empowered to produce better tests which will yield higher code quality, faster testing iterations, and fewer bugs!

 

Alyssa Bartuch Headshot

Alyssa Bartuch