Skip to main content

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.

tip

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:

  1. Form text box exists
  2. 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:

MethodExplanation
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

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:

  1. The LinkedIn social link has been rendered
  2. 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:

MethodExplanation
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:

  1. The image is rendered when all require attributes are provided. (alt, src, title)
  2. The image 'src' attribute matches the imageUrl provided
  3. The image 'alt' attribute matches the altText provided.
  4. 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:

MethodExplanation
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:

  1. No violations found for each Schwab supported language
  2. 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:

MethodExplanation
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:

  1. A component can receive a callback function as a prop
  2. That callback is triggered when appropriate
  3. 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:

MethodExplanation
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:

  1. A button component can receive a callback function as a prop
  2. That callback is triggered when the user clicks the button
  3. 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:

MethodExplanation
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:

  1. Component renders different content based on prop values
  2. 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:

MethodExplanation
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:

  1. Elements can be queried by role, text, label, etc.
  2. 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:

MethodExplanation
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:

  1. A user can interact with the multi select input
  2. 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:

MethodExplanation
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:

  1. A user can interact with the multi select input
  2. 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:

MethodExplanation
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:

  1. A user can interact with the multi select input
  2. 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:

MethodExplanation
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:

  1. A user can interact with the multi select input
  2. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. User use a keyboard to enter text into a textbox field
  2. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. A textbox field is disabled
  2. User is unable to focus on the disabled textbox
  3. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. The textbox field will receive values entered
  2. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. 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:

MethodExplanation
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:

  1. One 'h1' heading has been rendered
  2. The persons name should be present, unaltered, and appear once
  3. 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:

MethodExplanation
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:

  1. The link can be hovered
  2. 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:

MethodExplanation
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:

  1. The link can be un-hovered
  2. 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:

MethodExplanation
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:

  1. User can traverse inputs by clicking tab
  2. 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:

MethodExplanation
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.

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:

  1. Assert that passing in JsonApi data returns an object with the correct json schema
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/

/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/

/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})

Test Breakdown:

MethodExplanation
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:

  1. 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.
  2. We highjack all next router calls to any fake urls generated randomly by test factories.
  3. 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?
  4. We remove the chance for confusion or complexity that may arise from next attempting to do things that require an external server.
  5. 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:

  1. That that the next router's 'push' method is called when a user clicks on our components CTA button.
  2. 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:

MethodExplanation
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:

  1. That passing "ABE Lincoln" as an argument, returns "abeLincoln"
test('...', async () => {
/*-------------------------------------------------------------------
| Arrange
-------------------------------------------------------------------*/

/*-------------------------------------------------------------------
| Act
-------------------------------------------------------------------*/

/*-------------------------------------------------------------------
| Assert
-------------------------------------------------------------------*/
})

Test Breakdown:

MethodExplanation
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:

  1. 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:

MethodExplanation
expect().toBe()Assert the given value matches the expected value