Have you ever wondered where linting rules come from or how they’re written? Recently a couple of Double Agents (Dale Karp and Josh Justice) found the need for a new linting rule, got together to implement said rule, and contributed it to a linting library so everyone could benefit.

We thought breaking that process down could also be useful. In this post, Dale discusses why linting is valuable, takes a look at this new lint rule, and walks through the impetus behind its creation. Then, in a follow-up post, Josh will do a deep dive into the process of pairing on creating the rule and getting it published in an open-source library.

Why lint?

Why are linters an important tool in a developer’s kit? A linter provides many benefits. It can guide developers toward best practices or point out issues with specific code patterns while recommending alternative implementations. A linter can be used to enforce specific rules within a code base to maintain a consistent style across different files and authors.

When a linter is integrated into an IDE or code editor, it can feel like pair programming with a very knowledgeable developer - the kind that has a deep understanding of the most obscure edge cases. This allows us to free up space in our minds to focus on solving the actual problems at hand instead of fretting over smaller details like formatting or using the correct language constructs for the job.

Linters can be useful in reminding developers about best practices if they haven’t worked in a particular language for an extended period of time. Linters can also be great teaching tools while exploring a new language or framework. For instance, Clippy — the primary linting tool for the Rust language — goes beyond pointing out not-so-obvious problems. It also offers alternatives to existing code — using your own code as an example in order to write Rust in a more idiomatic way.

React Testing Library and queries

React Testing Library (or RTL) is a popular tool used to help write tests for React components. The library supplies convenience functions specific to React for rendering components and querying & interacting with DOM nodes.

When paired with a test runner such as Jest, developers can chain together RTL queries with different “matchers” — functions that test for a specific thing to be true or false. This allows a developer to write easy-to-read and succinct tests. Matchers are provided by both Jest and RTL and operate on the result of an expect() call in Jest — the expectation object.

For example, given the following statement:

expect(screen.getByRole("dialog")).toBeVisible();

We use the query function getByRole() to retrieve a specific element from the DOM tree. The result of this query is given to Jest’s expect(), which returns an expectation object. We can then run matchers such as toBeVisible() in order to assert that our DOM node should be visible to users.

With RTL, there are three “families” of query functions one can use. The first are the get* queries. The get* queries are used to look for elements that are assumed to exist on the page. If a get* query fails to return the expected number of results, an error is thrown by the query function itself.

The next type are the query* queries. While saying “query queries!” can be quite the mouthful, they are simple to understand. query* queries work similarly to get* except the function will not throw an error if the desired DOM node is not found; instead, null is returned. This is useful for tests where we want to assert that a specific DOM node will not exist within the tree.

Finally, we have the find* queries. These are similar to get* but use the Promise API to wait for elements that appear asynchronously. The promise will be rejected with an error if the desired DOM node is not found. We won’t be discussing these queries in this post, but it’s good to know that they exist.

Let’s do a quick recap of everything discussed so far: We use a query from RTL to search for a DOM node in our component. The result of this query is passed to Jest’s expect() function so we can use matchers to assert that some property of our component is true or false.

Now that we have an understanding of how both RTL and Jest queries work, it’s time for a story about how one linting rule could have saved me some time and a headache after an extended absence from using React and RTL.

The story so far…

After months of working on migrating a legacy backend codebase at a client, I was finally back to writing React! The work in question involved contributing to a client’s new component library. While writing tests for the component I was building, I wrote an assertion:

expect(screen.queryByLabelText("Close")).toBeVisible();

On the surface, there seems to be nothing wrong with this query. We want to make sure that a DOM element with the label text “Close” is visible to a user. Easy, right?

Unfortunately, the semantics of the statement are not quite correct. As discussed above, query* will return the element if it exists in the DOM tree or null if no match is found. As the author of the test, I know that this element will exist in the tree when making this assertion. But will that be clear to others who may look at these tests in the future?

The statement written above reads as “An element with a label of ‘Close’ might exist, and if it does, it should be visible.” But if we were to change the query in our example to getByLabelText, the statement would read, “An element with a label of ‘Close’ will exist in the tree, and it will be visible!” While the difference might seem subtle, using the correct functions for tests can communicate intent more clearly. It can also lead to error messages that more accurately describe what went wrong. If I stuck with queryByLabelText and, for some odd reason, the element was not in the DOM, the following error would be raised from the matcher:

received value must be an HTMLElement or an SVGElement. Received has value: null

After thinking about it for a bit, I’d eventually figure out what went wrong, but it wouldn’t be obvious from the get-go. The error message raised by getByLabelText would describe the problem clearly:

TestingLibraryElementError: Unable to find a label with the text of: Close

The error message is now thrown by the call to getByLabelText. Succinct error messages lead to less time spent scratching our heads and allow us to quickly fix the issue and move on.

Creating a rule

I wondered to myself why a linter hadn’t caught this particular error. It seemed like a common mistake anyone could make — especially when said person hadn’t written any React in a good few months! There is a plugin for the popular JavaScript linter eslint, called eslint-plugin-testing-library, that adds linting rules covering best practices for writing tests with RTL. I would have thought that the rules included with the plugin would have caught this particular mismatch of query and matcher, but it did not.

Fate must have dictated that I encounter this particular issue. Around the same time, my colleague Josh Justice encountered the exact same issue! He put a call out in our company Slack to pair with another Double Agent to fix this, and I couldn’t help but respond.

The fruit of our labour was prefer-query-matchers — a new configurable rule in eslint-plugin-testing-library. This rule Josh and I created allows for the configuration of matchers to be restricted for use with a specific type of query.

For example, if I want my linter to have my back and kindly remind me that what I did above might not be completely clear to others, I could configure a rule to restrict the toBeVisible matcher to usage with get* queries:

rules: {
  ...,
  'testing-library/prefer-query-matchers': [
    'error',
    {
      validEntries: [{ matcher: 'toBeVisible', query: 'get' }],
    },
  ],
},

Now my editor will kindly remind me to use the correct query, saving me time and future headaches!

VSCode reporting eslint errors in the editor using the above example scenario

Using the rule

If you’ve read this post and thought to yourself, “Wow, I sure could use this linting rule in my own project!” here’s how to enable it:

  1. Install eslint and eslint-plugin-testing-library. eslint-plugin-testing-library should be at least v5.11.0 in order to have access to the prefer-query-matchers rule.

  2. In your project’s eslint configuration file, enable the rule by adding the following key/value to the rules object:

    'testing-library/prefer-query-matchers': [
      'error',
      {
        validEntries: [{ matcher: 'toBeVisible', query: 'get' }],
      },
    ],

And that’s it! With the rule enabled, you too can have one less thing to keep in your working memory, knowing that eslint and prefer-query-matchers have your back.

Now you have everything you need to put the prefer-query-matchers rule into use. If you’d like to know more about the process of writing it, in the next post, Josh Justice will talk about his own encounter with using the wrong query for a matcher, putting out the call to pair on creating the rule, and our experience of contributing to an open source project.

Join the conversation about this post on our N.E.A.T community

Not a N.E.A.T. community member yet? More info.

Dale Karp

Person An icon of a human figure Status
Double Agent
Hash An icon of a hash sign Code Name
Agent 00116
Location An icon of a map marker Location
Toronto, ON

Josh Justice

Person An icon of a human figure Status
Sleeper Agent
Hash An icon of a hash sign Code Name
Agent 00155
Location An icon of a map marker Location
Conyers, GA