Actually a “wrapper” is the topic here. The subject is testing of course. In this case React Unit tests.
The path to good React unit test coverage requires a number of activities and approaches as well as a diverse assortment of knowledge. Here’s a summary in one place.
1. Just render it!
This is where I start off for a new component or a component with no test coverage. Just try and render it! This follows the concept that 80% of the value can be obtained with 20% of the effort and I have found that just the basic ability to render a component with default attributes is a great place to start. The other part of writing a basic render test is that it will be the scaffold you need for the complexity you will need to test as the component grows and/or is changed. It will help to start setting the example that every implementation has a specification and it will communicate to the next person working on this piece of code the expectation that it has a test and both need to be maintained.
import { screen, render } from '@testing-library/react';import SomeComponent from './SomeComponent';
import React from 'react';
describe('A component', () => {
it('should render the component', () => {
render(SomeComponent, );
expect(screen.getByText('something')).toBeInTheDocument();
});
Note:
For a complex component with many lines, parameters, methods and conditional branches, adding tests for the first time may be quite challenging. This is useful feedback. Large, complex, application components are hard to change to meet changing business needs and requirements and they more frequently have bugs due to the complex interaction of parameter values and the logic in code path branches. Writing tests (including struggling to write them!) exposes this complexity and encourages the breakout of smaller, more focused components with much fewer dependencies and few or no code branch paths leading to software that is easier and thus cheaper to change more quickly.
2. Render with router wrapper !
Other than the highest level component, it’s pretty likely that you’re using React Router, so that’s the second technique I’ll cover. It’s quite simple and a frequent pattern. Here’s an example of what code looks like to account for it.
import { screen, render } from '@testing-library/react';import SomeComponent from './SomeComponent';
import React from 'react';
import { BrowserRouter } from 'react-router-dom'; // <-- Note: added
describe('A component', () => {
it('should render the component', () => {
render(SomeComponent, { wrapper: BrowserRouter }); // <-- Note use of 'wrapper'
expect(screen.getByText('something')).toBeInTheDocument();
});
3. Render with context wrapper !
The third technique concerns a global state such as the logged in user which is currently provided to the component through a context wrapper. In cases such as this you can provider the wrapper yourself with the value you want to be under test. The resulting code looks like this (this is for testing a logout link):
it('renders a logout link for logged in users', () => {
const page = render(
<UserAuthContext.Provider value={{
user: { uid: 'a' }, signIn: jest.fn(), signUp: jest.fn(), logOut: jest.fn()
}}
>
<AppNavBar />,
</UserAuthContext.Provider>,
{ wrapper: BrowserRouter }
);
const navLink = page.getByText(/Logout/i, { selector: 'nav a' });
expect(navLink).toBeInTheDocument();
The code for the context wrapper returns the following which you are stubbing out (as shown above)
...
AuthContextProvider = ...
return (
<UserAuthContext.Provider
value={{ user, signIn, signUp, logOut }}
>
{children}
</UserAuthContext.Provider>
);
4. Render with default param wrapper !
The fourth technique solves an interesting challenge: what happens when the state is only maintained internally, i.e. through a useState hook in the component under test and not passed in or currently ‘wrapped’ and thus not exposed to a mechanism such as external testing to change it’s state?
One approach here is to switch the application to using a context wrapper. However this is likely not the right approach in many cases where you don’t need to create what are essentially global variables accessible through horizontal ‘pyramid of doom’ wrappers (i.e. many of them), when your only goal is to be able to set the initial and subsequent states of the component that you want to test.
This final challenge therefore requires a change to the application component. You introduce a new parameter which is the value to use for the default state. Then, in the useState hook of the application component, you use that as the initial value of the useState or useContext (as in this example) hook, i.e.
import
const SomeSet = ({ defaultSomeSet }: SomeSetProps) => {
const { someList } = defaultSomeSet || useContext(SomesContext); // <-- Use the param 'or' the context.
This then enables you to write a test and pass in parameter(s) to control the flow paths, i.e.
const defaultSomeSet = 'initial'
render(<component {defaultSomeSet} />)
—
The above techniques should give you most of the control you need for writing React tests. Good luck!