-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
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
Changes from 8 commits
06172a0
8b01244
352bdcc
d8a2398
dc28df5
ba6825d
4fd1541
e5f95f1
3533f61
6556244
5bc9025
995e313
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
||
A recommended test to ensure basic functionality looks like the following: | ||
|
||
```jsx | ||
test('<App /> renders successfully', () => { | ||
render(<App />, { wrapper: Router }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (and also show the RTL imports) |
||
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 }); | ||
thatzacdavis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checked the docs, you're correct that I'm not sure how to set the equivalent of this initialEntries={["/the-old-thing"]} using Is it acceptable to directly access There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really know why |
There was a problem hiding this comment.
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.