An introduction to @testing-library: Part 1

Blog post cover

Testing our apps is a vital part of modern web-development that is often overlooked by new developers. I myself struggled with testing early on. It can be difficult to grasp and there are far fewer free resources available online for testing compared to other topics.

Why do we test our apps?

We want to write tests because they help certify the behavior of our app. You can think of your tests as the documentation for what your code does.

I thought I would introduce you to a set of libraries we can use to help test our apps called Testing Library.

For this introduction you will see me use the React package which is a wrapper on the core library. If you're not familiar with React don't worry. The core principles are the same across the other frameworks/libraries.

On top of the core library there are wrappers that allow us to use this set of testing utilities for multiple different JavaScript frameworks including React, Vue, Svelte and much more.

Content

In part one we will explore 👇

  1. What is Testing Library?
  2. What benefits does Testing Library provide?
  3. What kind of tests can we write with Testing Library?
  4. The flow of testing with Testing Library?
  5. Queries
  6. User Events
  7. Looking ahead to part two!

1.) What is Testing Library?

Testing Library is a collection of utilities that allow us to test our apps in a similar way to how users interact with our site which is a good testing practice. One of the focuses of the library is to provide us with methods of querying for our DOM nodes that is representative of how users would find these nodes on a page.

The description provided by Testing Library on their site is as follows:

The core library, DOM Testing Library, is a light-weight solution for testing web pages by querying and interacting with DOM nodes (whether simulated with JSDOM/Jest or in the browser).

It is not however a testing framework or a test runner. This means we generally use this set of libraries in combination with a testing framework like Jest or Cypress. For the purpose of this introduction I will be running tests using arguably the most popular testing framework, Jest.

2.) What benefits does Testing Library provide?

Testing in a user focused manner gives us the confidence that the tests we write are a true reflection of the user experience.

When we write our tests we want to ensure that we leave out the implementation details of our app. By doing this we ensure our tests are maintainable because any refactoring of our app/components won't cause tests to suddenly fail.

What I mean by this is that we typically want to test the things that our users will interact with and see in our apps. Do you have some state that changes what the user will see on the page? If you do test it.

This article by the creator of Testing Library - Kent C. Dodds explains in detail why we want to avoid testing implementation details - Testing Implementation Details - Kent C. Dodds.

3.) What kind of tests can we write with Testing Library?

The great thing is we can write all kinds of tests using this set of libraries.

  • Unit Tests ✔
  • Integration Tests ✔
  • End-to-end Test ✔

4.) The flow of testing with Testing Library?

Personally I have been using Testing Library in combination with React. The idea is the same across other frameworks/libraries.

The general flow for our tests in React will be something like this 👇.

  • Render our component passing in some mock props if required
  • Query for our nodes in component with maybe some initial assertions.
  • Perform some user action like typing or a click
  • Assert some change that a user would see based on the user input

We can render our React components for testing using the render method which we can import from the main library like this:

import { render } from "@testing-library/react";

and in our test pass in our component to render it:

render(<SomeComponent />);

Next we'll take a look at how we query for elements 😃.

5.) Queries

An important part of Testing Library is being able to query for DOM nodes in a user focused manner. We do this using methods called Queries.

Queries allow us to find elements that might exist on the page. Knowing the correct query to use for a given situation is an important part of using the library. We need to query for elements so that we can perform some assertions or user events on them.

Then general syntax for querying with Testing Library is as follows 👇.

screen.getByRole("button");

screen is an object that has all the available queries bound to the document.body. We can import it from the main library of whatever context we are using (in this case React) like this 👇.

import { screen } from "@testing-library/react;

The query we used in this case is called getByRole which queries for a single node that has the role of button. Let's take a look at the different query variations we have available.

Query Variations

Queries allow us to find DOM nodes. We can query for single nodes or multiple nodes and queries can be put into three different categories.

  • getBy...

This query returns a single matching node or an error for no matching nodes. This is usually the go to variation when we are looking for a single node that we expect to be in the document.

  • queryBy...

This query returns a single matching node or null for no matching nodes. This variation is usually preferred when we want to assert that the node is not present in the document.

  • findBy...

This query returns a promise that resolves when the element is found. It will reject if no node is found before the 1000ms default timeout. We use this query variation when we expect to have to wait some time before our result is present to the user (e.g some asynchronous request).

These queries also have AllBy... variations that allow us to query for multiple DOM nodes returned as an arrays (e.g getAllByRole). Often our components will have multiple elements of the same role for example and we can group them all using this query.

