Why Testing Matters

Testing is how you verify what your code actually does, rather than what you hope it does. A test suite runs through your code line by line to confirm the application executes without errors. This becomes especially valuable after updates: run your tests, and you'll know immediately whether a change broke something that was already working.

Testing serves a few clear purposes:

  • Prevents regression. Regression is when a previously fixed bug reappears or a feature stops working as intended after some change.
  • Verifies complex components. Modular applications with many interacting parts need automated checks to confirm each piece behaves correctly.
  • Improves reliability. A tested app is more robust and less prone to unexpected failure.

Like any tool, testing has trade-offs. On the plus side, it prevents regression, lets you focus on current work instead of worrying about the past, enables modular construction of complex applications, and reduces manual verification. The downsides: you must write, debug, and maintain additional code, and a non-critical test failure can block continuous integration.

Types of Tests

Unit tests isolate a single unit of code — a function, method, procedure, module, or object — and verify that it performs as expected. If you're testing whether a statement or loop works properly, that's unit testing.

Component tests verify an individual part of an application in isolation. Since React apps are built from components, component testing focuses on each component separately, without considering how it integrates with others. This requires more sophisticated tooling than simple unit tests.

Snapshot tests capture the code of a component at a specific moment, so you can compare that state against any other possible state. This ensures the UI doesn't change unexpectedly.

Jest and Enzyme

Jest is a JavaScript testing framework with a focus on simplicity. It's installed with npm or Yarn and falls into the category of test runners. It works great for React applications, but it's not limited to them.

Enzyme is a library designed specifically for testing React components. It lets you write assertions that simulate actions and confirm whether the UI is working correctly. Jest and Enzyme complement each other, so we'll use both.

Setting Up Your Test Environment

If you're new to React, Create React App is the fastest start — it ships with Jest ready to use.

npm init react-app my-app

Next, install Enzyme, enzyme-adapter-react-16, and react-test-renderer (match the adapter number to your React version):

npm install --save-dev enzyme enzyme-adapter-react-16 react-test-renderer

Create a setupTest.js file in the src folder of your project:

import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });

This imports Enzyme and configures the adapter so your tests can run.

Before writing tests, you'll need to know a few core concepts:

  • it or test — the function passed to these methods runs as a block of tests.
  • describe — an optional method for grouping any number of it or test statements.
  • expect — the condition your test must pass; it compares the received parameter to a matcher.
  • mount — renders the full DOM, including a parent component's child components.
  • shallow — renders only the individual component being tested, without its children. This enables true isolation testing.

Creating a Test File

Jest recognizes test files in two ways: any file in a directory named __test__, or any file with the suffix .spec.js or .test.js. Jest searches the entire repository to find them.

Let's write a basic test. Open App.test.js and verify that the app component renders and produces expected output:

it("renders without crashing", () => {
  shallow(<App />);
});

it("renders Account header", () => {
  const wrapper = shallow(<App />);
  const welcome = <h1>Display Active Users Account Details</h1>;
  expect(wrapper.contains(welcome)).toEqual(true);
});

The first test uses shallow to check that our app component renders without crashing. The second test confirms that an h1 tag with the text "Display Active User Account" appears in the component, using the Jest matcher toEqual.

Run the test:

npm run test 
/* OR */
npm test

