jest: abstract away utilities with moduledirectories

2021-05-01

 | 

~6 min read

 | 

1041 words

In many React applications, there’s an <App/> component that is wrapped by a number of providers - themes, redux, routing, etc. When it comes to testing, this should generally be transparent. We shouldn’t care about the providers because most of the time we want to test our components in isolation. Unfortunately, that’s not always possible.

Take theming for example. With a theme, the idea is the user determines the theme at the root of the application and then that choice is respected all the way down the tree. Said another way, a theme may affect every component’s CSS.

If we’re using Jest snapshots, then the snapshot may look at the styles that are being applied to an element and not account for what is injected by the theme.

src/app.js
import React from "react"
import { ThemeProvider } from "emotion-theming"
import MainComponent from "./shared/MainComponent"
import * as themes from "./themes"

function App() {
  const [theme, setTheme] = React.useState("dark")
  const handleThemeChange = ({ target: { value } }) => setTheme(value)
  return (
    <div>
      <ThemeProvider theme={themes[theme]}>
        <MainComponent />
      </ThemeProvider>
    </div>
  )
}

export default App

If we had previously been testing the MainComponent with a snapshot, like below, it would fail (assuming that the Main Component does in fact change based on the theme):

src/shared/__tests__/MainComponent.test.js
import React from "react"
import { render } from "@testing-library/react"
import MainComponent from "./MainComponent"

test("it renders", () => {
  const { container } = render(<MainComponent value="0" />)
  expect(container.firstChild).toMatchInlineSnapshot()
})

Explicit Rendering

One way to account for this is by rendering the full context so that our tested MainComponent is rendered within the theme provider just like it is in the app.

src/shared/__tests__/MainComponent.test.js
import React from "react"
import { render } from "@testing-library/react"
import MainComponent from "./MainComponent"
import { ThemeProvider } from "emotion-theming"
import { dark } from "./themes"

test("it renders", () => {
  const { container } = render(
    <ThemeProvider theme={dark}>
      <MainComponent value="0" />
    </ThemeProvider>,
  )
  expect(container.firstChild).toMatchInlineSnapshot()
})

The issue is that this is rather verbose and we’d need to do it for every test.

Wrapper Solution

One step toward improving this problem is with a wrapper function that will take a component, wrap it with the theme provider, and then return it. While this means we need to call a function every time, the bulk of the work is done only once. For example:

src/shared/__tests__/MainComponent.test.js
import React from "react"
import { render } from "@testing-library/react"
import MainComponent from "./MainComponent"
import { ThemeProvider } from "emotion-theming"
import { dark } from "./themes"

function Wrapper({ children }) {
  return <ThemeProvider theme={dark}>{children}</ThemeProvider>
}

test("it renders", () => {
  const { container } = render(<MainComponent value="0" />, {
    wrapper: Wrapper,
  })
  expect(container.firstChild).toMatchInlineSnapshot()
})

Note: This is taking advantage of the wrapper option of testing-library’s render API.

Abstract Away! Mask Testing-Library’s Render

Instead of creating a custom wrapper per test, we can actually create a new render method that masks the one exported by testing-library. The one we create, however, can be used to provide all of the the context providers our app relies on - theming, redux, toasts, etc.

One step at a time. First, let’s just make it so our tests don’t need to specify the wrapper:

src/shared/__tests__/MainComponent.test.js
import React from "react"
import { render } from "@testing-library/react"
import MainComponent from "./MainComponent"
import { ThemeProvider } from "emotion-theming"
import { dark } from "./themes"

+ function renderWithProvider(ui, options) {
+     return render(ui, { wrapper: Wrapper, ...options })
+ }

function Wrapper({ children }) {
    return <ThemeProvider theme={dark}>{children}</ThemeProvider>
}

test("it renders", () => {
-     const { container } = render(<MainComponent value="0" />, {wrapper: Wrapper})
+     const { container } = renderWithProvider(<MainComponent value="0" />)
    expect(container.firstChild).toMatchInlineSnapshot()
})

renderWithProviders is long though, and we want this to feel as natural as possible. So, let’s do a little more refactoring:

src/shared/__tests__/MainComponent.test.js
import React from "react"
- import { render } from "@testing-library/react"
+ import { render as rtlRender } from "@testing-library/react"
import MainComponent from "./MainComponent"
import { ThemeProvider } from "emotion-theming"
import { dark } from "./themes"

- function renderWithProvider(ui, options) {
-     return render(ui, { wrapper: Wrapper, ...options })
+ function render(ui, options) {
+     return rtlRender(ui, { wrapper: Wrapper, ...options })
}

function Wrapper({ children }) {
    return <ThemeProvider theme={dark}>{children}</ThemeProvider>
}

test("it renders", () => {
+     const { container } = renderWithProvider(<MainComponent value="0" />)
-     const { container } = render(<MainComponent value="0" />)
    expect(container.firstChild).toMatchInlineSnapshot()
})

And now, the final step so that other tests can take advantage of this “enhanced” render function, we can move it into a utility’s directory:

./test/test-utils.js
import React from "react"
import PropTypes from "prop-types"
import { render as rtlRender } from "@testing-library/react"
import { ThemeProvider } from "emotion-theming"
import * as themes from "../src/themes"

function render(ui, { theme = themes.dark, ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider theme={theme}>{children}</ThemeProvider>
  }
  Wrapper.propTypes = {
    children: PropTypes.node,
  }

  return rtlRender(ui, { wrapper: Wrapper, ...options })
}

export * from "@testing-library/react"
// override the built-in render with our own
export { render }

Then, within our test, we’d just import this new utility and everything looks like it used to - except we’re importing all of our testing library methods from test-utils instead of @testing-library/react:

src/shared/__tests__/MainComponent.test.js
import React from "react"
import { render } from "../../../test/test-utils"
import MainComponent from "./MainComponent"

test("it renders", () => {
  const { container } = render(<MainComponent value="0" />)
  expect(container.firstChild).toMatchInlineSnapshot()
})

Treat Utils As A Module

The last step is to make is so that we don’t have to use relative imports for our test utilities. It’d be really nice if, instead of ../../../test/test-utils we could just import from test-utils.

Fortunately, this is the same process I wrote about in Jest: Configuring Jest to Resolve Modules. Since everything is in our test directory in the root of the project, it’s adding one more module:

webpack.config.js
const path = require("path")

module.exports = {
    resolve: {
-         modules: ["node_modules", path.join(__dirname, "src"), "shared"],
+         modules: ["node_modules", path.join(__dirname, "src"), "shared", path.join(__dirname, "test")],
    },
}

This change will make it so that we can import test-utils instead of ../../../test/test-utils, though, depending on your linting configuration it may throw an linting error.1

Footnotes


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!