C3 AI Documentation Home

Enzyme Vs. React Testing Library

Overview

Enzyme was one of the first robust testing libraries to test React components, and we have been using it for a long time, but it is no longer maintained. Enzyme has reached end of life. React 16 is its last supported version, and the UI Framework would like to start using React 18. To support this effort, we have decided to use React Testing Library moving forward. We have chosen this library because of the mindset it promotes when writing tests and the ease in transition from Enzyme.

This document will go over some of the main differences between the two libraries:

  1. Change of Mindset
  • When using enzyme we often rely on testing that a React element is rendered. This is not the correct mindset for React Testing Library. Instead, we should focus on testing that what the end user actually sees on the page is what is expected.
  1. New helper types
  1. New matchers
  1. User interactions

Change of mindset

Moving forward, we should be using the React Testing Library motto:

The more your tests resemble the way your software is used, the more confidence they can give you.

This refers to avoiding testing implementation details (what React elements are rendered) and focusing more on the end product. For example, see this excerpt from test_UiSdlAreaChartReact that uses the Enzyme mindset of checking for intermediate steps instead of the end result such as checking that SDLLineBarChart is rendered:

JavaScript
import * as React from 'react';
import EnzymeAdapter from 'enzyme-adapter-react-16';
import { configure as enzymeConfigure, shallow, mount } from 'enzyme';
import jasmineEnzyme from 'jasmine-enzyme';
import { UiSdlAreaChart as Props } from '@c3/types';
import SpecHelper from '@c3/ui/UiSdlSpecHelper';

describe('ui elements', function () {
  ...

  it('renders the SDLLineBarChart component', function () {
    expect(this.wrapper).toContainMatchingElement('SDLLineBarChart');
  });

  it('renders the chart title', function () {
    expect(this.wrapper).toContainExactlyOneMatchingElement('.c3-card-title');
  });

});

Here, the test is expecting a SDLLineBarChart component to be rendered, but the end user does not see something as a SDLLineBarChart, but merely as a chart or, in this case, an empty state image being rendered. As such, the migrated test instead looks for that image as follows (can be found in test_UiSdlAreaChartReact):

JavaScript
import * as React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';
import { UiSdlAreaChart as Props } from '@c3/types';
import userEvent from '@testing-library/user-event';
import UiSdlAreaChart from '@c3/ui/UiSdlAreaChartReact';

describe('ui elements', function () {
  beforeAll(function () {
    jasmine.getEnv().addMatchers(JasmineDOM);

    this.renderWithTranslations = (props) =>
      ReactTesting.renderWithIntl(<UiSdlAreaChart {...props} />, {
        'SDLDemo.UiSdlAreaChart.componentChartActions.name1': 'Export CSV (Metrics)',
        'SDLDemo.UiSdlAreaChart.componentChartActions.name2': 'Export JPEG',
        'SDLDemo.UiSdlAreaChart.yAxis.name': 'yAxis Test Name',
        'SDLDemo.UiSdlAreaChart.dataSpec.value.label': 'Electricity',
      });
  });

  beforeEach(function () {
    this.data = {
      series: [],
      missing: [],
      eventData: {
        0: {
          objs: [
            {
              id: '0',
              project: 'Test Project',
              timestamp: '2021-10-22T00:00:00.000+03:00',
            },
          ],
        },
      },
    };

    this.renderWithTranslations({
      header: {
        title: 'Card Title',
      },
      data: this.data,
      dataSpec: {
        dataType: {
          typeName: 'TestMetricEvaluatable',
        },
        value: {
          label: 'SDLDemo.UiSdlAreaChart.dataSpec.value.label',
        },
        seriesType: 'area',
      },
    });
  });

  it('renders the SDLLineBarChart component', function () {
    // use the img rendered by empty state as a proxy for SDLLineBarChart
    expect(screen.getByRole('img')).toBeVisible();
  });

  it('renders the chart title', function () {
    expect(screen.getByText('Card Title')).toBeVisible();
  });
});

Also in this test, we can see the difference between searching for a classname and actually looking for the text. Since the text Card Title is what the user sees, that should be the first choice in testing this. That being said, it is not impossible to search for a classname when using React Testing Library, but it should only be a last resort behind using real end results. This can be achieved using this test case where document.querySelector is used to find the selector on the screen:

