Replacing Enzyme for React Shallow/Unit Testing

Brief:

As of React 18, Enzyme will no longer be a supported test engine for react code.
https://discuss.openedx.org/t/oep-11-update-enzyme-deprecation/10245

Enzyme is incredibly prevalent in our code repos, as well as in community code that uses our platform. We needed a solution that allows the fundamental testing patterns used in this code to persist without major overhauls of code and patterns.

Here, I will review a simple library that we will be using in a number of edx repos to replace enzyme with a solution that should provide comparable functionality to the portions of enzyme that we wish to maintain, utilizing code that will persist through React 18, as per a Principal Facebook react developer.

Sources:

Goodbye Enzyme. Future of Unit Testing in React v.18

https://thesametech.com/snapshot-testing-in-rtl/

What is Enzyme? Why was it important?

Enzyme is a testing library in react that focused on low-level component unit testing and snapshots. Code written around and tested with Enzyme is validated through shallow rendering of components and validation of rendered content through snapshots and inspection, with a minimum amount of external dependencies being rendered.
As a basic example, this tends to mean that a component that uses internationalization, redux, and paragon (our component library), will not touch any of those pieces of code, but rather have all of those libraries mocked out. In the mindset of unit testing:

  • No extra Provider component wrappers necessary

  • No logic or imports from children needing to be regarded

  • Child components render as shallow react components with their passed props, but do not run their render or import logic.

In order to accurately test components with this pattern, we tend to:

  • Split out all behaviors into hooks that can be independently tested

  • For each of the possible render states of the component

    • take a shallow “snapshot” of the render to validate order and notify on changes.

    • Inspect shallow renders to validate props passed to children, based on hooks and component props.

Why React-Testing-Library is not necessarily a suitable “replacement” for Enzyme

A strong contender in the react testing space is the React Testing Library (@testing-library/react). This library focuses on the mindset of testing components “as the user would interact with them”. This is a powerful testing methodology, but makes it a difficult “replacement” for Enzyme. Specifically, in the capability to mock out libraries for validation.

The renderer for the testing library renders with jsdom, which requires that all output be in low-level (kebab-case) components, with jsdom-accepted properties, which does not directly represent how react components are used and makes it difficult to validate those components without fully rendering them.

In particular, I had trouble coming up with a mocking pattern for component libraries like paragon that support components with features like:

  • nested children (Form and Form.Group in a single component)

  • refs on mocked components

  • react args passed to jsdom mocked components.

In addition, one of the core conceits of the React Testing Library is to validate components based on their aria-role, which is very difficult when using mocked components.

TL;DR;

Most of what this means is that reworking a code base from enzyme to react testing library would require a massive overhaul not just in mocking mechanisms, but in what is mocked and how things are tested in general, which would require a massive refactor in those repo’s, and potentially a philosophical disconnect in testing ideologies.

Solution: @openedx/react-unit-test-utils

I have built a library that will act very similarly to Enzyme in key ways that allow persistence of react mocking patterns and snapshots.  This library builds on the react-test-renderer, built into react (through React 18) and proposed by a principal Facebook React Developer:
Goodbye Enzyme. Future of Unit Testing in React v.18

Building on that, I used a small snippet of code that uses simple logic to emulate some of how enzyme renders managed clean/shallow snapshots:

https://thesametech.com/snapshot-testing-in-rtl/

The goal is to publish this solution as part of a shared react utils library through an openedx repo, though it is currently in a personal repo: GitHub - muselesscreator/react-unit-test-utils: React component building and testing utils for a post Enzyme world. .

 

The library provides a shallow renderer that:

  • Supports the same mocking patterns as enzyme

    • This means it will continue to support nested/react-style components.

  • Allows querying the rendered component (and all children) by

    • findByType (string or component class)

    • findByTestID (previously only available in react-testing-library)

  • Allows checking if a rendered component or child matches an example snippet of jsx.

Usage

In a test file that was using Enzyme, you can replace your enzyme render by replacing a few lines of code

Shallow Rendering

Import

// replace import { shallow } from 'enzyme'; // with import { shallow } from '@openedx/react-unit-test-utils';

Snapshots

// replace el = shallow(<Component {...props} />); expect(el).toMatchSnapshot(); // with el = shallow(<Component {...props} />); expect(el.snapshot).toMatchSnapshot();

Inspection (by component type)

// replace el = shallow(<Component {...props} />); expect(el.findByType(Button).at(0).props().onClick).toEqual(passedOnClick); // with el = shallow(<Component {...props} />); expect(el.findByType(Button)[0].props.onClick).toEqual(passedOnClick);

Inspection (by testID)

JSX comparison

Mocking component Libraries

What this Solution Doesn’t do

refs

Issue

As with enzyme’s shallow renderer, this renderer will not pass refs.

Solution

If you want to test ref behavior, it is recommended to have a separate test that uses React-Testing-Library (with simple jsdom mocks), which will faithfully forward refs for testing just behavior around the rendered ref.

Deep Render (Component State and Component Prop updates)

Issue

This repo does not reproduce the mount (deep render) functionality from enzyme. It also does not provide access to component state or to updating the component props for testing effects.

Solution (for unit tests)

To test state and effects with this mechanism, it is recommended to separate them out from the component and render behavior. The repo contains a somewhat opinionated method of using and mocking state to make it easy to test, as well as an example of testing effect calls, based on their prerequisites array.
Solution (for integration tests)

This repo is really only intended to assist with the Unit side of the testing equation. For integration tests, it is very much still recommended to use another library (such as the React Testing Library)