jest: extending jest's assertion library

2021-01-17

 | 

~5 min read

 | 

815 words

Jest has a robust assertion library, but it’s also a general purpose tool. This can lead to some awkward tests, or imperative at least.

Fortunately, Jest’s assertion library is extensible! For example, testing-library has a whole suite of assertions specifically for the DOM designed to tie in with Jest. It’s called jest-dom and includes a number of Custom matchers. These matchers are designed to replace traditional assertions like toBe with more idiomatic variants like toHaveTextContent.

So, how is it used?

This post will walk through:

  1. Installing the necessary dependencies
  2. Importing the assertion library and updating an existing test
  3. Comparing the differences between the classic Jest assertions and others, like testing-library’s jest-dom
  4. Modifying the jest.config.js to avoid boilerplate.

Installing Dependencies

Which dependencies need to be installed will vary based on which assertion library you want to install in order to extend Jest. However, in the case of this demo, we’ll be using @testing-library/jest-dom, so, we’ll add that as a dev dependency in our package.json:

% yarn add --dev @testing-library/jest-dom

Extending Jest’s assertion library

Now that we have the dependency installed, we can modify an existing test. Let’s take this test checking the contents of a button and modify it.

The standard Jest test is fairly imperative, relying on the engineer writing the test to specify exactly what should be equal to what (i.e., the button’s textContent should be “Click Me”):

component.test.js
import React from "react"
import { render } from "@testing-library/react"
import Component from "../component"

test('the component renders with "Click Me" as its text', () => {
  const { getByTestId } = render(<Component />)
  const button = getByTestId("test-button-id")

  expect(button.textContent).toBe("Click Me")
})

If we import jest-dom, we can write a more declarative test:

component.test.js
import * as jestDOM from "@testing-library/jest-dom"import React from "react"
import { render } from "@testing-library/react"
import Component from "../component"

expect.extend(jestDOM)
test('the component renders with "Click Me" as its text', () => {
  const { getByTestId } = render(<Component />)
  const button = getByTestId("test-button-id")

  expect(button).toHaveTextContent("Click Me")})

By importing jest-dom we now have new assertions available once expect has been extended. In this case, we’re using the toHaveTextContent and targeting the entire element, instead of specifying the textContent of the element.

Clearer Error Messages

While writing more idiomatic code is a benefit, there’s likely an ever larger benefit to using jest-dom: clearer error messages in the event that the test fails.

For example, using the original test would print the following:

standard-jest-log
 FAIL   client  src/component.test.js
  ● the component renders with "Click Me" as its text

    expect(element).toHaveTextContent()

//highlight-start
    Expected: "Click Me"
    Received: "Click Me!"
//highlight-end

      15 |
    > 16 |   expect(button.textContent).toBe('Click Me')
         |                                  ^
      17 | })
      18 |

It’s easy to see that the expect doesn’t match the received. What’s less clear, however, is what those terms represent. To see that we need to look at the code block and see that we’re comparing the textContent of a DOM element to text.

Contrast that with the error printed when using the jest-dom assertions:

log-with-jestDOM
 FAIL   client  src/component.test.js
  ● the component renders with "Click Me" as its text

    expect(element).toHaveTextContent()

//highlight-start
    Expected element to have text content:
      Click Me
    Received:
      Click Me!
//highlight-end

      15 |
    > 16 |   expect(button).toHaveTextContent('Click Me')
         |                       ^
      17 | })
      18 |

Now it’s clear that we’re talking about an element and, specifically, inspecting it’s text content even before we get to the code block.

Avoiding Boiler Plate

Now that we’re getting real gains by extending Jest’s assertion library, let’s tackle the boiler plate code so that we can get these benefits without it!

In the most basic scenario, demonstrated above, every time we want to use the more specific assertions available via jest-dom, we have two additional lines in every file:

  1. import jest-dom
  2. extend expect

Fortunately, in the case of jest-DOM, ‘@testing-library’ exposes /extend-expect to slim this down to one line:

import "@testing-library/jest-dom/extend-expect"

But, still, we would need to do that for every test.

This is where Jest really shines in its flexible configuration. Jest has a setupFilesAfterEnv which accepts a list of modules and will run them before each test suite is executed.

These files could be relative paths (e.g., a ./jest-setup.js file), or modules within node_modules, which is the route we’ll take here:

jest.config.js
module.exports = {
  setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
}

With this in place, we do not need to have the import in our files but we still have access to the extended expect methods.

Wrap Up

Here we’ve walked through how to extend Jest’s native assertion libraries by demonstrating how to use testing-library’s jest-dom package. In doing so, we were able to make our tests more declarative, improve the readability of our error messages, and avoid additional cruft in our test suites through the use of Jest’s setupFilesAfterEnv API in its configuration.


Related Posts
  • Jest: How to configure Jest for testing Javascript applications
  • Jest: Debugging 'Not Implemented' Errors


  • 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!