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
150 changes: 145 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,153 @@
# 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.

TODO
Since we've wrapped our `App` 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Even though I read the above section about "Adding RR via CRA" I find it a bit confusing to hold all these references in my head. I think it would help to explain the exact structure that is required and when you'll need to wrap with Router, in a general way without referencing the example files.


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

```jsx
test('<App /> renders successfully', () => {
render(<App />, { wrapper: Router });
Copy link
Contributor

Choose a reason for hiding this comment

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

Should also show the imports - is this a memory router or other kind? What JSDOM setup is required, if any?

Copy link
Contributor

Choose a reason for hiding this comment

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

(and also show the RTL imports) import {screen, render} from '@testing-library/react'

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
it('navigates to /about', () => {
render(<App />, { 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
it('navigates to /about and back to /', () => {
render(<App />, { 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:

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

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

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

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

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

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

it('navigates to /about and back to /', () => {
render(<App />, { 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 {Route, Switch, Redirect} from 'react-router-dom'

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

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 {MemoryRouter, Route} from 'react-router-dom'
import { render, fireEvent, screen } from '@testing-library/react';
import RedirectHandler from './RedirectHandler'

it('redirects /the-old-thing to /the-new-thing', () => {
render(
<MemoryRouter initialEntries={["/the-old-thing"]}>
<Route component={RedirectHandler}>
</MemoryRouter>
)

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

```

This test initializes the `MemoryRouter` with an initial path of `/the-old-thing` and verifies that the `RedirectHandler` correctly changes the path to `/the-new-thing`
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.

Will this actually happen with a MemoryRouter? I don't think it updates the location. But you could read from the history object (or not use an in-memory router).

Choose a reason for hiding this comment

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

Just checked the docs, you're correct that MemoryRouter won't update window.location.

I'm not sure how to set the equivalent of this

initialEntries={["/the-old-thing"]}

using BrowserRouter, in order to set up for the redirect.

Is it acceptable to directly access history.location for testing purposes?

Copy link
Contributor

@alexkrolick alexkrolick Mar 7, 2020

Choose a reason for hiding this comment

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

I don't really know why initialEntries exists (maybe to restore across sessions?). Pushing history and waiting for redirect to result in the new URL seems more realistic to me for web use cases. Again, there are limitations to both approaches. window.location.href = doesn't trigger navigation in JSDOM, for example.