JavaScript
describe('ui elements', function () {
  beforeAll(function () {
    jasmine.getEnv().addMatchers(JasmineDOM);

    this.renderWithTranslations = (props) =>
      ReactTesting.renderWithIntl(<UiSdlAreaChart {...props} />, {
        'SDLDemo.UiSdlAreaChart.componentChartActions.name1': 'Export CSV (Metrics)',
        'SDLDemo.UiSdlAreaChart.componentChartActions.name2': 'Export JPEG',
        'SDLDemo.UiSdlAreaChart.yAxis.name': 'yAxis Test Name',
        'SDLDemo.UiSdlAreaChart.dataSpec.value.label': 'Electricity',
      });
  });

  beforeEach(function () {
    this.data = {
      series: [],
      missing: [],
      eventData: {
        0: {
          objs: [
            {
              id: '0',
              project: 'Test Project',
              timestamp: '2021-10-22T00:00:00.000+03:00',
            },
          ],
        },
      },
    };

    this.renderWithTranslations({
      header: {
        title: 'Card Title',
      },
      data: this.data,
      dataSpec: {
        dataType: {
          typeName: 'TestMetricEvaluatable',
        },
        value: {
          label: 'SDLDemo.UiSdlAreaChart.dataSpec.value.label',
        },
        seriesType: 'area',
      },
    });
  });

  it('renders the chart div', function () {
    expect(document.querySelector('.c3-sdl-chart-content')).toBeVisible();
  });
});

The change in mindset will likely be the hardest part about the new testing framework, so getting used to testing for end result will help immensely going forward.

New helper type

For many tests, UiSdlSpecHelper is used to provide extra functionality, especially around mocking, checking that components are rendered, and rendering components with translations enabled. Since many of these rely on enzyme functionality, UiSdlSpecHelper#importType, UiSdlSpecHelper#createSpy, UiSdlSpecHelper#mockImportedObject, UiSdlSpecHelper#spyOnEffect, UiSdlSpecHelper#removeDependencies, UiSdlSpecHelper#mountWithIntl, UiSdlSpecHelper#isComponentRendered and UiSdlSpecHelper#shallowWithIntl have been deprecated. There is a new type UiSdlReactTesting that will provide renewed functionality for createSpy, mockImportedObject,spyOnEffect, isComponentRendered, and removeDependencies and UiSdlReactTesting#renderWithIntl will replace the functionality for mountWithIntl and shallowWithIntl.

All of the spy-related UiSdlSpecHelper changes are due to the use of UiSdlSpecHelper#importType. To replace this functionality, we now need to pass the full imported type object of the type we would like to create a spy for. For example in test_UiSdlI18nContextReact:

JavaScript
import * as React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import JasmineDOM from "@testing-library/jasmine-dom";
import { UiSdlI18nContext as Props } from "@c3/types";
import * as UiSdlI18nContextModule from "@c3/ui/UiSdlI18nContextReact";
import ReactTesting from "@c3/ui/UiSdlReactTesting";

const UiSdlI18nContext = UiSdlI18nContextModule.default;

this.mockTranslationStatus = async function (
  mockValue: MockTranslationsStatus
): Promise<jasmine.Spy> {
  const translationsSpy = await ReactTesting.createSpy(
    "UiSdlI18nContextReact",
    "useDynamicallyImportedTranslations",
    UiSdlI18nContextModule
  );
  translationsSpy.and.returnValue(
    mockValue || {
      translations: undefined,
      languageTag: undefined,
      isLoading: false,
      error: undefined,
    }
  );
  return translationsSpy;
};

Here we import the the full modules of UiSdlI18nContextReact as UiSdlI18nContextModule and pass that through to UiSdlReactTesting#createSpy.

New matchers

Since enzyme provided some of the matchers we had used, we now will use those provided by @testing-library/jasmine-dom. These provide basically the same functionality as matchers from enzyme with slightly fewer options and options more directed to the DOM directly.

User interactions

enzyme provides simulation on the EnzymeWrapper directly, but this just simulates calling an on{Event} function rather than fully recreating the interaction. To help us better recreate real user interactions, we can use @testing-library/user-event.

This allows us to have more confidence with our interaction testing while passing in several options. For example, in test_UiSdlAreaChartReact, we can see the extra delay added for the hover event as follows:

JavaScript
import * as React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';
import userEvent from '@testing-library/user-event';
import UiSdlAreaChart from '@c3/ui/UiSdlAreaChartReact';

it('does render a popup if there is hoverDescription', async function () {
  // Semantic ui popup has a 50ms delay for showing up on hover
  const user = userEvent.setup({delay: 50});
  await user.hover(document.querySelector('.c3-card-title-subtitle-container'));
  expect(screen.getByText('hoverDescription')).toBeVisible();
});

This will give us more power to control testing interactions in unit tests rather than relying on more complicated end to end tests for small interactions.

To see more about authoring tests with React Testing Library, please see this document.

Was this page helpful?