Next.js Unit Testing Tutorials
Component testing is a crucial stage in the development of any enterprise level React/Next.js application. As the literal building blocks of our Schwab React/Next web UI, it is imperative to prove that our components are functional and accessible, on all supported devices, in every supported language, at all times. We do this by writing holistic unit tests that not only confirm that the above statement is true, but also serve as CI/CD gatekeepers blocking any code commit that causes a tests to fail.
If you're a developer who is just learning to unit test, we know first hand that it can feel extremely daunting. We were once in your shoes friend, don't worry, we got you! We've broken component testing down into bite size segments to help you better understand what to test and how. We suggest compartmentalizing components testing this way, it should assist you in determining what tests are required for your component.
Once you get the hang of things, you'll find that testing can actually be fun and rewarding! It is a proven fact that by writing tests, good developers quickly become an amazing developers overnight. This transition happens naturally as you start writing code that can be easily tested & avoiding gotchas that you discovered only when running your code through hundreds of language, browser and test data variations. Trust us, you'll see.
We have also developed a library of demo tests that will cover the commonly required tests for components. We strongly encourage developers to copy these demos tests and modify them to for their needs VS starting from scratch. If you have not walked through our "What should i test?" exercise, we recommend doing this as well to help you establish a list of what should be tested.
Just need to copy some test code? Visit the Jest Playground repo.
Attributes
Aria Attribute Testing
In this example, keeping non-visual experiences in mind, we are going to test that a form textbox has an accessible error message.
Visit the Jest playground accessibility repo for more examples of aria tests.
Test asserts that:
- Form text box exists
- Form text box has an accessible error message
import { expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
test("Has accessible error message ", async (): Promise<void> => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(
<form>
<input aria-label="Has Error" aria-invalid="true" aria-errormessage="error-message"/>
<div id="error-message" role="alert">This field is invalid</div>
</form>
);
const field = screen.getByRole("textbox");
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(field).toHaveAccessibleErrorMessage();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates an element by role |
| expect().toHaveAccessibleErrorMessage()) | Assert that an element has the expected accessible error message |
Link Attribute Testing
Test that a link has specific attributes.
We can now rest assured that social links are conditionally rendered. Awesome!
However, we have not proven that these links have the expected attributes.
Let's do this now by confirming that social links are generated with the correct href attribute.
Test asserts that:
- The LinkedIn social link has been rendered
- The rendered LinkedIn link has the correct 'href' attribute
test(`LinkedIn link attribute integrity`, () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const props = new Factory().makeProps();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<StoryPerson {...props} />);
const socialLink = screen.getByRole('link', { name: 'Follow on linkedin' });
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(socialLink).toBeInTheDocument();
expect(socialLink).toHaveAttribute('no-icon', 'true');
expect(socialLink).toHaveAttribute('target', '_blank');
expect(socialLink).toHaveAttribute('href', `https://www.linkedIn.com/in/${props.linkedin}`);
expect(socialLink).toHaveClass('person-icon linkedin-icon');
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the <h1> heading element |
| expect().toHaveAttribute() | Asserts the element has a specific attribute optionally passing the expected value |
| expect().toContain() | Assert the name exists in the <h1> tag |
Image Attribute Testing
Test that an image has specific attributes.
Because this StoryPerson component accepts image attributes as well, we should test that the image is being rendered as expected.
Test asserts that:
- The image is rendered when all require attributes are provided. (alt, src, title)
- The image 'src' attribute matches the imageUrl provided
- The image 'alt' attribute matches the altText provided.
- The image 'title' attribute matches the jobTitle provided
test(`Image attribute allocation`, async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const props = new Factory().makeProps();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<StoryPerson {...props} />);
const image = screen.getByRole('img');
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(image).toBeInTheDocument();
expect(image).toHaveAttribute('src', props.imageUrl);
expect(image).toHaveAttribute('alt', props.altText);
expect(image).toHaveAttribute('title', props.jobTitle);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the image element |
| expect().toHaveAttribute() | Asserts the element has a specific attribute optionally passing in an expected value |
Axe Scans
The Accessibility Rules
The axe-core package (via axe-jest) has different types of rules, for WCAG 2.0, 2.1, 2.2 on level A, AA and AAA as well as a number of best practices that help to identify common accessibility practices like ensuring every page has an h1 heading, and to help us avoid "gotchas" in ARIA like where an ARIA attribute we have used will get ignored. The complete list of rules, grouped WCAG level and best practice, can found in doc/rule-descriptions.md.
With axe-core, we can find on average 57% of WCAG issues automatically. Additionally, axe-core will return elements as "incomplete" where axe-core could not be certain, and manual review is needed. To catch bugs earlier in the development cycle, we recommend leveraging the axe-linter vscode extension as well.
Accessibility Violation Testing
In this example, keeping non-visual experiences in mind, we are going to leverage jests axe-core package to test for accessibility violations.
Utilize JsxTestTools to quickly scan and assert that your component does not render with accessibility violations.
Test asserts that:
- No violations found for each Schwab supported language
- No violations found for each Schwab supported device breakpoint
import MyComponentPropsFactory from '@schwab/schema/factories/ui/MyComponentPropsFactory';
import { JsxTestTools } from '@schwab/test/ui/tools/JsxTestTools';
import MyComponent from '#components/MyComponent';
new JsxTestTools().runAllTests(MyComponent, new MyComponentPropsFactory());
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| axe(container) | Locates the <h1> heading element |
| expect().toHaveNoViolations()) | Asserts the name prop appears only once |
Callbacks
When unit testing, we are testing that callback logic is handled appropriately. We will utilize callback mocks to prove that, when invoked, our callback executes as expected.
Most of the time we'll be testing that a callback has been triggered when the appropriate user action has taken place. Those tests are in the User Interaction section. However, when testing callbacks fired by non-user-interaction lifecycle events, such as UseEffect and UseMount, we need to approach callback testing differently.
Test asserts that:
- A component can receive a callback function as a prop
- That callback is triggered when appropriate
- That callback is triggered with the correct arguments
import { expect, test } from "@jest/globals";
import { render } from "@testing-library/react";
test(`Lifecycle callback method invoked with correct arguments`, () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const mockCallback = jest.fn();
const expectedCallbackParameters = {
id: 123,
name: "Chad Testington"
};
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(
<MyComponent
onLoad={mockCallback}
loadData={expectedCallbackParameters}
/>
);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(expectedCallbackParameters);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| jest.fn() | Creates a mock callback function |
| render() | Renders the component with callback prop |
| expect().toHaveBeenCalledTimes() | Asserts the callback was invoked the expected number of times |
| expect().toHaveBeenCalledWith() | Asserts the callback received the correct arguments |
Click
Test that a button click event is handled correctly.
Test asserts that:
- A button component can receive a callback function as a prop
- That callback is triggered when the user clicks the button
- That callback is triggered with correct arguments
import { expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test(`Button click callback triggered`, async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const mockCallback = jest.fn();
const expectedArgument = "button-value";
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<button onClick={() => mockCallback(expectedArgument)}>Click Me</button>);
const button = screen.getByRole("button");
await userEvent.click(button);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(expectedArgument);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| jest.fn() | Creates a mock callback function |
| render() | Renders the component |
| screen.getByRole() | Locates button element |
| userEvent.click() | Simulates user click |
| expect().toHaveBeenCalledTimes() | Asserts callback invocation count |
| expect().toHaveBeenCalledWith() | Asserts callback arguments |
Conditionally Rendered
Test that a component renders conditionally based on props or state.
Test asserts that:
- Component renders different content based on prop values
- Elements appear or disappear based on conditions
import { expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
test(`Conditional rendering based on prop`, () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const { rerender } = render(<MyComponent showContent={false} />);
/*-------------------------------------------------------------------
| Assert - Initially hidden
-------------------------------------------------------------------*/
expect(screen.queryByText("Content")).not.toBeInTheDocument();
/*-------------------------------------------------------------------
| Act - Re-render with different prop
-------------------------------------------------------------------*/
rerender(<MyComponent showContent={true} />);
/*-------------------------------------------------------------------
| Assert - Now visible
-------------------------------------------------------------------*/
expect(screen.getByText("Content")).toBeInTheDocument();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component, returns helper methods |
| rerender() | Re-renders the component with new props |
| screen.queryByText() | Queries for text, returns null if not found |
| screen.getByText() | Gets element by text, throws if not found |
| expect().not.toBeInTheDocument() | Asserts element is not in DOM |
DOM Queries
Query Methods Overview
Different query methods for different scenarios:
- getBy: Throws error if element not found (use for elements that should exist)
- queryBy: Returns null if element not found (use for asserting non-existence)
- findBy: Returns promise, waits for element (use for async operations)
Test asserts that:
- Elements can be queried by role, text, label, etc.
- Correct query methods are used for different scenarios
import { expect, test } from "@jest/globals";
import { render, screen, waitFor } from "@testing-library/react";
test(`DOM query methods`, async () => {
/*-------------------------------------------------------------------
| Arrange & Act
-------------------------------------------------------------------*/
render(
<div>
<button>Click Me</button>
<input aria-label="Username" />
</div>
);
/*-------------------------------------------------------------------
| Assert - Synchronous queries
-------------------------------------------------------------------*/
expect(screen.getByRole("button")).toBeInTheDocument();
expect(screen.getByText("Click Me")).toBeInTheDocument();
expect(screen.getByLabelText("Username")).toBeInTheDocument();
expect(screen.queryByText("Not Here")).not.toBeInTheDocument();
/*-------------------------------------------------------------------
| Assert - Asynchronous query
-------------------------------------------------------------------*/
const asyncElement = await screen.findByRole("button");
expect(asyncElement).toBeInTheDocument();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| screen.getByRole() | Query by ARIA role |
| screen.getByText() | Query by text content |
| screen.getByLabelText() | Query by label |
| screen.queryByText() | Query that returns null if not found |
| screen.findByRole() | Async query that waits for element |
Fetch
A fetch methods interactions with external APIs or any other dependencies should be mocked as far as unit testing is concerned. Remember, if we testing an external source, this would be considered an integration test.
When testing a fetch method, we are testing things like the call to, the response from, and any throwable exceptions of the fetch method itself. i.g. Test that an exception is thrown when incorrect arguments are passed into the method.
Therefore, there should not be any assertions made on the actual expected response from an external source. We instead mock the external source, then make the assertions required to prove that a method is functioning as expected in isolation.
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})
Form Multi Select
Select One Option
Test that a user can select a single option from the multi select input.
Test asserts that:
- A user can interact with the multi select input
- A user can select a single option from a multi select input
test("User can select one option", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<MultiSelect />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Blue"]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.queryAllByRole("option", { selected: true })).toHaveLength(1);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.queryAllByRole() | Queries all elements matching a specific role |
| expect().toHaveLength() | Asserts an array contains a specific number of items |
Deselect A Single Option
Test that a user can deselect a single selected option.
Test asserts that:
- A user can interact with the multi select input
- A user can deselect a single option
test("User can select one option", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<MultiSelect />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Red", "Blue"]);
await userEvent.deselectOptions(screen.getByRole("listbox"), ["Blue"]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.queryAllByRole("option", { selected: true })).toHaveLength(1);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| userEvent.deselectOptions() | Deselects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.queryAllByRole() | Queries all elements matching a specific role |
| expect().toHaveLength() | Asserts an array contains a specific number of items |
Select Multiple Options
Test that a user can select multiple options from the multi select input.
Test asserts that:
- A user can interact with the multi select input
- A user can select a multiple options from a multi select input
test("User can select multiple options", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<MultiSelect />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Red", "Blue"]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.queryAllByRole("option", { selected: true })).toHaveLength(2);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.queryAllByRole() | Queries all elements matching a specific role |
| expect().toHaveLength() | Asserts an array contains a specific number of items |
Deselecting Multiple Options
Test that a user can deselect multiple selected options.
Test asserts that:
- A user can interact with the multi select input
- A user can deselect multiple selected options
test("User can deselect multiple options", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<MultiSelect />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Red", "Blue"]);
expect(screen.queryAllByRole("option", { selected: true })).toHaveLength(2);
await userEvent.deselectOptions(screen.getByRole("listbox"), ["Red", "Blue"]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.queryAllByRole("option", { selected: true })).toHaveLength(0);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| userEvent.deselectOptions() | Deselects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.queryAllByRole() | Queries all elements matching a specific role |
| expect().toHaveLength() | Asserts an array contains a specific number of items |
Form Select
Make a selection
Test that a user can select an option.
Test asserts that:
- A user can select an option
test("User can select an option", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Select />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Green"]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.getAllByRole('option', { selected: true })).toHaveLength(1)
expect(screen.queryByRole("option", { selected: true, name: "Green" })).not.toBeNull()
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.getAllByRole() | Locates all elements with a specific role |
| expect().toBeNull() | Assert the given value is null |
Deselect a selection
Test that a user can deselect an option.
Test asserts that:
- User can un-select an option
test("User can deselect an option", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Select />);
await userEvent.selectOptions(screen.getByRole("listbox"), ["Blue"]);
expect(screen.queryAllByRole("option", { name: "Blue", selected: true })).toHaveLength(1);
await userEvent.selectOptions(screen.getByRole("listbox"), [""]);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.queryAllByRole("option", { name: "Blue", selected: true })).toHaveLength(0);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| userEvent.selectOptions() | Selects one or more options |
| screen.getByRole() | Locates the element via role |
| screen.getAllByRole() | Locates all elements with a specific role |
| screen.queryAllByRole() | Queries all elements matching a specific role |
| expect().toHaveLength() | Asserts an array contains a specific number of items |
| expect().toBeNull() | Assert the given value is null |
Form Textbox
Form Input
Test that a user is able to leverage their keyboard to type text into a field.
If your component allows a user to enter values by typing on their keyboard, we should write a test that renders the component, enters values as a user utilizing their keyboard, and then assert that the expected result has taken place.
Test asserts that:
- User use a keyboard to enter text into a textbox field
- The field stores the value as it was entered and is not truncated
import { faker } from "@faker-js/faker/locale/en";
import { describe, expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import Input from "../../../../src/components/forms/textbox/input";
describe("User typed input tests", () => {
test("User can enter text into input field", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const userName = faker.person.firstName();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Input />);
const input = screen.getByRole("textbox") as HTMLInputElement;
await userEvent.type(input, userName);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(input.value).toBe(userName);
});
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| userEvent.type() | Types values into an input |
| expect().toBe() | Assert that a value matches the expected value |
Textbox Required
Test that a textbox is required.
Test asserts that:
- A textbox field is required
import { expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
test("Test input is required", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<input title="test" aria-required="true" />);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.getByRole("textbox")).toBeRequired();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| expect().toBeRequired() | Assert the given value matches the expected value |
Textbox Disabled
Test that a textbox is disabled and a user is not able to type values into the textbox.
Test asserts that:
- A textbox field is disabled
- User is unable to focus on the disabled textbox
- User cannot enter values into the textbox
test("Test input is disabled", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<input title="test" disabled="true" />);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.getByRole("textbox")).toBeDisabled();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| expect().toBeDisabled() | Assert the given value matches the expected value |
Textbox Paste
Test that a user can paste values into a textbox from their clipboard.
Test asserts that:
- A textbox field will receive values pasted in from a users clipboard
test("User can paste text into an input field", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const user = userEvent.setup({ writeToClipboard: true });
const userName = faker.person.firstName();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Input />);
const input = screen.getByRole("textbox") as HTMLInputElement;
await user.type(input, userName);
await user.dblClick(input);
const copied = await user.copy();
await user.clear(input);
await user.paste(copied);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(input.value).toBe(userName);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| userEvent.setup() | Allows us write multiple consecutive interactions that behave just like the described interactions by a real user |
| render() | Renders the component passing in props via spread operator. |
| user.type() | Types values into an input |
| user.dblClick() | Double clicks an element |
| user.copy() | Locates the element via role |
| user.clear() | Locates the element via role |
| user.paste() | Locates the element via role |
| expect().toBe() | Assert the given value matches the expected value |
Textbox Clear
Test that a user can clear textbox values.
Test asserts that:
- The textbox field will receive values entered
- The textbox will allow values entered to be cleared
test("User can clear entered text from input field", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const userName = faker.person.firstName();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Input />);
const input = screen.getByRole("textbox") as HTMLInputElement;
await userEvent.type(input, userName);
await userEvent.clear(input);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(input.value).toBe("");
});
Test Breakdown:
| Method | Explanation |
|---|---|
| userEvent.setup() | Allows us write multiple consecutive interactions that behave just like the described interactions by a real user |
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| userEvent.type() | Types values into an input |
| user.clear() | Locates the element via role |
| expect().toBe() | Assert the given value matches the expected value |
Textbox Copy
Test that a user can copy textbox values.
Test asserts that:
- The textbox field will allow its values to be copied
test("User can copy entered text from input field", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const user = userEvent.setup({ writeToClipboard: true });
const userName = faker.person.firstName();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Input />);
const input = screen.getByRole("textbox") as HTMLInputElement;
await user.type(input, userName);
await user.dblClick(input);
const copied = await user.copy();
await user.type(input, "something else");
await user.clear(input);
await user.paste(copied);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(input.value).toBe(userName);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| userEvent.setup() | Allows us write multiple consecutive interactions that behave just like the described interactions by a real user |
| render() | Renders the component passing in props via spread operator. |
| user.type() | Types values into an input |
| user.dblClick() | Double clicks an element |
| user.copy() | Locates the element via role |
| user.clear() | Locates the element via role |
| user.paste() | Locates the element via role |
| expect().toBe() | Assert the given value matches the expected value |
Textbox Numeric
Test that a user can only enter numeric values into a textbox.
Test asserts that:
- The textbox field will allow numeric values only
test('user can only enter numeric values into a textbox', () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const props = new Factory().mock();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<NumericInput />);
const input = screen.getByRole("textbox");
await user.type(input, '1a2b3c');
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(input.value).toBe(123);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| userEvent.type() | As a real user, types values into an input |
| expect().toBe() | Assert the given value matches the expected value |
Headings
Test that headings are rendered with correct hierarchy and content.
In this example we are going to test that a persons name appears within a H1 tag.
As the developer of this component, we need to test how the persons "name" prop is being handled.
Keeping SEO and non-visual experiences in mind, we have a few assertions that we need to make.
Test asserts that:
- One 'h1' heading has been rendered
- The persons name should be present, unaltered, and appear once
- The persons name should live within the H1 tag.
test('Persons name lives within H1 tag and only appears once', () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const props = new Factory().makeProps();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Component {...props} />);
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.getAllByText(props.name)).toHaveLength(1);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(props.name);
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getAllByRole() | Locates all h1 heading elements |
| expect().toHaveLength(1) | Asserts only 1 h1 tag exists containing the name prop value |
Hover
Hover
Test that a links class name changes on hover.
Test asserts that:
- The link can be hovered
- The link class changes to the expected class on hover
import { describe, expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import Link from "../../../../src/components/ctas/link";
describe("User mouse hover tests", () => {
test("Class changes when user hovers over link.", async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Link> Charles Schwab </Link>);
await userEvent.hover(screen.getByRole('link'));
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(screen.getByRole('link').className).toBe("hovered");
});
});
Test Breakdown:
| Method | Explanation |
|---|---|
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole('link') | Locates the link within the DOM |
| userEvent.hover() | Hovers the link |
| expect().toBe() | Asserts the className value equals the expected class of "hovered" |
Un-hover
Test that a links class name changes when no longer hovered.
Test asserts that:
- The link can be un-hovered
- The link class changes to the expected class on un-hover
import { describe, expect, test } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
describe("User mouse unhover tests", () => {
test('User unhover event', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const user = userEvent.setup();
let isHovered = false;
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<div>Hover</div>);
const component = screen.getByText('Hover');
component.addEventListener('mouseover', () => {
isHovered = true
});
component.addEventListener('mouseout', () => {
isHovered = false
});
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(isHovered).toBe(false);
await user.hover(component);
expect(isHovered).toBe(true);
await user.unhover(component);
expect(isHovered).toBe(false);
});
});
Test Breakdown:
| Method | Explanation |
|---|---|
| userEvent.setup() | Allows us write multiple consecutive interactions that behave just like the described interactions by a real user |
| render() | Renders the component passing in props via spread operator. |
| screen.getByText() | Locates the element by its text contents |
| user.hover() | Hovers the link |
| user.unhover() | Un-hovers the link |
| expect().toBe() | Asserts an "isHovered" boolean state |
Keyboard Testing
Tab Sequence
Test that a user can traverse inputs by clicking the tab key.
If your component reacts when a user clicks the tab key, we should write a test that renders the component, clicks the tab key, and then assert that the expected result has taken place.
Test asserts that:
- User can traverse inputs by clicking tab
- User can focus on each individual input by tabbing
test('user can tab through multiple inputs', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const user = userEvent.setup();
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<div>
<input type="checkbox" />
<input type="radio" />
<input type="number" />
</div>);
const checkbox = screen.getByRole('checkbox');
const radio = screen.getByRole('radio');
const number = screen.getByRole('spinbutton');
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(document.body).toHaveFocus();
await user.tab();
expect(checkbox).toHaveFocus();
await user.tab();
expect(radio).toHaveFocus();
await user.tab();
expect(number).toHaveFocus();
await user.tab();
// cycle goes back to the body element
expect(document.body).toHaveFocus();
await user.tab();
expect(checkbox).toHaveFocus();
});
Test Breakdown:
| Method | Explanation |
|---|---|
| userEvent.setup() | Allows us write multiple consecutive interactions that behave just like the described interactions by a real user |
| render() | Renders the component passing in props via spread operator. |
| screen.getByRole() | Locates the element via role |
| user.tab() | Executes a real user tab button keydown interaction |
| expect().toHaveFocus() | Assert whether the element has focus or not |
Mocking
Looking for mocking support?
Awesome! & We are eager to help.
Below you'll find references that have helped your peers in the past. We suggest taking a look around.
If these resources aren't helpful for your use case, please ping on Teams and we will schedule a training call. Happy testing!
Visit the Jest Playground: The Jest Playground repo hosts a decent collection of examples tests and as well as step by step tutorials that will highlight an array of ways to mock and/or spy on methods.
- Mocked callbacks
- Spy on a class method
- Mocked callback response testing
- Mocked method has been called x number of time
Copy Some Code: You can try borrowing the logic from our next/navigation mocks here: nextjs-web/packages/test/mocks/next/navigation.js
Pointer
There are two types of actions: press and move.
This enables us to test when a user presses a button or how a user touches the screen.
Important: When testing, the framework will not try to determine if the pointer action you describe is possible at that position in your layout.
Example tests are currently being created, please refer to the React Testing Library documentation for examples.
Transform
In this example we are going to test the utility method returns the expected response attributes.
Let's say we are testing a function that transforms a Drupal JsonApi responses into normalized json schema usable by Next components.
Test asserts that:
- Assert that passing in JsonApi data returns an object with the correct json schema
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})
Test Breakdown:
| Method | Explanation |
|---|---|
| expect().toHaveProperty() | Assert a property at provided reference keyPath exists for an object |
useRouter
Overview
The next/navigation useRouter hook allows us to programmatically change routes inside client components.
NextJs application has been configured to mock all next/navigation package methods automatically while testing.
By mocking all next/navigation methods automatically, we accomplish a few important tasks:
- We intercept any next router attempts to call or redirect us to external link. For example, a production API endpoint that was accidentally added to a test.
- We highjack all next router calls to any fake urls generated randomly by test factories.
- We can spy on the mocked methods and make assertions against them within our test. i.g. I clicked a link in meganav, did next router received the correct url as a result?
- We remove the chance for confusion or complexity that may arise from next attempting to do things that require an external server.
- We can prove that we are not only testing a single unit of code, but also we can prove that we are still functional if a dependency is deprecated or a vendor change is required.
Note: If your component or utility method leverages multiple external dependencies, you'll want to mock those dependencies as well.
Next useRouter is a Mock!
All next/navigation useRouter() hook methods are mocked automatically while testing.
By mocking next/navigation useRouter hook methods we gain the ability to collect usage information to be leveraged within our tests.
This is powerful as we can see how many times a method was called within a single test. We can also ensure that the arguments received match our expectations.
Mocked methods are present and callable. However, we do not mock their actual implementation.
This means they are non functional and will do nothing when called.
This is by design as we only need to test our 'unit' of code and do not need to test the "integration" of next useRouter hook.
As always, we can spy on all hook methods and confirm the expected interactions are taking place within our test.
Examples:
expect(mockedRouter).toHaveBeenCalledWith('https://schwab.com/ira');expect(mockedRouter).toHaveBeenCalledTimes(3);
router.push()
In this example we are going to test that our mocked routers 'push' method is called as expected when a user clicks our cta button. We will also verify that the expected redirect url is being passed into the routers push method as an argument.
As mentioned above, the all router methods are automatically mocked by design so calling the 'push()' method doesn't actually navigate to the provided destination.
However, once we mock a method with Jest, Jest will track all calls and responses to and from the method allowing us to assert the amount of calls it will receive, the arguments passed when called, and that we are receiving the expected response in return.
Test asserts that:
- That that the next router's 'push' method is called when a user clicks on our components CTA button.
- Assert that the 'push' method received the expected redirect url.
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
const router = useRouter();
const ctaUrl = 'https://schwab.com/register';
const mockedRouter = jest.mocked(router.push);
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
render(<Component url="ctaUrl" />
await userEvent.click(screen.getByRole('button'));
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
expect(mockedRouter).toHaveBeenCalledWith(ctaUrl);
})
Test Breakdown:
| Method | Explanation |
|---|---|
| expect().toHaveBeenCalledWith() | Assert that a mock function was called with specific arguments. |
Utilities
Example 1: "camelize"
In this example we are going to test a "camelize" method returns the expected response.
Let's say we are testing a "camelize" function that transforms a string in to camel cased string.
Test asserts that:
- That passing "ABE Lincoln" as an argument, returns "abeLincoln"
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})
Test Breakdown:
| Method | Explanation |
|---|---|
| expect().toBe() | Assert the given value matches the expected value |
Example 2: convertToPascal
In this example we are going to test the convertToPascal method returns the expected response.
Test asserts that:
- That passing "This text may be convertible, but it is not a car!" as an argument, returns our value in pascal case.
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/
/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})
Test Breakdown:
| Method | Explanation |
|---|---|
| expect().toBe() | Assert the given value matches the expected value |