It is also common to store the results of our queries into variables so that we can reference them in multiple places without having to re-perform the query like this 👇.

const button = screen.getByRole("button");

What can we query for?

Deciding how to query for an element is an important part of using this library. We can find elements in several different ways such as finding text in the document, element roles and label text. Despite this some query methods are preferred to others.

This is because we want to ensure that our tests are a good representation of how our users interact with the app. Certain queries are more accessible than others for example users that visit your site using assistive technology such as screen readers.

If we query an element by its role instead of its text content we can be sure that our elements can be found accessibly as our impaired users may find them.

Let's take a look at what we can query for. For the following queries I will stick to getBy but we can also use any of the other variants.

  • getByRole()

👆 This is usually the preferred method of query because we can query for roles accessibly by the name that screen readers will read out. There is a lot you can get with this query which I was initially unaware of but it should be the first choice.

You can find a list of ARIA roles here - MDN Web Docs - Using ARIA: Roles, states, and properties

  • getByText()

👆 Used to query for non interactive elements that have some text content like a <div>.

  • getByLabelText()

👆 This query will get the <input> element associated with the <label> text that we pass to it. It is usually the preferred method of querying our inputs.

  • getByPlaceholderText()

👆 Used to query for an element that has some placeholder text such as in an <input>. It is recommended to use getByLabelText over this for querying inputs.

  • getByDisplayValue()

👆 This will return the element that has a matching value. Can be used to find an <input>, <textarea> or <select> element.

  • getByAltText()

👆 Used to find the element that has a matching alt text to the value we pass it.

  • getByTitle()

👆 Query an element that has a matching title attribute value to the value we pass it.

  • getByTestId()

👆 We can pass a data attribute in the form data-testid="something" to an element and then query for it using getByTestId.

This query is generally not recommend because it is not friendly to accessibility and involves polluting our markup with attributes not relevant to the users.

Because using the right queries is important Testing Library provides us with a function that provides suggestions for which queries to use. We can import the configure() function from our primary library like this 👇.

import { configure } from "@testing-library/react";

and inside our tests we can call it and pass in the throwSuggestions option like this 👇.

configure({ throwSuggestions: true });

This will provide you with potentially better query options when you run your tests and can be helpful early on.

Many of these queries are also able to take optional second parameters for example 👇

screen.getByText("hello world", { exact: false });

meaning we don't have to match the string "hello world" exactly.

Or this 👇

screen.getByRole("button", { name: "reset" });

where we narrow down our button search to an element that also has the name "reset".

There is a lot we can do with our queries and it would beneficial to you to have a play around with trying out different queries. We will start implementing some of these queries in some tests in part two!

You can check out these query methods in more detail here - Testing Library - About Queries.

6.) User Events

Now that know how to query for elements let's see how can simulate some user actions.

Testing Library provides a companion library called user-event that allows us to perform these user-actions available though @testing-library/user-event. Using this library we can perform actions such as user click events, typing, tabbing, hovering and much more. Check out the Testing Library docs here for the full list with explanations - Testing Library - user-event.

First we import the userEvent object as a default export like this 👇.

import userEvent from "@testing-library/user-event";

Then we have a bunch of methods available on this object that allow us to simulate user events like this 👇.

userEvent.click(screen.getByRole("button"));

where we specify the event and the element we wish to execute the event on in the case of the click event.

Let's have a quick look at how queries and events are connected inside an example test file for a React component. The logic of the component or tests isn't important at this stage and we won't be making any assertions just yet.

queries and user events examples

Here we setup our test with our describe and test blocks which are part of the Jest testing framework. First we import our screen wrapper which allows us to access our queries. Next we import the render method which just allows us to render our react component to the DOM.

Then we render our component and perform our queries for elements we would expect in our component. We can store the result of the query inside constants which is good to do if we plan on referencing them often. Finally we execute some example user events on our elements.

The next step would be to start making some assertions which you will be familiar with if you've used a testing framework like Jest. Testing Library builds on these assertions which we will see in part 2.

7.) Looking ahead to part two!

Now we now why we need to test and how Testing Library can help us, the next step is to write some tests. Finally 😂.

But unfortunately not today otherwise it will end up being far too long.

If you're new to Testing Library I suggest playing around with rendering some component and practicing with the different queries available. We will see this in action in part 2 along with making assertions, dealing with asynchronous events and a look at different queries in action.

Thanks for reading! Feel free to say hello @Kieran6dev.

See you in Part 2 👋.