Let’s suppose you are a developer. Now let’s suppose you are a front-end developer. Imagine that you have been recently hired to perform this role in a company that has a large, complex React web application. You see that managing such a complex application is a struggle. You don’t understand the actual flow of information, you see that there’s some inexplicable logic that you are afraid to touch, there’s a deep component nesting architecture, and so on.
If someone told you to make some changes in this codebase, this would probably be your reaction: 😱.
But you recall hearing from some wise white-haired man that there’s a way to manage such complexity. That man used a specific word to address this problem: refactoring! This is typically done when you want to rewrite a piece of code in a simpler way, breaking the logic into smaller chunks to reduce complexity.
Before you proceed, you remember that this is a very complex application. How can you be sure that you’re not breaking anything? You recall something else from the wise old man: testing!
At Cloud Academy, we have our large complex application built on a stack composed of React/Styled Components/Redux/Redux saga. The sagas are responsible for making the Rest API calls and updating the global state, while components will receive that state, updating the UI accordingly.
So, given our React stack, our choice was:
Even though these two libraries both result in testing React components, they differ significantly in how the test is performed.
Enzyme
focuses testing on implementation details, since you need to know the internal structure of the components and how they interact with each other. This gives you more control over the test execution, enabling deep testing on state and property changes. However, it makes the tests brittle, since almost every modification to the implementation needs a test update. Generally speaking it can be useful when testing simple components in which you have some logic that you don’t want to be altered, or with less component aggregation and interaction and more focused on content rendering. We use Enzyme in our design system Bonsai
library.
Testing Library for React
focuses on how the components behave from a user standpoint. To perform the test, you don’t need to know the implementation details, but rather how it renders and how the user should interact with it. This enables testing of very complex components. Since you don’t need to be concerned about the internals, you only need to give meaningful props, mock dependencies where needed (independent from the chosen framework), and test the output, checking what you expect to be rendered (content, labels, etc.) or interacting as a user would. We use Testing Library for React in our main application project, with which you can test whole pages without worrying too much about their complex structure.
Let’s dig into how we perform tests our main React codebase leveraging Testing Library
.
We can see our application structured in different layers:
Each of these layers deserves a thorough tour about how they should be tested, but this article is focused on how we test React layers, so we’ve identified four main categories of tests:
This is usually the page entry point, where all the magic happens by provisioning the actual data to the underlying components and controlling their actions with callbacks.
A complete test usually needs:
If the test is well prepared, assertions are very easy, as you probably just need to inject the fixture data and check if it’s rendering like you expect. This ensures fairly wide coverage of the internal components, without taking into account the implementation details.
One suggested practice is to test at least the “happy path” and the “error” situations. Testing all the other possible code branches is also highly recommended, but only after the most common paths have been covered.
beforeEach(() => { ... mockHistory = createMemoryHistory(); initialState = { .... //some content here course: { description: '.....' nextStep: { title: '....' } } ... }; }); test('should render the course step content from the state', () => { connectedRender( <Router history={mockHistory}> <ContainerWithSomeContent /> </Router>, { initialState, reducer, }, ); expect(screen.getByText(initialState.course.description)).toBeInTheDocument(); expect(screen.getByText(`Next: ${initialState.course.nextStep.title}`)); });
Let’s take a small detour through how to test connected components. As you see above, the test is using the connectedRender
API. This is a utility function to enable testing on containers that are connected to Redux store without having to set up its boilerplate code in every test.
In order to test those kind of components, you simply need to pass these items to this utility function: the jsx, the reducer and the initial state that will be used to construct the store that will forward the state using the redux Provider.
The following is the implementation of that utility.
import React from 'react'; import { render as rtlRender } from '@testing-library/react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; function render( ui, { initialState, reducer, store = createStore(reducer, initialState), ...renderOptions } = {}, ) { function Wrapper({ children }) { return <Provider store={store}>{children}</Provider>; } return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); } // re-export everything export * from '@testing-library/react'; // override render method export { render as connectedRender };
Given that the “happy path” is being tested on the container, there’s the possibility that you don’t need to perform tests on the internal components, that should be dumb as much as possible. If some kind of logic is present here (e.g., displaying different labels depending on props values), it’s also good to create tests to cover these specific situations, handling them here instead of containers.
If you have many possible combinations, a suggested practice is to write tabular tests.
Hooks can be tested without being forced to render them inside components using renderHook
from @testing-library/react-hooks
library. Testing hooks is somewhat similar to testing functions and components at the same time.
The hook returns a result
object on which you can make assertions by accessing its current
property.
You should avoid immediate destructuring here if you are planning to call a function on the result that changes the hook state. In fact, this triggers the library to re-render the result, assigning a new reference to the result.current
property.
//render the hook const { result } = renderHook(() => useSomeSuffWithChangeableState()) //read its result value const { changeState, value } = result.current // if you expect to change the state here and have an updated value act(() => { changeState() }) // this assertion will fail, since the "value" will still be the old one expect(value).toEqual(..) // to assert the new value you need to re-read it expect(result.current.value)
Generic functions usually don’t need anything from react testing library, since no jsx rendering should happen inside of them. In this case, you can simply assert using standard matchers, again with tabular testing if needed.
It's Flash Sale time! Get 50% off your first year with Cloud Academy: all access to AWS, Azure, and Cloud…
In this blog post, we're going to answer some questions you might have about the new AWS Certified Data Engineer…
This is my 3rd and final post of this series ‘Navigating the Vocabulary of Gen AI’. If you would like…