Testing Lists Items With React Testing Library

2 mins |

November 22, 2020

Most of the applications usually have some kind of lists. They are a bit different to test as they contain dynamic values. Let’s see how we can test them using React Testing Library.

We can explore that by an example by a list of Fruits. Here we lists 5 static fruits, but this could remain almost same even if we have dynamically updated lists.

1const fruits = ["Bananas", "Apples", "Strawberries", "Grapes", "Oranges"]
2
3function FruitList() {
4 return (
5 <section>
6 <h1 id="fruits-heading">Fruits</h1>
7 <ul aria-labelledby="fruits-heading">
8 {fruits.map(fruit => (
9 <li key={fruit}>{fruit}</li>
10 ))}
11 </ul>
12 </section>
13 )
14}

Key things to remember📝:

  • use <ul>, <ol> and <li> to semantically list items. If you can’t use them, at least add appropriate aria roles.
  • add aria label (aria-label and aria-labelledby) to <ul> or <ol> to semantically label what the list is about.

Let us see how we can test this list of fruits. Most often we don’t want to test exact order in which the elements were rendered or sometimes what exactly the items were. In these cases all we need is to see how many elements were rendered.

If you want to just test whether X items are rendered

1import React from "react"
2import { render, screen, within } from "@testing-library/react"
3
4it("should render list of 5 fruits", () => {
5 render(<FruitList />)
6 const list = screen.getByRole("list", {
7 name: /fruits/i,
8 })
9 const { getAllByRole } = within(list)
10 const items = getAllByRole("listitem")
11 expect(items.length).toBe(5)
12})

We are able to query the the list by screen.getByRole("list", { name: /fruits/i }) because we have labelled the <ul> with aria-label="fruits".

Once we get the list (<ul>), we need to get all the list items rendered under the list. We can achieve that by using within (RTL provides this method which can be used to query within an element). Here we use it to query all the listitem under the <ul>. If we are unable to use <li> or add listitem role to the list, you can add data-testid. I would strongly discourage 👎 as it might not be accessible.

This might not be enough in some cases. We might have lists where you are listing the items in chronological order or we have a feature where a list is sorted on some conditions. In these cases, we might want to test the order in which the elements were rendered. We can achieve that by taking a snapshot of the list.

If you want to test the list as a snapshot

1import React from "react"
2import { render, screen } from "@testing-library/react"
3
4it("should render list of fruits in a specific order by dom snapshot", () => {
5 render(<FruitList />)
6 const list = screen.getByRole("list", {
7 name: /fruits/i,
8 })
9 expect(list).toMatchInlineSnapshot(`
10 <ul
11 aria-label="Fruits"
12 >
13 <li>
14 Bananas
15 </li>
16 <li>
17 Apples
18 </li>
19 <li>
20 Strawberries
21 </li>
22 <li>
23 Grapes
24 </li>
25 <li>
26 Oranges
27 </li>
28 </ul>
29 `)
30})

Here we are testing the snapshot of the DOM, which might not be easy to read when the list item is huge. It might have a lot more elements than what we have right now. In these cases, we would be better off with testing the name or label of each element instead of taking the snapshot of the entire list.

Here we can just pick the textContent from the DOM element. This should be fine in most of the cases.

If you want to test important bits of the list

1import React from "react"
2import { render, screen, within } from "@testing-library/react"
3
4it("should render list of fruits in a specific order", async () => {
5 render(<FruitList />)
6 const list = screen.getByRole("list", {
7 name: /fruits/i,
8 })
9 const { getAllByRole } = within(list)
10 const items = getAllByRole("listitem")
11 const fruitNames = items.map(item => item.textContent)
12 expect(fruitNames).toMatchInlineSnapshot(`
13 Array [
14 "Bananas",
15 "Apples",
16 "Strawberries",
17 "Grapes",
18 "Oranges",
19 ]
20 `)
21})

If for some reason, you don’t want to use toMatchInlineSnapshot, then you could also use use toEqual.

1expect(fruitNames).toEqual([
2 "Bananas ",
3 "Apples ",
4 "Strawberries ",
5 "Grapes ",
6 "Oranges ",
7])

The benefit we get out of creating the list like fruitNames is that we could render some non-essential elements in our list and expect our test to pass.

for eg. let’s say we made the list to show some image

1{fruits.map(fruit => (
2 <li key={fruit}>
3 {fruit}
4+ <img src={imageSource} alt={fruit} />
5 </li>
6))}

Our tests snapshot will break 💔 because now the list item contains image.

1Bananas
2+ <img
3+ alt="Bananas"
4+ src="https://example.com/images/bananas"
5+ />

But the tests that test the textContent will not break 😱 as we are not adding any more text content. Depending on our use case, we can choose either approach. I would recommend taking text content (or any other relevant information) from the item instead of the complete list. We can add some unit tests to cover what exactly is rendered in each list item.

TL;DR

When testing list items, if you don’t care about the order in which the elements are rendered. We can test the length of the items rendered. And unit test how the list is rendered.

If we care about the ordering, then we can test the snapshot of the list. If the list has a lot of elements, we can pick the most important content from the list item and test against that. And again, we can test how the list item is rendered by unit tests.

Now we know how to test those awesome 🕺 lists you have.


Got a Question? Bala might have the answer. Get them answered on #AskBala

Edit on Github

Subscribe Now! Letters from Bala

Subscribe to my newsletter to receive letters about some interesting patterns and views in programming, frontend, Javascript, React, testing and many more. Be the first one to know when I publish a blog.

No spam, just some good stuff! Unsubscribe at any time

Written by Balavishnu V J. Follow him on Twitter to know what he is working on. Also, his opinions, thoughts and solutions in Web Dev.