Build Reusable Testing Queries 
Tags: react, testing, javascript, webdev
Published: 2019-06-11 13:51:42

---
title: Build Reusable Testing Queries 
published: true
description: Keep a testbase maintainable and less brittle with reusable test utils
tags: react, testing, javascript, webdev
cover_image: https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?ixlib=rb-1.2.1&auto=format&fit=crop&w=2948&q=80
---

This post is inspired by a problem I had about two weeks ago; I wrote brittle tests that interacted with the `Select` component from React Material UI. After a bunch of time spent that day attempting many solutions, I landed on one that I am satisfied with... That solution is what I am sharing today!

**TLDR;** Keep a testbase maintainable and less brittle by sharing reusable DOM queries. The patterns for accessing "abstraction details" of a third-party component can change over time, but updates can be made in a single spot.

## The Problem

I want to write tests that are maintainable and resemble the way my software is used. This means I need to simulate user interaction within components, including any third-party component. However...

1. Data attributes may not appear in a third-party component.
2. Data attributes may not appear on the intended element inside a third-party component.

*I am a huge fan of data-testids, but I can't always rely upon them when working with a third-party component.*

**Quick Aside: The Material `Select` component uses `react-select`. This post will only use `react-select` in a contrived example...**

After some debugging, I discovered an `id` on the `input` tag inside `react-select`.

```jsx
<input
  aria-autocomplete="list"
  autocapitalize="none"
  autocomplete="off"
  autocorrect="off"
  id="react-select-2-input" {/* That's helpful! */}
  spellcheck="false"
  style="box-sizing: content-box; width: 2px; border: 0px; font-size: inherit; opacity: 1; outline: 0; padding: 0px;"
  tabindex="0"
  type="text"
  value=""
/>
```
After testing by querying for the `id`, I discovered that it increments based on the amount of rendered Select components on the page. I wouldn't trust this as a test id! This can potentially change at anytime causing cascading test failures. A good rule of thumb is to have a reserved id for testing. However, we don't have access to use data attributes or this `id` on input anymore... I would rather have an `id` on the root tag of the component anyways; then I can query anything scoped inside the component... Turns out, I can do this!

*"Here is a hot take"*, if a component package does not allow data attributes, read the documentation and learn what can be passed as a substitute. There may be an `id` or something that can be rebranded as a test id. In my case, I can do exactly that. In my contrived example, I can create my own internal `Select` component that reintroduces `react-select` with a required `dataTestId` prop. Now I can use my internal component that has a **trusted** test id. 

```jsx
// Select.js
import ReactSelect from 'react-select'
import React from 'react'
import PropTypes from 'prop-types'

function Select({ dataTestId, ...props }) {
  return <ReactSelect {...props} id={dataTestId} />
}

Select.propTypes = {
  dataTestId: PropTypes.string.isRequired,
}

export default Select
```

## The Solution

Let's carry on with some good old fashion “acceptance criteria.”

* I see my selected value in the input field of the `Select` component
* I see my selected value in the `span` **directly below** the `Select` component

Here is the working contrived example that meets the acceptance criteria, but we need tests to **ensure we avoid regression in production!**

```jsx
import React from 'react'
import Select from './Select'

const options = [
  { value: 'chocolate', label: 'Chocolate' },
  { value: 'strawberry', label: 'Strawberry' },
  { value: 'vanilla', label: 'Vanilla' },
]

function App() {
  const [selectedOption, setSelectedOption] = React.useState({})

  return (
    <div>
      <Select
        dataTestId="select-ice-cream"
        value={selectedOption}
        onChange={valSelected => setSelectedOption(valSelected)}
        options={options}
      />
      <span data-testid="select-ice-cream-selected">You selected {selectedOption.value}</span>
    </div>
  )
}

export default App
```

If we were to inspect the third-party component, there's a lot of `div`s and stuff within it. A lot of "abstraction details" that we don't care about. It can be rather difficult testing an unmocked third-party component, but doing so gives me better confidence that the application works correctly. Alright, since we are not using `data-testid`, we cannot use the `queryByTestId` selector from React Testing Library. I am going to use the DOM `querySelector` instead...

```js
it('renders without crashing', () => {
  const { container, debug } = render(<App />)
  const inputEl = container.querySelector('[id="select-ice-cream"] input')
  debug(inputEl)
})
```
*I don't know of a React Testing Library query available to us that queries for an attribute. That's why we're using the DOM `querySelector`.* We can do better though, we can turn the above into a custom query! And even better, I will return an object with elements that are needed for fulfilling the acceptance criteria!

```js
it('shows selected value in input field and right below select', () => {
  const { querySelectComponent } = render(<App />, {
    queries: {
      ...queries,
      querySelectComponent: (root, id) => {
        return {
          rootEl: root.querySelector(`[id=${id}]`),
          inputEl: root.querySelector(`[id=${id}] input`),
          spanEl: document.querySelector(
            `div[id=${id}] + span[data-testid='${id}-selected']`
          ),
        }
      },
    },
  })

  const { rootEl, inputEl, spanEl } = querySelectComponent('select-ice-cream')

  fireEvent.change(inputEl, { target: { value: 'strawberry' } }) // change input value to strawberry
  fireEvent.keyDown(inputEl, { key: 'Tab', code: 9 }) // select what the input value has as the selected value

  //Assertions!
  expect(spanEl).toHaveTextContent(/strawberry/)
  expect(getByText(rootEl, 'Strawberry')).toHaveTextContent('Strawberry')	
})
```
The test block now covers the acceptance criteria! And yes, we have a very specific selector containing abstraction details. `div[id=${id}] + span[data-testid='${id}-selected']`. That selector is to make sure that the span appears directly below `Select` as the Acceptance Criteria describes. _The user should select a value and see the selected value in the input field of `Select` and within the `span` **directly below** `Select`._  

The current test block has queries to abstract the details of component selectors. It is ideal having the queries reusable inside any test block. Anyone who needs to interact with the `Select` component, can use the same selector patterns within their tests. Every test can reuse the same pattern for accessing abstraction details of a third-party component, or possibly an internal component. But when `react-select` updates, I can update my queries from a single spot!

```js
//testUtils.js
export const selectComponentQueries = (root, id) => {
  return {
    rootEl: root.querySelector(`[id=${id}]`),
    inputEl: root.querySelector(`[id=${id}] input`),
    spanEl: document.querySelector(
      `div[id=${id}] + span[data-testid='${id}-selected']`
    ),
  }
}
```

```js
//App.test.js
it('shows selected value in input field and right below select', () => {
  const { container } = render(<App />)

  const { rootEl, inputEl, spanEl } = selectComponentQueries(
    container,
    'select-ice-cream'
  )

  fireEvent.change(inputEl, { target: { value: 'strawberry' } })
  fireEvent.keyDown(inputEl, { key: 'Tab', code: 9 })

  expect(spanEl).toHaveTextContent(/strawberry/)
  expect(getByText(rootEl, 'Strawberry')).toHaveTextContent('Strawberry')
})
```

## Conclusion

Abstraction details of components can change. Keep a testbase maintainable and less brittle with shareable test utils for something like queries. That way, all tests use the same reusable code. Having queries in a single source will allow change to come much easier.

---

Hello! I'm [Jon Major Condon](https://jonmajorc.me/). I am a Senior Software Farmer that tends to client codebases at [Bendyworks](https://bendyworks.com/). As a farmer of software, I focus on anything web, but my curiosity usually leads me down rabbit holes... *"Jon Major just fell down another rabbit hole… Stay tuned for the next blog post! 👋"*