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
Closed
110 changes: 107 additions & 3 deletions docs/advanced-guides/testing/testing-with-react-testing-library.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,117 @@
# Testing With React Testing Library
# Testing With React Testing Library (RTL)

## 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).

## Basic Test

A basic rendering test can be important to esnure that we have everything installed and set up correctly. Fortunately, this is easy to do with RTL.

Since we've wrapped our `App` component in the `Router` in our `index.js` file, we do have to wrap it in each of our isolated 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.

A recommended simple test looks like the following:

```jsx
test('renders react router header', () => {
const { getByText } = render(<Router><App /></Router>);
Copy link
Contributor

@alexkrolick alexkrolick Mar 5, 2020

Choose a reason for hiding this comment

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

We're presently recommending people import screen from RTL instead of destructuring the render result. This works better with autocomplete and makes each test a little simpler.

👀 @kentcdodds

Copy link
Member

Choose a reason for hiding this comment

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

Yup, so this would be:

import {screen, render} from '@testing-library/react';

test('renders react router header', () => {
  render(<Router><App /></Router>);
  const header = screen.getByText('Welcome to React Router!');
  expect(header).toBeInTheDocument();
})

It would be even better to recommend the wrapper option so rerender works as expected:

import {screen, render} from '@testing-library/react';

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

And then to take it to the next level, there's a custom render: https://testing-library.com/docs/react-testing-library/setup#custom-render

Hope that context is helpful!

Copy link
Author

Choose a reason for hiding this comment

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

I see that in the repo README @alexkrolick but not in the docs site, the destructuring is still there, which is what I've been going off of for a while.

I can definitely update these examples, and I can also put in a PR for the RTL docs.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, we're still in the process of updating things to the new recommendations. PRs would be appreciated!

Copy link
Author

Choose a reason for hiding this comment

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

No worries, I can definitely help with that.

Thanks for all the help with this! I'll work on some updates for this tonight.

const header = getByText('Welcome to React Router!');
expect(header).toBeInTheDocument();
});
```

This ensures that we can render our `App` component and that the `h1` we put in during our setup guide is in the document.

## Testing Routes and Redirects

TODO

## Testing Links and Navigation

TODO
Testing a link and the subsequent navigation with React Testing Library is as easy as firing a click event on the link itself and asserting on the window's location to test that it worked.

This is accomplished like so:

```jsx
it('goes to about when link clicked', () => {
const { getByText } = render(<Router><App /></Router>);

const aboutLink = getByText('About');
act(() => {
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 easily test that we end up back on the home page by triggering a second click, this time on the home link:

```jsx
it('goes to about when link clicked and then back to home when link clicked', () => {
const { getByText } = render(<Router><App /></Router>);

const aboutLink = getByText('About');
act(() => {
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 = getByText('Home');
act(() => {
fireEvent.click(homeLink);
});
expect(window.location.pathname).toBe('/');
});
```

This test will initially fail if we run it right 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 ran together.

Below is the full file to help see all of the tests together:

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

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

afterEach(cleanup);
Copy link
Member

Choose a reason for hiding this comment

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

Oh yeah, and cleanup is unnecessary and happens automatically now: https://testing-library.com/docs/react-testing-library/api#cleanup


test('renders react router header', () => {
const { getByText } = render(<Router><App /></Router>);
const header = getByText('Welcome to React Router!');
expect(header).toBeInTheDocument();
});

it('goes to about when link clicked', () => {
const { getByText } = render(<Router><App /></Router>);

const aboutLink = getByText('About');
act(() => {
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't need to use act around fireEvent because it's already wrapped in act.

You should rarely need to use act in regular tests. For more on this: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning

fireEvent.click(aboutLink);
});

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

it('goes to about when link clicked and then back to home when link clicked', () => {
const { getByText } = render(<Router><App /></Router>);

const aboutLink = getByText('About');
act(() => {
fireEvent.click(aboutLink);
});
expect(window.location.pathname).toBe('/about');

const homeLink = getByText('Home');
act(() => {
fireEvent.click(homeLink);
});
expect(window.location.pathname).toBe('/');
});

```