React component testing helps ensure components are performing the expected UI & UX behaviors. This post provides some tips to get started.
Table of Contents:
đ What to test
đ The render function, common queries & expects, and triggering events
đ Mocking data, functions, and libraries (+ special cases for mocking Redux and React Router)
đ Running the tests & debugging
What to Test
Component testing should cover the componentâs UI/UX behavior expectations, including how it handles different props or data input. The key here is testing the behavior, not the implementation. Easiest way to scaffold our tests is taking the product requirements and write them out in plain English that can be easily understood by product or project managers. We can start a testing block with âIt shouldâŠâ or âIt should notâŠâ to describe happy paths and fail paths perspectively.
Hereâs an example of a scaffolded test file:
// The `describe` blocks are used to explain the testing context,
// and the `it` blocks are used to lay out detailed behaviors
describe('data value is not provided', () => {
it('should render a text input', () => {
// test details
});
};
describe('data value is provided', () => {
it('should render title if one is provided', () => {
// test details
});
it('should render a picture if one is provided', () => {
// test details
})
};
describe('give suggestions for data input', () => {
it('should render suggestion titles', () => {
// test details
});
it('should not allow users to pick more than one suggested items', () => {
// test details
});
}
In general, here are a few things component testing should cover:
- The rendering of the intended HTML elements
- The rendering of elements based on props
- The events being triggered as expected and with intended results
- The state of the app, for example loading and error states
- For styling, we donât need to test the exact CSS (thatâs a whole other category called visual testing), we should test style changes if it corresponds to prop change or user interaction.
The Render Function and Basic Queries
Let me start with a basic example:
import React from 'react';
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
const makeProps = () => ({
content: 'hiking',
});
describe('MyComponent', () => {
const props = makeProps();
it('should display content', () => {
const { container } = render(<MyComponent {...props} />);
expect(container).toHaveTextContent(props.content);
expect(container.firstChild.nodeName).toBe('DIV');
});
});
Hereâs what we did:
- Use
render
from@testing-library/react
to render the component into the virtual DOM and from there we can get thecontainer
, which is the rendered HTML component. We can also get a series of methods likegetByTestId
, weâll look at those next. - We use
expect
to express that we want the container to have thecontent
we passed in as props and that it should be rendered in adiv
element. - We use a
makeProps
helper to generate props. This is not required, but itâs helpful as the tests get complicated because we can reuse this helper and pass arguments based on different contexts to get different sets of props.
Common Queries
Now letâs go through some basic queries we can utilize from the render
function.
-
There are three types of queries:
get(getAll)By-
,query(queryAll)By-
, orfind(findAll)By-
:-
In a lot of cases these three types of queries are interchangeable, however thereâre scenarios where one is more suitable. For example accessing an element that may be null, we need to use
queryBy
becausegetBy
would return error. If we want the error to be returned, we should usegetBy
. ForfindBy
, it can be used to find the first element when multiple elements exist, it can also be used when needing to await changes in the DOM.-
Example: Use
queryBy
to assert an element that should not be in the DOM
it('should not display the button', () => { const { queryByTestId } = render(<MyComponent {...props} />); const theButton = queryByTestId('the-button'); expect(theButton).toBeNull(); });
-
-
There are a variety of elements we can query against, including labelText, title, role, testId, etc. Note that using
testId
should be the last resort because it is the least accessible property by the actual users.To find multiple elements, should use the
-all
queries. These queries will return us an array of elements. To access a specific element, we can use their index to access the array, or access them viatestId
.-
To get text content inside an HTML element, use
.innerHTML
it('should display value up to 100 characters', () => { const { container, getByTestId } = render(<MyComponent {...props} />); const display = getByTestId('content').innerHTML; expect(container).toHaveTextContent(display); expect(display.length).toBe(100); });
-
When needing to find elements by partial test, Regex comes in handy. For example, I needed to find by text âWorked here forâŠâ but the exact number of working years is dynamic:
const item = getByText(/Worked here for .*/);
-
We can also use Regex to assess the working years display is of a certain format:
expect(item.innerHTML).toMatch(/^Worked here for .*years(?:(.*)months)?/gi);
-
-
To find an image, we can use
-ByAltText
, e.g.
it('should render avatar with the given url', async () => { const { getByAltText } = await render(<MyComponent />); const avatar = getByAltText('avatar'); expect(image).toHaveAttribute('src', 'the_given_url'); });
Common Expects
- Expect to be in the document:
expect(element).toBeInTheDocument
- Expect to have certain length:
expect(items.length).toBe(5)
-
Expect a link is leading to the right url:
it('should render learn more button with the link to the info site', () => { const { getByText } = render(<MyComponent />); const link = getByText('Learn more'); expect(link).toHaveAttribute('href', 'www.info.com'); expect(link).toHaveAttribute('target', '_blank'); expect(link).toHaveAttribute('rel', 'noopener noreferrer'); });
Expect text to be a certain format:
expect(text).toMatch(regex)
-
Expect function to have been called with arguments.
expect(myFunction).toHaveBeenCalledWith({ argOne: 700, argTwo: 'some text', callbackArg: expect.any(Function), });
-
Expect the rest of props are being called if spreading props via
...restOfProps
it('should pass through other props', () => { const { container } = render(<MyComponent data-testid="testID" />); expect(container.firstChild).toHaveAttribute('data-testid', 'testID'); });
-
Expect optional props can be passed in. For example, assert optional
style
is passed in:
it('should pass through additional style', () => { const { container } = render(<MyComponent style={{ maxWidth: 500 }} />); expect(container.firstChild).toHaveStyle('max-width: 500px'); });
Triggering Events
To tigger events, we use fireEvents
from the react testing library. Example:
import { render, fireEvent } from '@testing-library/react';
it('should fire authentication function when clicking sign in', () => {
const { getByTestId } = render(<MyComponent />);
const signInButton = getByTestId('sign-in-button');
expect(signInButton).toBeInTheDocument();
fireEvent.click(signInButton);
expect(myAuthFunction).toHaveBeenCalled();
});
Mocking
Since React component testing is testing for a certain component, it should be contained within the componentâs own context. Therefore the component working as expected should not rely on the inner workings of the outside libraries or external functions utilized by the component. We need to mock those functions out to give expected outcome. Similarly, our tests should not rely on successful data fetching, so we need to mock out the data our tests would receive as well.
Mock Data
âFakerâ comes in as a handy library when mocking data. You can type data out yourself too, but using âfakerâ puts the emphasis on the data format rather than the exact data content, which mimics actual user scenarios better. Here are a few handy use cases:
import { faker } from '@faker-js/faker';
// Generate one random word
const word = faker.random.word();
// Generate random text
const content = faker.random.words(50); // optional word count argument
// Generate random image
const image = faker.image.avatar();
// Generate random uuid
const id = faker.datatype.uuid();
// Generate random first and last names
const name = faker.name.firstName(), faker.name.lastName()].join(' ');
// Generate random boolean value
const booleanValue = faker.datatype.boolean();
// Generate random number with precision, for example needing levels 100 | 200 | 300
const level = faker.random.number({ min: 100, max: 300, precision: 100 });
After mocking out the data, we can either feed it directly to our test components as props, or we can feed it as mock API response, to do so, use mockReturnValue
or mockReturnValueOnce
when needing multiple mock api calls within one test.
Mock Functions
Mock functions let you spy on the behavior of a function our component is using but donât need to test for its behavior directly.
- If the function result doesnât impact the test, can just do:
const myFn = jest.fn()
-
To mock media functions, use
spyOn
and then restore it withmockRestore
afterwards.
const playSub = jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(() => {}); it('should play audio on click', () => { const { getByTitle } = render(<MyAudio id={faker.datatype.uuid()} />); const playButton = getByTitle('Play'); fireEvent.click(playButton); expect(playSub).toHaveBeenCalled(); playSub.mockRestore(); });
-
If need to mock for all tests, do mocks in
beforeAll
and cleanup inafterAll
beforeAll(() => { jest.mock(myFn, arg => ({ key: value })); }); afterAll(() => { jest.restoreAllMocks(); });
Mock Libraries
Similar idea to mocking functions, if we use functions from a library, we want to mock it out so only the function output matters.
-
Import the library weâre going to mock then use
mockImplementation
.
import lib from 'lib'; jest.mock('lib'); it('should work', () => { lib.mockImplementation(() => 'output'); });
-
If mocking a module, need to get the path, not the name of the import.
// â don't do this jest.mock('intersection'); // â do this jest.mock('lodash/intersection');
-
If mocking a library function that needs initialization:
// â don't do this jest.mock('lib', () => ({})); // â do this jest.mock('lib', () => () => ({}));
Special case: Mock Redux store
To test with Redux, we need to mock the store and render the test component with the provider and provided store. To solve this, we can a renderWithStore
helper that will:
- Import our store
- Provide our test component with the needed redux provider wrapper
- Wraps the
render
function from the testing library - Then in the testing file,
renderWithStore
can be used instead ofrender
when needing access to redux
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
// Add store imports
import myStore from 'store/myStore';
const rootReducer = combineReducers({
myStore
// ...and other needed stores
});
const renderWithStore = (
ui,
{
initialState,
store = createStore(
rootReducer,
initialState,
),
...renderOptions
} = {},
) => {
const Wrapper = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
return render(ui, { wrapper: Wrapper, ...renderOptions });
};
Special case: Mock React Router
For react router, we need to wrap our test components in the Router and use createMemoryHistory
from the âhistoryâ library to mock the memory history.
import { Router } from 'react-router-dom';
// Note that BrowserRouter ignores history prop, so need to use Router if need to use mockHistory
import { createMemoryHistory } from 'history';
import { render, fireEvent } from '@testing-library/react';
it('should navigate to my_page when the buton is clicked', () => {
const mockHistory = createMemoryHistory({ initialEntries: ['/home'] });
const { getByText } = render(
<Router history={mockHistory}>
<MyComponent />
</Router>,
);
const button = getByText('Check out my page');
fireEvent.click(button);
expect(mockHistory.location.pathname).toMatch(/my_page/);
});
-
If
useLocation
from react-router impacts the test, we need to mock it out as well, otherwise we would get error saying max depth call reached.
jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), useLocation: () => ({ pathname: mockPath, key: `${mockPath}-mockKey`, }), }));
Running the Tests & Debugging
-
To run the tests, do
npm test
in the command line. This will enter watch mode and automatically run and rerun all new or modified tests.- You can then press
a
in the watch mode to run all tests, orf
to run only failed tests. - To run a specific test file, put the file path name into your command line:
npm test src/components/MyComponent/MyComponent.test.js
- To run multiple specific test files, do
npm test testPath anotherTestPath
- If youâd like auto completing path name to work in the command line, instead of
npm test
, youâll need to donode_modules/.bin/jest
- If youâd like auto completing path name to work in the command line, instead of
-
To run a single test, do
.only
afterit
; to skip a test, dox
in front ofit
// Run only this test it.only('should do something', () => {}); // Skip this test xit('should not do something', () => {});
- You can then press
-
Use
debug
to visualize what the render method has created in the virtual DOM
it('should do something', () => { const { debug, container } = render(<MyComponent />); debug(); // will show the component in html format in the debugger log });
-
Use vscode debugger to pause and start to inspect into a test
-
In the root directory, create
.vscode
folder, in there, create alaunch.json
file and put in the following code:
{ "version": "0.2.0", "configurations": [ { "name": "Run React tests", "type": "node", "request": "launch", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", "args": ["test", "--runInBand", "--no-cache", "--env=jsdom"], "cwd": "${workspaceRoot}", "protocol": "inspector", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" } ] }
And then can use the play button in the debugger panel to run the test. Running the tests this way will let us step into the breakpoints we add in the code.
-
- Make sure providers are provided when needed â for libraries that consume context, for example styling libraries with a theming object, we should make sure their provider is imported and wrapped around our test components.
-
If you are using Typescript and getting error that
data-testid
isnât a defined prop, you can extend the html element prop the component is consuming so thatdata-testid
is included. Example:
interface Props extends React.ComponentProps<'div'>{ // data-testid is an optional prop for the div elments myPropOne: React.ReactNode; myPropTwo: string; }
Thatâs it for now. Hope this post covered some general questions you have when starting with React component testing. You might feel that there are so much to wrap your head around, donât get intimidated, it will get easier once you get into the groove. Have fun testing!
Top comments (0)