"The more your tests resemble the way your software is used, the more
confidence they can give you." -
@kentcdodds
To just get a basic component logging to our terminal during a test, we can have:
import * as React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 create a div to render your component to (💰 document.createElement)
const div = document.createElement('div');
// 🐨 append the div to document.body (💰 document.body.append)
document.body.append(div);
// 🐨 use ReactDOM.render to render the <Counter /> to the div
ReactDOM.render(<Counter />, div);
console.log(document.body.innerHTML);
});
To start validating messages from the div we created, we could do the following (for the example of the counter with a single text element):
// simple test with ReactDOM
// http://localhost:3000/counter
import * as React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 create a div to render your component to (💰 document.createElement)
const div = document.createElement('div');
// 🐨 append the div to document.body (💰 document.body.append)
document.body.append(div);
// 🐨 use ReactDOM.render to render the <Counter /> to the div
ReactDOM.render(<Counter />, div);
// 🐨 get a reference to the message div:
const message = div.firstChild.querySelector('div');
//
// 🐨 expect the message.textContent toBe 'Current count: 0'
expect(message.textContent).toBe('Current count: 0');
});
To now test the clicking of the buttons, we can do the following:
// simple test with ReactDOM
// http://localhost:3000/counter
import * as React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 create a div to render your component to (💰 document.createElement)
const div = document.createElement('div');
// 🐨 append the div to document.body (💰 document.body.append)
document.body.append(div);
// 🐨 use ReactDOM.render to render the <Counter /> to the div
ReactDOM.render(<Counter />, div);
// 🐨 get a reference to the increment and decrement buttons:
const [decrement, increment] = div.querySelectorAll('button');
// 🐨 get a reference to the message div:
// 💰 div.firstChild.querySelector('div')
const message = div.firstChild.querySelector('div');
//
// 🐨 expect the message.textContent toBe 'Current count: 0'
expect(message.textContent).toBe('Current count: 0');
// 🐨 click the increment button (💰 increment.click())
increment.click();
// 🐨 assert the message.textContent
expect(message.textContent).toBe('Current count: 1');
// 🐨 click the decrement button (💰 decrement.click())
decrement.click();
// 🐨 assert the message.textContent
expect(message.textContent).toBe('Current count: 0');
//
// 🐨 cleanup by removing the div from the page (💰 div.remove())
// 🦉 If you don't cleanup, then it could impact other tests and/or cause a memory leak
div.remove();
});
But instead of div.remove, it is better for us to set a before hook. The reason being that if a test fails, it could cause subsequent failures at the clean up did not occur.
beforeEach(() => (document.body.innerHTML = ''));
Use dispatchEvent
To follow more closely with what happens when the user clicks in React, we change to use dispatchEvent:
// simple test with ReactDOM
// http://localhost:3000/counter
import * as React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../../components/counter';
beforeEach(() => (document.body.innerHTML = ''));
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 create a div to render your component to (💰 document.createElement)
const div = document.createElement('div');
// 🐨 append the div to document.body (💰 document.body.append)
document.body.append(div);
// 🐨 use ReactDOM.render to render the <Counter /> to the div
ReactDOM.render(<Counter />, div);
// 🐨 get a reference to the increment and decrement buttons:
const [decrement, increment] = div.querySelectorAll('button');
const incrementClickEvent = new MouseEvent('click', {
// required for event delgation to work (required by React)
bubbles: true,
cancelable: true,
button: 0,
});
const decrementClickEvent = new MouseEvent('click', {
// required for event delgation to work (required by React)
bubbles: true,
cancelable: true,
button: 0,
});
// 🐨 get a reference to the message div:
// 💰 div.firstChild.querySelector('div')
const message = div.firstChild.querySelector('div');
//
// 🐨 expect the message.textContent toBe 'Current count: 0'
expect(message.textContent).toBe('Current count: 0');
increment.dispatchEvent(incrementClickEvent);
// 🐨 assert the message.textContent
expect(message.textContent).toBe('Current count: 1');
// 🐨 click the decrement button (💰 decrement.click())
decrement.dispatchEvent(decrementClickEvent);
// 🐨 assert the message.textContent
expect(message.textContent).toBe('Current count: 0');
//
// 🐨 cleanup by removing the div from the page (💰 div.remove())
// 🦉 If you don't cleanup, then it could impact other tests and/or cause a memory leak
div.remove();
});
Simple Test With React Testing Library
Liked the above example, but did not enjoy the boilerplate. Implementing react-testing-library looks like so:
// simple test with React Testing Library
// http://localhost:3000/counter
import * as React from 'react';
// 🐨 import the `render` and `fireEvent` utilities from '@testing-library/react'
import { render, fireEvent } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 swap ReactDOM.render with React Testing Library's render
// Note that React Testing Library's render doesn't need you to pass a `div`
// so you only need to pass one argument. render returns an object with a
// bunch of utilities on it. For now, let's just grab `container` which is
// the div that React Testing Library creates for us.
const { container } = render(<Counter />);
// 🐨 instead of `div` here you'll want to use the `container` you get back
// from React Testing Library
const [decrement, increment] = container.querySelectorAll('button');
const message = container.firstChild.querySelector('div');
expect(message.textContent).toBe('Current count: 0');
// 🐨 replace the next two statements with `fireEvent.click(button)`
fireEvent.click(increment);
expect(message.textContent).toBe('Current count: 1');
fireEvent.click(decrement);
expect(message.textContent).toBe('Current count: 0');
});
One of the slight differences is that fireEvent is automatically wrapped in ReactTestUtils act() function. If you ever see an act warning, then that's absolutely something that you probably need to deal with, but you never need to wrap a call to fireEvent in act.
The only reason I'm mentioning that to you is because I see it all the time. If you ever come across an act warning, your solution is not to wrap the fireEvent call in act. The solution will be something else entirely.
Using jest-dom for better messages
We can import @testing-library/jest-dom into the file (or in a setup file) to extend our Jest assertions to include things such as .toHaveTextContent for better error messaging. Once done we can update our code to look like so:
// simple test with React Testing Library
// http://localhost:3000/counter
import * as React from 'react';
// 🐨 import the `render` and `fireEvent` utilities from '@testing-library/react'
import { render, fireEvent } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
// 🐨 swap ReactDOM.render with React Testing Library's render
// Note that React Testing Library's render doesn't need you to pass a `div`
// so you only need to pass one argument. render returns an object with a
// bunch of utilities on it. For now, let's just grab `container` which is
// the div that React Testing Library creates for us.
const { container } = render(<Counter />);
// 🐨 instead of `div` here you'll want to use the `container` you get back
// from React Testing Library
const [decrement, increment] = container.querySelectorAll('button');
const message = container.firstChild.querySelector('div');
expect(message).toHaveTextContent('Current count: 0');
// 🐨 replace the next two statements with `fireEvent.click(button)`
fireEvent.click(increment);
expect(message).toHaveTextContent('Current count: 1');
fireEvent.click(decrement);
expect(message).toHaveTextContent('Current count: 0');
});
Avoid Implementation Details
The implementation of your abstractions does not matter to the users of your abstraction and if you want to have confidence that it continues to work through refactors then neither should your tests.
The only difference between these implementations is one wraps the button in a
span and the other does not. The user does not observe or care about this
difference, so we should write our tests in a way that passes in either case.
So here's a better way to search for that button in our test that's
implementation detail free and refactor friendly:
render(<Counter />);
screen.getByText('0'); // <-- that's the button
// or (even better) you can do this:
screen.getByRole('button', { name: '0' }); // <-- that's the button
In the example, we did the following change:
// Avoid implementation details
// INITIAL CODE
import * as React from 'react';
// 🐨 add `screen` to the import here:
import { render, fireEvent } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
const { container } = render(<Counter />);
// 🐨 replace these with screen queries
// 💰 you can use `getByText` for each of these (`getByRole` can work for the button too)
const [decrement, increment] = container.querySelectorAll('button');
const message = container.firstChild.querySelector('div');
expect(message).toHaveTextContent('Current count: 0');
fireEvent.click(increment);
expect(message).toHaveTextContent('Current count: 1');
fireEvent.click(decrement);
expect(message).toHaveTextContent('Current count: 0');
});
// Avoid implementation details
// FIRST CHANGE
import * as React from 'react';
// 🐨 add `screen` to the import here:
import { render, fireEvent, screen } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
render(<Counter />);
// 🐨 replace these with screen queries
const decrement = screen.getByText('Decrement');
const increment = screen.getByText('Increment');
const message = screen.getByText('Current count: 0');
expect(message).toHaveTextContent('Current count: 0');
fireEvent.click(increment);
expect(message).toHaveTextContent('Current count: 1');
fireEvent.click(decrement);
expect(message).toHaveTextContent('Current count: 0');
});
// Avoid implementation details
// FINAL CHANGE
import * as React from 'react';
// 🐨 add `screen` to the import here:
import { render, fireEvent, screen } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
render(<Counter />);
// 🐨 replace these with screen queries
const decrement = screen.getByRole('button', { text: /decrement/i });
const increment = screen.getByRole('button', { text: /increment/i });
const message = screen.getByText(/current count/i);
expect(message).toHaveTextContent('Current count: 0');
fireEvent.click(increment);
expect(message).toHaveTextContent('Current count: 1');
fireEvent.click(decrement);
expect(message).toHaveTextContent('Current count: 0');
});
The screen utility can be used to find computations based on attributes that can found using the accessibility tab.
There is information on priority for testing implementation details that can be found here.
Browser events (handling more than just a click)
If the implementation detail is changed for the event that fires it (by is a subtle relation to a click), our tests will break. To be resilient to this (or to test similar interactions) then we can do the following:
// Avoid implementation details
// http://localhost:3000/counter
import * as React from 'react';
// 🐨 add `screen` to the import here:
import { render, userEvent, screen } from '@testing-library/react';
import Counter from '../../components/counter';
test('counter increments and decrements when the buttons are clicked', () => {
render(<Counter />);
// 🐨 replace these with screen queries
const decrement = screen.getByRole('button', { text: /decrement/i });
const increment = screen.getByRole('button', { text: /increment/i });
const message = screen.getByText(/current count/i);
expect(message).toHaveTextContent('Current count: 0');
userEvent.click(increment);
expect(message).toHaveTextContent('Current count: 1');
userEvent.click(decrement);
expect(message).toHaveTextContent('Current count: 0');
});
All we need to change is fireEvent to userEvent. When running click with a userEvent, it will fire all kinds of events for us to test these different scenarions that a user may make.
Form Testing
To test our form, we can first debug what is on the screen to render our the current HTML from the component that is rendered:
// form testing
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Login from '../../components/login';
test('submitting the form calls onSubmit with username and password', () => {
render(<Login />);
screen.debug();
});
We can assert that our form works as expected by updating the code to the following:
// form testing
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Login from '../../components/login';
test('submitting the form calls onSubmit with username and password', () => {
let submittedData;
const handleSubmit = data => (submittedData = data);
render(<Login onSubmit={handleSubmit} />);
const username = 'chucknorris';
const password = 'i need no password';
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(submittedData).toEqual({
username,
password,
});
});
Using a Jest Mock function
This is us listening and assert what the onSubmit function call is passed and how many times it is fired:
// form testing
// 💯 use a jest mock function
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Login from '../../components/login';
test('submitting the form calls onSubmit with username and password', () => {
const handleSubmit = jest.fn();
render(<Login onSubmit={handleSubmit} />);
const username = 'chucknorris';
const password = 'i need no password';
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username,
password,
});
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
Generate test data
"Something to keep in mind is that people are going to be reading this test in the future. They may not know the implementation of login and what decisions were made and what's important. Everything that they see in the test, they're going to assume it's important. They're going to assume that it's important that we render the login with an onSubmit prop."
We can use Faker to help the user know that the implementation value is not important.
// form testing
// 💯 generate test data
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import faker from 'faker';
import Login from '../../components/login';
function buildLoginForm() {
return {
username: faker.internet.userName(),
password: faker.internet.password(),
};
}
test('submitting the form calls onSubmit with username and password', () => {
const handleSubmit = jest.fn();
render(<Login onSubmit={handleSubmit} />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username,
password,
});
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
Allow for overrides
We just allow overrides so that a user can handle special cases.
// form testing
// 💯 allow for overrides
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import faker from 'faker';
import Login from '../../components/login';
// overrides is the important part
function buildLoginForm(overrides) {
return {
username: faker.internet.userName(),
password: faker.internet.password(),
...overrides,
};
}
test('submitting the form calls onSubmit with username and password', () => {
const handleSubmit = jest.fn();
render(<Login onSubmit={handleSubmit} />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username,
password,
});
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
Use Test Data Bot
This is using a test data bot utility @jackfranklin/test-data-bot to help create test factories that can automatically be overriden.
// form testing
// 💯 use Test Data Bot
// http://localhost:3000/login
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { build, fake } from '@jackfranklin/test-data-bot';
import Login from '../../components/login';
const buildLoginForm = build({
fields: {
username: fake(f => f.internet.userName()),
password: fake(f => f.internet.password()),
},
});
test('submitting the form calls onSubmit with username and password', () => {
const handleSubmit = jest.fn();
render(<Login onSubmit={handleSubmit} />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
username,
password,
});
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
// mocking HTTP requests
// 💯 reuse server request handlers
// http://localhost:3000/login-submission
import * as React from 'react';
import {
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { build, fake } from '@jackfranklin/test-data-bot';
import { setupServer } from 'msw/node';
import { handlers } from 'test/server-handlers';
import Login from '../../components/login-submission';
const buildLoginForm = build({
fields: {
username: fake(f => f.internet.userName()),
password: fake(f => f.internet.password()),
},
});
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());
test(`logging in displays the user's username`, async () => {
render(<Login />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByText(username)).toBeInTheDocument();
});
Testing the unhappy path
// mocking HTTP requests
// 💯 test the unhappy path
// http://localhost:3000/login-submission
import * as React from 'react';
import {
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { build, fake } from '@jackfranklin/test-data-bot';
import { setupServer } from 'msw/node';
import { handlers } from 'test/server-handlers';
import Login from '../../components/login-submission';
const buildLoginForm = build({
fields: {
username: fake(f => f.internet.userName()),
password: fake(f => f.internet.password()),
},
});
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());
test(`logging in displays the user's username`, async () => {
render(<Login />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByText(username)).toBeInTheDocument();
});
test('omitting the password results in an error', async () => {
render(<Login />);
const { username } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
// don't type in the password
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByRole('alert')).toHaveTextContent('password required');
});
Use inline snapshots
It is not great to hardcode things such as "error messages" in case the error message ever changes.
// mocking HTTP requests
// 💯 use inline snapshots for error messages
// http://localhost:3000/login-submission
import * as React from 'react';
import {
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { build, fake } from '@jackfranklin/test-data-bot';
import { setupServer } from 'msw/node';
import { handlers } from 'test/server-handlers';
import Login from '../../components/login-submission';
const buildLoginForm = build({
fields: {
username: fake(f => f.internet.userName()),
password: fake(f => f.internet.password()),
},
});
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());
test(`logging in displays the user's username`, async () => {
render(<Login />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByText(username)).toBeInTheDocument();
});
test('omitting the password results in an error', async () => {
render(<Login />);
const { username } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
// don't type in the password
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByRole('alert').textContent).toMatchInlineSnapshot(
`"password required"`,
);
});
Using one-off server handlers
This is an ability to override the current handlers that have been implemented.
// mocking HTTP requests
// 💯 use one-off server handlers
// http://localhost:3000/login-submission
import * as React from 'react';
import {
render,
screen,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { build, fake } from '@jackfranklin/test-data-bot';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { handlers } from 'test/server-handlers';
import Login from '../../components/login-submission';
const buildLoginForm = build({
fields: {
username: fake(f => f.internet.userName()),
password: fake(f => f.internet.password()),
},
});
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test(`logging in displays the user's username`, async () => {
render(<Login />);
const { username, password } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
userEvent.type(screen.getByLabelText(/password/i), password);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByText(username)).toBeInTheDocument();
});
test('omitting the password results in an error', async () => {
render(<Login />);
const { username } = buildLoginForm();
userEvent.type(screen.getByLabelText(/username/i), username);
// don't type in the password
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByRole('alert').textContent).toMatchInlineSnapshot(
`"password required"`,
);
});
test('unknown server error displays the error message', async () => {
const testErrorMessage = 'Oh no, something bad happened';
server.use(
rest.post(
'https://auth-provider.example.com/api/login',
async (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: testErrorMessage }));
},
),
);
render(<Login />);
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i));
expect(screen.getByRole('alert')).toHaveTextContent(testErrorMessage);
});
Mocking Browser APIs and Modules
Mocking HTTP requests is one thing, but sometimes you have entire Browser APIs
or modules that you need to mock. Every time you create a fake version of what
your code actually uses, you're "poking a hole in reality" and you lose some
confidence as a result (which is why E2E tests are critical). Remember, we're
doing it and recognizing that we're trading confidence for some practicality or
convenience in our testing. (Read more about this in my blog post:
The Merits of Mocking).
To learn more about what "mocking" even is, take a look at my blog post
But really, what is a JavaScript mock?
An example is when Kent needed to mock the browser window.resizeTo and polyfill window.matchMedia:
This allows to capability of continuing to test in Jest while not running in a browser.
Sometimes, a module is doing something you don't want to actually do in tests.
Jest makes it relatively simple to mock a module:
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// __tests__/some-test.js
import { add, subtract } from '../math';
jest.mock('../math');
// now all the function exports from the "math.js" module are jest mock functions
// so we can call .mockImplementation(...) on them
// and make assertions like .toHaveBeenCalledTimes(...)
Additionally, if you'd like to mock only parts of a module, you can provide
your own "mock module getter" function:
jest.mock('../math', () => {
const actualMath = jest.requireActual('../math');
return {
...actualMath,
subtract: jest.fn(),
};
});
// now the `add` export is the normal function,
// but the `subtract` export is a mock function.
The act function is placed around the resolve (you'll see the error show up in the console) and it happens because the callback is updating the state in a 3rd-party component being used.
We need to ensure all the side-effects are flushed before we continuing with the tests (effects that may be inperceivable to us).
It will now ensure that UI is stable. It is one of the few places where you need to use the act API.
This is an alternative way to solve the problem through mocking.
// mocking Browser APIs and modules
// 💯 mock the module
// http://localhost:3000/location
import * as React from 'react';
import { render, screen, act } from '@testing-library/react';
import { useCurrentPosition } from 'react-use-geolocation';
import Location from '../../examples/location';
jest.mock('react-use-geolocation');
test('displays the users current location', async () => {
const fakePosition = {
coords: {
latitude: 35,
longitude: 139,
},
};
let setReturnValue;
function useMockCurrentPosition() {
const state = React.useState([]);
setReturnValue = state[1];
return state[0];
}
useCurrentPosition.mockImplementation(useMockCurrentPosition);
render(<Location />);
expect(screen.getByLabelText(/loading/i)).toBeInTheDocument();
act(() => {
setReturnValue([fakePosition]);
});
expect(screen.queryByLabelText(/loading/i)).not.toBeInTheDocument();
expect(screen.getByText(/latitude/i)).toHaveTextContent(
`Latitude: ${fakePosition.coords.latitude}`,
);
expect(screen.getByText(/longitude/i)).toHaveTextContent(
`Longitude: ${fakePosition.coords.longitude}`,
);
});
Context and Custom Render Method
How to test components that use context.
From the lesson:
A common question when testing React components is what to do with React
components that use context values. If you take a step back and consider the
guiding testing philosophy of writing tests that resemble the way our software
is used, then you'll know that you want to render your component with the
provider:
The one problem with this is if you want to re-render the <ComponentToTest />
(for example, to give it new props and test how it responds to updated props),
then you have to include the context providers:
This Wrapper could include providers for all your context providers in your
app: Router, Theme, Authentication, etc.
To take it further, you could create your own custom render method that does
this automatically:
import { render as rtlRender } from '@testing-library/react';
// "rtl" is short for "react testing library" not "right-to-left" 😅
function render(ui, options) {
return rtlRender(ui, { wrapper: Wrapper, ...options });
}
// then in your tests, you don't need to worry about context at all:
const { rerender } = render(<ComponentToTest />);
rerender(<ComponentToTest newProp={true} />);
From there, you can put that custom render function in your own module and use
your custom render method instead of the built-in one from React Testing
Library. Learn more about this from the docs:
A basic example of this in practise (not the custom setup):
// testing with context and a custom render method
// http://localhost:3000/easy-button
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../components/theme';
import EasyButton from '../../components/easy-button';
test('renders with the light styles for the light theme', () => {
const Wrapper = ({ children }) => (
<ThemeProvider initialTheme="light">{children}</ThemeProvider>
);
render(<EasyButton>Easy</EasyButton>, { wrapper: Wrapper });
const button = screen.getByRole('button', { name: /easy/i });
expect(button).toHaveStyle(`
background-color: white;
color: black;
`);
});
Render Method
This will be a special render function to encaspsulate the duplication.
// testing with context and a custom render method
// 💯 create a custom render method
// http://localhost:3000/easy-button
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from '../../components/theme';
import EasyButton from '../../components/easy-button';
function renderWithProviders(ui, { theme = 'light', ...options } = {}) {
const Wrapper = ({ children }) => (
<ThemeProvider value={[theme, () => {}]}>{children}</ThemeProvider>
);
return render(ui, { wrapper: Wrapper, ...options });
}
test('renders with the light styles for the light theme', () => {
renderWithProviders(<EasyButton>Easy</EasyButton>);
const button = screen.getByRole('button', { name: /easy/i });
expect(button).toHaveStyle(`
background-color: white;
color: black;
`);
});
test('renders with the dark styles for the dark theme', () => {
renderWithProviders(<EasyButton>Easy</EasyButton>, {
theme: 'dark',
});
const button = screen.getByRole('button', { name: /easy/i });
expect(button).toHaveStyle(`
background-color: black;
color: white;
`);
});
App Test Utils
"Now, we want to swap the @testing-library/react module with our app-test-utils. What I recommend for every application that's using React Testing Library, your test should not import @testing-library/react. > "Instead, you should make your own module that re-exports everything from @testing-library/react and has a render() with providers type of function. We've already got this if we go to our test directory and then test-utils. Then right in here, we are doing something that looks a little familiar."
// testing with context and a custom render method
// 💯 swap @testing-library/react with app test utils
// http://localhost:3000/easy-button
import * as React from 'react';
import { render, screen } from 'test/test-utils';
import EasyButton from '../../components/easy-button';
test('renders with the light styles for the light theme', () => {
render(<EasyButton>Easy</EasyButton>, { theme: 'light' });
const button = screen.getByRole('button', { name: /easy/i });
expect(button).toHaveStyle(`
background-color: white;
color: black;
`);
});
test('renders with the dark styles for the dark theme', () => {
render(<EasyButton>Easy</EasyButton>, { theme: 'dark' });
const button = screen.getByRole('button', { name: /easy/i });
expect(button).toHaveStyle(`
background-color: black;
color: white;
`);
});
You can setup the relative path to be absolute if you setup you jest.config.js file correctly to handle it under moduleDirectories!