Skip to content

Testing v6 w/ React Testing Library Docs #7169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
164 changes: 159 additions & 5 deletions docs/advanced-guides/testing/testing-with-react-testing-library.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,167 @@
# Testing With React Testing Library
# Testing With React Testing Library (RTL)

`react-testing-library` is a lightweight family of packages that help us test UI components in a manner that resembles the way users interact with our applications.

To quote their (very excellent) documentation:

The more your tests resemble the way your software is used, the more confidence they can give you.

https://testing-library.com/docs/intro

## Getting Setup

TODO
This guide assumes you followed the instructions for [Adding React Router via Create React App](../../installation/add-to-a-website.md#create-react-app). If you did not start with Create React App, but still have the same application structure, you can install [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) by following [their installation guide](https://github.com/testing-library/react-testing-library#installation).

## Testing Routes and Redirects
## Basic Test

A basic rendering test ensures that we have everything installed and set up correctly. Fortunately, RTL gives us the tools to accomplish this.

Since we've wrapped our `HomePage` (renamed from `App` after following the aforementioned instructions) component in the `Router` in our `index.js` file, we do have to wrap it around each of our individual component tests, otherwise `history` will be undefined. If the `Router` had been inside of our `App`, then we would not have to wrap it inside of our tests.

TODO
A recommended test to ensure basic functionality looks like the following:

```jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import HomePage from './HomePage';
import { BrowserRouter as Router } from 'react-router-dom';

test('<HomePage /> renders successfully', () => {
render(<HomePage />, { wrapper: Router });
const header = screen.getByText('Welcome to React Router!');
expect(header).toBeInTheDocument();
});
```

This test ensures that the `App` component renders, and that the `h1` we added during the setup guide exists in the document.

## Testing Links and Navigation

TODO
`react-router` has a lot of tests verifying that the routes work when the location changes, so you probably don't need to test this stuff.

If you need to test navigation within your app, you can do so by firing a click event on the link itself and asserting that the path changed to the expected value.

This is accomplished like so:

```jsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import HomePage from './HomePage';
import { BrowserRouter as Router } from 'react-router-dom';

it('navigates to /about', () => {
render(<HomePage />, { wrapper: Router });

const aboutLink = screen.getByText('About');
fireEvent.click(aboutLink);

expect(window.location.pathname).toBe('/about');
});
```

Since our setup guide then replaces the about link with a link back to the home page, we can test navigating back to the home page by triggering another click, this time on the home link:

```jsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import HomePage from './HomePage';
import { BrowserRouter as Router } from 'react-router-dom';

it('navigates to /about and back to /', () => {
render(<HomePage />, { wrapper: Router });

const aboutLink = screen.getByText('About');
fireEvent.click(aboutLink);

expect(window.location.pathname).toBe('/about');
Copy link
Contributor

@alexkrolick alexkrolick Mar 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be the case if you use an in-memory router (it does not update the window.location). I think there are different opinions about this. The downside of using a "full" router is that JSDOM doesn't give you a way to reset the navigation stack, so if you do something like "go back 3 times" you could end up on a previous test's URL (even if you reset the initial location as described below, the number of items on the stack doesn't reset). On the other hand, memory router doesn't represent what's going in the actual app as well, especially if you have some regular <a> tags mixed in or manual manipulation of the URL.

Copy link

@sadsa sadsa Sep 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using MemoryRouter in my tests and using a test component as a the route to redirect to.

const RedirectPage = () => {
  const [searchParams] = useSearchParams();
  const action = searchParams.get("action");

  if (!action) return <Navigate to="/" />;

  return <></>;
};

 test("Should redirect to the home screen if 'action' query param is missing", async () => {
    function HomePage() {
      return <h1>Home</h1>;
    }
    const { findByRole } = render(
        <Router initialEntries={["/redirect"]}>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/redirect" element={<RedirectPage />} />
          </Routes>
        </Router>
    );
    const homePageHeading = await findByRole(/heading/i);
    expect(homePageHeading).toBeVisible();
  });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if you test the page contents, you can make an assertion. But you can't assert window.location.pathname with MemoryRouter, because it doesn't update that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I've moved away from using the MemoryRouter and instead I render the same router that's rendered in my source code and interact with window.location directly instead: https://github.com/kentcdodds/bookshelf/blob/d9d70fceaea790dfe57ee5a46c13f9f9cc810675/src/test/app-test-utils.js#L15

Works way better. And I get more confidence out of my tests.


const homeLink = screen.getByText('Home');
fireEvent.click(homeLink);

expect(window.location.pathname).toBe('/');
});
```

This test will initially fail if it is run immediately after the previous test. This is because the history stack still thinks we are on the about page. To fix this, we can use Jest's `beforeEach` function to replace our history state using: `window.history.replaceState({}, '', '/');`. This makes `react-router` think that we are on the home page when we start each test. Now, all three tests should be passing when run together.

Below is the full file with all of the tests together:

`HomePage.test.js`
```jsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import HomePage from './HomePage';
import { BrowserRouter as Router } from 'react-router-dom';

beforeEach(() => {
window.history.replaceState({}, '', '/');
});

test('<HomePage /> renders successfully', () => {
render(<HomePage />, { wrapper: Router });
const header = screen.getByText('Welcome to React Router!');
expect(header).toBeInTheDocument();
});

it('navigates to /about', () => {
render(<HomePage />, { wrapper: Router });

const aboutLink = screen.getByText('About');
fireEvent.click(aboutLink);

expect(window.location.pathname).toBe('/about');
});

it('navigates to /about and back to /', () => {
render(<HomePage />, { wrapper: Router });

const aboutLink = screen.getByText('About');
fireEvent.click(aboutLink);

expect(window.location.pathname).toBe('/about');

const homeLink = screen.getByText('Home');
fireEvent.click(homeLink);

expect(window.location.pathname).toBe('/');
});
```

## Testing Routes and Redirects

Let's say that we've built a redirect component that deals with re-routing the user from `/the-old-thing` to `/the-new-thing`

`RedirectHandler.js`
```jsx
import React from 'react'
import { Redirect } from 'react-router-dom'

const RedirectHandler = () => <Redirect from="/the-old-thing" to="the-new-thing" />;

export default RedirectHandler;
```

And here we have one method of testing that the redirect is behaving as expected.

`RedirectHandler.test.js`
```jsx
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { render } from "@testing-library/react";
import RedirectHandler from "./RedirectHandler";

it("redirects /the-old-thing to /the-new-thing", () => {
window.history.pushState({}, "The Old Thing", "/the-new-thing");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to pushState() to /the-new-thing at the beginning of the test? It seems to me that if this is how the history is set up, then we aren't really testing whether the redirect is happening.

Am I misunderstanding how pushState is functioning here?


render(
<BrowserRouter>
<RedirectHandler />
</BrowserRouter>
);

expect(window.location.pathname).toBe("/the-new-thing");
});

```

This test initializes the `BrowserRouter` with an initial path of `/the-old-thing` and verifies that the `RedirectHandler` correctly changes the path to `/the-new-thing`.