testing library: different ways to skin a cat

2020-09-11

 | 

~3 min read

 | 

560 words

https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

I was recently writing some tests using testing-library when I found myself needing to select a checkbox wrapped within multiple other elements to fire click event. In investigating how to do this, I found several APIs that are quite useful, both specific to testing, but also standard Web APIs.

Let’s consider the example where I have a <List> that takes an array of ListItems and renders each one as a <ListItem>. The <ListItem> is essentially name with a checkbox next to it.

The <List> is supposed to limit one box is checked at a time. Once one was checked, others would be disabled. A simplified view of the <ListItem> component:

ListItem.tsx
function ListItem(props) {
    const {
        isSelectable,
        isChecked,
        isIndeterminate,
        setSelected,
        disabled,
        inputIcon,
    } = props
    return (
        <div data-testid={"listItem"}>
            <div>
                {isSelectable && (
                    <div data-testid={"checkbox-listItem"}>
                        <input
                            type="checkbox"
                            checked={isChecked}
                            indeterminate={isIndeterminate}
                            onChange={setSelected}
                            disabled={disabled}
                        />
                        <Icon icon={inputIcon}>
                    </div>
                )}
                <div>{title}</div>
            </div>
        </div>
    )
}

To test this, I took advantage of the getBy* queries of testing-library, but also mixed in some native APIs, e.g., querySelector, and learned about others, like within (a testing-library API) and closest (a native one).

Here was my first attempt:

__tests__/ListItem.tsx

test('my test', async () => {
    const items = [{key: '1',title: '1'}]

    const { getByTestId} = render(<List items={items} />)
    async waitFor(() => {
        const ListItem = getByTestId('listItem')
        const nodeInputWrapper = ListItem.querySelector('div')?.querySelector('div');
        const checkbox = nodeInputWrapper && (getByRole(nodeInputWrapper, 'checkbox') as HTMLInputElement);
    })
})

There are multiple ways I could have made this better:

  1. I could have made this more concise by going directly to the checkbox-listItem data tag instead of using the querySelector (at the time, however, I didn’t know the checkbox-listItem variant existed).

  2. In retrospect, it would have been trivial to add a more specific data tag - perhaps selecting the checkbox itself instead of the wrapper. This would have obviated the need to use any other APIs.

  3. Understanding that there was only one checkbox within the ListItem, I could skip the nodeInputWrapper step altogether:

    const ListItem = getByTestId("listItem")
    const checkbox = getByRole(ListItem, "checkbox") as HTMLInputElement
  4. If I had needed to look up the tree (i.e. at parents), I could have used the closest API. This is not part of testing-library, but is a standard web API. Not a great use case here for me, however Giorgio Polvara has a nice one in his blog post on testing-library

  5. Another lesson from Giorgio was how to think about the within API for testing-library. In this case if I have multiple items in my list getBy will throw an error. In that case, one solution would be to getByText (again, assuming unique text), and then using the closest to find the right parent. Once that was selected, I would be able to constrain the search using within. Of course, just typing this out is an excellent reminder for the value of the data-testid approach! It’s so much simpler!

The point, of this post at least, is that there’s often multiple solutions to a problem. Some of them will be simpler, though not all. Knowing different ways to solve a problem, however, allow you to see different ways to put pieces together. I’m glad I took a deeper look at the different queries to understand how they might work and that there are some native Web APIs that are also helpful!



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!