Automating React test migration from Enzyme to Testing Library

Background

I work with the MAAS UI team at Canonical. Our UI used to be written in Angular, but recently we migrated it to React v17, since Angular has reached EOL. Now that the migration is complete, we want to get updated to React v18; however, our unit tests are all written using Enzyme, which is deprecated beyond v17, meaning that we have to migrate everything over to a new testing library - React Testing Library. RTL is also far better for testing components and pages, since it’s testing based on what the user actually sees, rather than just granting the library access to the raw React code and extracting data.

The problem

Migrating tests from Enzyme to RTL by hand is a fairly laborious process, which usually consists of the following steps:

  • Remove enzyme imports

  • Import screen and render from testing-library/react as well as userEvent

  • Convert all mount calls to render and use utils like renderWithBrowserRouter where possible

    • This may mean removing imports for Provider and/or any routers

  • Convert any wrapper.find() calls to screen.getBy*()

  • Change interactive component tests to async functions using userEvent

  • Ensure all relevant components are accessible

  • Create label enums for components that require them (usually buttons and headers)

  • Make sure the test still runs correctly and doesn't result in false positives/negatives

Our codebase has over 600 tests, 304 of which still need to be migrated.

Finding a solution

Solution 1 - Complete automation, 100% coverage

In theory, a lot of these steps are repeatable, and could be handed off to automation. The ideal goal would be to find a way to automate every single one of the steps mentioned above, and run it all in one go to migrate every remaining Enzyme test to an RTL test. However, this is easier said than done, specifically for the following cases:

  • Rewriting interactive tests to async - since most of the enzyme tests find components by testid (which is not a great way of testing), we need to make sure that all components can be easily accessed. For some components you may be able to simply look them up by their testid and find the associated test value, but a lot of the time extra legwork is required.

  • Ensuring all relevant components are accessible - this can be quite a challenge, since some components require aria labels being added in order to make them accessible and testable. Aria labels should be clear and descriptive, and this requires a human to write them to ensure this. As well as this, a lot of components can make use of aria-labelledby, which again would require human review to ensure that the label is being fetched from the correct component, as well as writing a function to give some generate components unique IDs on their outputs.

  • Making sure that all tests run correctly - again, this will require a human review on every test, something which is simple when migrating tests one by one, but would take a very long time if all tests were migrated in a single batch

  • Convert any wrapper.find() calls to screen.getBy*() - each one of these calls requires manual inspection to ensure that the correct component is being found, and often requires the programmer to remove searches for testids on components, since this is a fragile way of testing.

In summary, this solution would be absolutely deal if it worked without flaws, and would save us weeks (if not months) of time spent migrating tests manually. However, there are too many issues with this solution to be feasibly implemented, and the time spent implementing it would likely be beyond the time it would take to just do it all by hand as we are now.

Solution 2 - Partial migration - a "helper"

Although some steps of automation have proven to be too complex to automate reliably, there are still some steps that should be able to be done with relative ease:

  • Remove enzyme imports

  • Import screen and render from testing-library/react as well as userEvent

  • Convert all mount calls to render and use utils like renderWithBrowserRouter where possible

Even converting any wrapper.find() calls to screen.getBy*() can be partially done - testids can be picked out and temporarily used to ensure a test runs correctly, after which a programmer can take these tests and make them more robust by testing for roles and labels.

This automation method could be implemented as a "helper" script, which could be run on individual tests, or on folders of tests at a time. This would take care of a lot of the repetitive tasks required for a developer to carry out, and could theoretically greatly decrease the per-batch time needed to migrate tests.

Conclusion

While complete automation would be an ideal solution, we unfortunately do not live in an ideal world. However, having a small “helper” script to take care of some of the most repetitive parts of migration would save a lot of time, and would allow us to get this upgrade done a lot sooner than if we continued to do this completely by hand.

You can see the source code for MAAS UI here.

Nick De Villiers

Associate Web Engineer at Canonical.

https://nick-dv.com