If everything passes, your terminal output should look like this:

  PASS  src/App.test.js
  √ renders without crashing (34ms)
  √ renders Account header (13ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        11.239s, estimated 16s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

Skipping Tests

Skipping a test marks it so Jest won't run it:

it.skip("renders without crashing", () => {
  shallow(<App />);
});

it("renders Account header", () => {
  const wrapper = shallow(<App />);
  const header = <h1>Display Active Users Account Details</h1>;
  expect(wrapper.contains(header)).toEqual(true);
});

The first test is skipped because of the skip method — Jest ignores it entirely and runs only the second test. You can also use it.only() to run just one test. Jest's watch mode is a nice convenience here: run npm test -- --watch and the test runner will watch for file changes and execute tests automatically.

Mocking

A mock is a convincing duplicate of an object or module, without any real inner workings. It may have minor functionality, but compared to the real thing, it's a fake. Mocks can be created automatically by Jest or manually.

Mocking reduces the number of dependencies that must be loaded and parsed when a test runs, which makes tests execute faster. Mock functions are also known as "spies" because they let you observe the behavior of a function called by other code — not just the output.

There are two ways to mock a function: create a mock function for use in test code, or write a manual mock to override a module dependency. Manual mocks are useful for stubbing functionality with mock data — for instance, using fake data instead of accessing a remote resource like a website or database.

This matters in practice. Suppose your app passes user-account props from a main App component to an Account component. To test this data flow, you don't want to hit a real database. Instead, create a manual mock function to use fake data:

const user = {
  name: "Adeneye David",
  email: "[email protected]",
  username: "Dave",
};

With this mock in place, you can write tests that verify props are being passed correctly:

describe("", () => {
  it("accepts user account props", () => {
    const wrapper = mount(<Account user={user} />);
    expect(wrapper.props().user).toEqual(user);
  });
  it("contains users account email", () => {
    const wrapper = mount(<Account user={user} />);
    const value = wrapper.find("p").text();
    expect(value).toEqual("[email protected]");
  });
});

The describe block identifies the component being tested. The first test confirms that the props passed to the mounted component equal the mock props we created. The second test mounts the Account component with user props and verifies we can find the corresponding <p> element.

You can also test component state. For example, check whether an error message state is initially null:

it("renders correctly with no error message", () => {
  const wrapper = mount();
  expect(wrapper.state("error")).toEqual(null);
});

This test uses the toEqual() matcher to verify the state. If any error message appeared in the app, the test would fail.

Testing components — their rendering, their props, their state — is the core of verifying that a React application keeps working as intended. With Jest and Enzyme, you can catch regressions, confirm that updates don't break existing behavior, and build confidence in your code.

What Snapshot Testing Captures

Snapshot testing records a component’s rendered output at a specific point in time and stores it as a JSON reference file. On subsequent test runs, Jest compares the component’s current output against that stored file. A mismatch fails the test, signaling that the UI has changed since the snapshot was taken.

Enzyme wrappers need to be converted to a Jest-compatible format first. The enzyme-to-json package handles this conversion:

npm install --save-dev enzyme-to-json

With the serializer configured, you can write a snapshot test. On the first run, Jest creates the snapshot and saves it in a new __snapshots__ directory inside src:

it("renders correctly", () => {
  const tree = shallow(<App />);
  expect(toJson(tree)).toMatchSnapshot();
});

Running the suite generates the snapshot file. On subsequent runs, Jest compares the live component output to the recorded snapshot.

npm run test

Enzyme’s shallow method renders only the component under test, skipping its children, which isolates the code being verified. The mount method, in contrast, renders the full DOM including all child components, which is useful when you need to test interactions across a component tree.

Handling Snapshot Changes

Consider what happens when a component’s markup changes after a snapshot has been recorded. If you modify the <h3> tag from <h3> Loading...</h3> to <h3>Fetching Users...</h3>, the test fails because the rendered output no longer matches the reference file. The terminal shows the diff:

 FAIL  src/App.test.js (30.696s)
  × renders correctly (44ms)

  ● renders correctly

    expect(received).toMatchSnapshot()
    Snapshot name: `renders correctly
1

    - Snapshot
    + Received

      
        

Display Active Users Account Details

- Loading... + Fetching Users...

7 | it("renders correctly", () => { 8 | const wrapper = shallow(); > 9 | expect(toJson(wrapper)).toMatchSnapshot(); | ^ 10 | }); 11 | 12 | /* it("renders without crashing", () => { at Object. (src/App.test.js:9:27) › 1 snapshot failed. Snapshot Summary › 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them. Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 1 failed, 1 total Time: 92.274s Ran all test suites related to changed files. Watch Usage: Press w to show more.

You have two options: revert the component change or update the snapshot to reflect the new output. Jest’s interactive CLI makes this easy — press w to show more options, then press u to update the snapshot:

› Press u to update failing snapshots.

After updating, the test passes with the new component markup recorded as the baseline.

Further Resources

Smashing Editorial