In the latest instalment of our Tales in Tech series organised by our JavaScript team, Lead Software Engineer Mark Salvin from notonthehighstreet took us through his teams' migration from Enzyme to React Testing Library. Mark walked us through the initial pain points the team faced with Enzyme that triggered the transition over to React Testing Library, the decision making process taken & how they used Mock Service Worker (MSW) to enhance their workflow.

Catch up on the full talk below or read on for a write up of all the advice and insight shared.

Why do we test? 

-To provide us with confidence that our apps work correctly, including both within user journey’s and some of the key business logic. 

-Automated tests protect us against regressions and can also help our workflow enhancement, so that we can run the test suite following any test we make. 

-Tests are a trade off between velocity and quality. With a good balance, tests enable us to move faster. Having a bad balance can result in a long manual testing process or an overly burdensome automated testing process. 

When Mark joined notonthehighstreet back in 2020, work had already begun on replatforming their website. With the foundation work in place, the team could begin building a new web app for the product listing vertical slice.


What did the team originally use?



- for unit tests, as well as unit/integrations tests of Redux toolkit slices.



- which was opted for over Cypress due to the advantage it has with cross browser testing.



- for accessibility static analysis.


- for visual regression testing.


- for performance testing.


- for run testing. 


- to ensure type safety and props validation.


- to provide for linting.

 

Pain Points of Enzyme

-Enzyme tests were time-consuming to write - we realised that we were often testing the implementation details and felt that the tests we wrote were arguably overly complex and often had a lot of crossover in the test coverage.

-We weren’t always capturing integration bugs, despite the high level of test coverage that we had.

-We often needed to rework our tests after refactors. 

-We had to ask, “are our tests really providing us with confidence or simply a false sense of security”.

A new Approach

This motto is a stark contrast to Enzyme, where you would test most things in isolation. 

With agreement from our team, we paired everyone off and performed spikes where we looked to test some complex and simple components. Afterwards we regrouped as a team to discuss our learnings, difficulties and compared approaches. On the back of this we were able to create our project standards that we would look to follow from that point. We defined some complex use cases we wanted to work on first and wrote a reference implementation, during which we created a lightweight suite of helpers. We really encouraged everyone to be active in the review of this reference implementation so that we had everyone on board.

Before...

I used an example of a test for a load more button, it is quite a typical test setup using a describe block. We have an initial state that we share in between tests and in this beforeall hook we are shadow rendering the button. As you can see we have very granular tests, where we are testing implementation detail specific things. The intention is good but it is very implementation detail orientated. This is typical when using Enzyme.


After...

So here is an example of what a test looks like from React Testing Library, you can see that the tests are simpler and more end-user focused. With this second test, we're testing what the end user actually sees, such as the loading spinner, and it essentially goes through the set of transitions as you’d expect.


Key Learnings:

The key learnings and decisions agreed as a team were:

-To avoid testing simplest components such as buttons and headings. 

-To avoid testing too high up at the page level - though there is a balance needed here:
The higher up you test, the more your test resembles the end user experience, which is a good thing and something you will see mentioned a lot in the React testing documents. However, the higher up you test the more test setup you will require, the more likely you are to need to mock frameworks and routing functionality, which will result in more brittle and less specific tests. Knowing this, there is a middle ground that as a team we felt we wanted to reach.

-We wanted to test the end user functionality over the implementation details. 

-We agreed to leverage the code coverage tools to identify any gaps in our testing and at that point we were able to triage any gaps and make the decision about whether a particular component should be covered by a higher up level component, or if it is just a very trivial component that doesn’t require any particular testing.

-To avoid nesting tests - this is something that introduces implicit context and it can increase the cognitive load of reasoning about the test, even though they may appear simpler at first. 

-React Testing Library recommends avoiding hasty abstractions in tests, as this can introduce errors and complexity in tests. We wanted to avoid getting into a position where due to increased test complexity, you end up having to write tests for your tests. We preferred duplication over the wrong abstraction and optimized for change first, whilst abstractions are necessary in some instances.

-We looked to introduce further tooling into our setup via a library called Mock Service Worker (MSW). For anyone who isn’t familiar with Mock Service Worker, it mocks both client and server API’s at the network level using a consistent API and this is useful because many mocking packages will work for either client or the server, but not for both.

-We created a MSW API mocks which enabled us to reduce the setup code and remove tight coupling with store architecture. This gave us increased confidence in the end-to-end integration.

MSW

With Mock Service Worker, we were able to able to enhance our workflow with API mocks that provide only necessary data for components under test, and this enabled us to render and test the components locally in isolation, which is beneficial as it enables local development when the API goes down and can also simplify development in testing in UX edge cases where the API response might be different to elicit organically.

So what were the benefits of moving to React Testing Library?

✅ Less time spent writing tests

✅ More confidence in user functionality

✅ Fewer UI behavioural defects and regressions

✅ No rebase or merge headaches

✅ Refactoring no longer breaking tests

**

A huge thank you to Mark and NOTHS for presenting this talk. If you would like to pick Mark's brains about Enzyme vs React Testing Library, you can follow him on LinkedIn here or check out some of his blogs and thoughts on Software Engineering on his website here - www.marksalvin.com

Mark is a Lead Software Engineer at notonthehighstreet, with over 10 years experience working with mobile and web technologies across the stack. He strives to take a pragmatic approach to implementing well architected solutions, ensuring that a focus is kept on delivering business value.