DEV Community

Cover image for How to test react-router redirection with testing-library
Clément Latzarus
Clément Latzarus

Posted on

How to test react-router redirection with testing-library

React testing-library is very convenient to test React components rendering from props, fire events and check DOM elements. react-router uses a <Redirect> component to trigger a redirect, but how can we test that this component is called using testing-library?

Let’s say we have a CreateBookForm component that creates a new book. It calls our API when the form is submitted.

// BookCreateForm.js import React, { useState } from 'react'; import api from './api'; function CreateBookForm() { const [title, setTitle] = useState(''); async function handleSubmit(event) { event.preventDefault(); await api.createBook({ title }); } return ( <form onSubmit={handleSubmit}> <input placeholder="Book's title" value={title} onChange={(event) => setTitle(event.target.value)} /> <button>Create book</button> </form> ); } export default CreateBookForm; 
Enter fullscreen mode Exit fullscreen mode

It's easy to test that our api is called when the form is submitted with testing-library:

// BookCreateForm.test.js import React from 'react'; import { render, act, fireEvent, waitFor } from '@testing-library/react'; import BookCreateForm from './BookCreateForm'; import api from './api'; jest.mock('./api'); test('it calls api on form submit', async () => { api.createBook = jest.fn(() => Promise.resolve({ id: 1 })); const { getByPlaceholderText, getByText, findByDisplayValue } = render(<BookCreateForm />); await act(async () => { const input = getByPlaceholderText(/Book's title/); fireEvent.change(input, { target: { value: 'Yama Loka Terminus' }}); await findByDisplayValue(/Yama Loka Terminus/); const button = getByText(/Create book/); fireEvent.click(button); }); expect(api.createBook).toHaveBeenCalledWith({ title: 'Yama Loka Terminus' }); }); 
Enter fullscreen mode Exit fullscreen mode

Now, let's say we want our component to redirect to the new book page once it's created.

// BookCreateForm.js import React, { useState } from 'react'; import { Redirect } from 'react-router-dom' import api from './api'; function CreateBookForm() { const [title, setTitle] = useState(''); const [createdId, setCreatedId] = useState(null); async function handleSubmit(event) { event.preventDefault(); const { id } = await api.createBook({ title }); setCreatedId(id); } return createdId ? <Redirect to={`/book/${createdId}`}/> : ( <form onSubmit={handleSubmit}> <input placeholder="Book's title" value={title} onChange={(event) => setTitle(event.target.value)} /> <button>Create book</button> </form> ); } export default CreateBookForm; 
Enter fullscreen mode Exit fullscreen mode

We'll probably have a router wrapping our form and a BookPage component:

// App.js function App() { return ( <div className="App"> <BrowserRouter> <Route path="/book/create"> <BookCreateForm /> </Route> <Route path="/book/:id"> <BookPage /> </Route> </BrowserRouter> </div> ); } 
Enter fullscreen mode Exit fullscreen mode

Now, our test runner will complain that we use <Redirect> outside of a router, so let's wrap our component test into one.

// BookCreateForm.test.js // … import { BrowserRouter } from 'react-router-dom'; // … const { container, getByPlaceholderText, getByText, findByDisplayValue } = render(<BrowserRouter><BookCreateForm /></BrowserRouter>); // … 
Enter fullscreen mode Exit fullscreen mode

Everything is working fine, but how can we ensure that our form component is redirecting to the new page after the api's response?

That's a tricky question and I've been struggling with this. I've seen some complex solutions involving creating fake routers or mocking the react-router module. But there's actually a pretty simple way to test this.

If we try to snapshot our component after our API was called, we'll notice that it renders an empty div.

expect(container).toMatchInlineSnapshot(`<div />`); 
Enter fullscreen mode Exit fullscreen mode

That's because the redirection indeed happened, but there was no route to redirect to. From the testing-library renderer perspective, they are no routes defined, we just ask it to render and empty router containing the form.

To ensure that our user gets redirected to /book/1 (as the book's id returned by our API mock is 1), we can add a route for that specific url with a simple text as children.

 const { container, getByPlaceholderText, getByText, findByDisplayValue } = render( <BrowserRouter> <BookCreateForm /> <Route path="/book/1">Book page</Route> </BrowserRouter> ); 
Enter fullscreen mode Exit fullscreen mode

And test that the component rendered the text:

expect(container).toHaveTextContent(/Book page/); 
Enter fullscreen mode Exit fullscreen mode

Our final test :

// BookCreateForm.test.js import React from 'react'; import { render, act, fireEvent } from '@testing-library/react'; import { BrowserRouter, Route } from 'react-router-dom'; import BookCreateForm from './BookCreateForm'; import api from './api'; jest.mock('./api'); test('it calls api on form submit', async () => { api.createBook = jest.fn(() => Promise.resolve({ id: 1 })); const { container, getByPlaceholderText, getByText, findByDisplayValue } = render( <BrowserRouter> <BookCreateForm /> <Route path="/book/1">Book page</Route> </BrowserRouter> ); await act(async () => { const input = getByPlaceholderText(/Book's title/); fireEvent.change(input, { target: { value: 'Yama Loka Terminus' }}); await findByDisplayValue(/Yama Loka Terminus/); const button = getByText(/Create book/); fireEvent.click(button); }); expect(api.createBook).toHaveBeenCalledWith({ title: 'Yama Loka Terminus' }); expect(container).toHaveTextContent(/Book page/); }); 
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
akoliyot profile image
Aswin Koliyot

Nice one!

Collapse
 
gaurav5430 profile image
Gaurav Gupta

instead of creating a fake route, can we not assert on the url change ?