@render-with/decorators
TypeScript icon, indicating that this package has built-in type declarations

2.0.11 • Public • Published

Render decorators 🪆 for React Testing Library

GitHub Workflow Status Code Coverage npm (scoped) NPM PRs welcome All Contributors

A render function that enables the use of decorators which elegantly wrap a component under test in providers:

render(<LoginForm />, withStore({ user: 'john.doe' }), withLocation('/login'), withTheme())

Table of Contents

Installation

This library is distributed via npm, which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev @render-with/decorators

or

for installation via yarn:

yarn add --dev @render-with/decorators

This library has the following peerDependencies:

npm peer dependency version

and supports the following node versions:

node-current (scoped)

Setup

In your test-utils file, re-export the render function that supports decorators:

// test-utils.js
// ...
export * from '@testing-library/react'  // makes all React Testing Library's exports available
export * from '@render-with/decorators' // overrides React Testing Library's render function

Then, install some decorators for the libraries used in your project:

npm install --save-dev @render-with/react-router @render-with/redux

Note: You can find an (incomplete) list of libraries with render decorators here.

Next, configure the decorators in your test-utils file (please refer to the individual decorator library's documentation for a complete setup guide):

// test-utils.js
// ...
export * from '@render-with/react-router' // makes decorators like withLocation(..) available
export * from '@render-with/redux'        // makes decorators like withState(..) available

And finally, use the decorators in your tests:

import { render, withStore, withLocation, withTheme } from './test-utils'

it('shows home page when logged in already', async () => {
  render(<LoginForm />, withStore({ user: 'john.doe' }), withLocation('/login'), withTheme())
  // ...
})

Note: With configureRender it is possible to create a custom render function with default decorators that will be applied in all tests without having to explicitly mention the default decorators.

The problem

Rendering React components in tests often requires wrapping them in all kinds of providers. Some apps require dozens of providers to function correctly. For instance:

  • React Router MemoryRouter
  • Redux StoreProvider
  • Material UI ThemeProvider
  • Format.JS IntlProvider
  • Backstage ApiProvider
  • ...

And there are, of course, all the custom Context.Providers.

React Testing Library recommends creating a custom render function:

// test-utils.js
const AllTheProviders = ({ children }) => (
  <ProviderA {/* ... */}>
    <ProviderB {/* ... */}>
      <!-- ... -->
        <ProviderZ {/* ... */}>
          {children}
        </ProviderZ>
      <!-- ... -->
    </ProviderB>
  </ProviderA>
)

const customRender = (ui, options) =>
  render(ui, {wrapper: AllTheProviders, ...options})

export { customRender as render }

But a custom render function is not always the best solution.

Some larger projects require a lot of providers and rendering all providers is not always possible.

Some tests need a little more control over the providers being rendered.

Defining a custom render function on a test-file-basis is possible, but it can introduce a lot of boilerplate code:

import { configureStore } from '@reduxjs/toolkit'

const renderComponent = () => render(
  <ThemeProvier theme='light'>
    <MemoryRouter initialEntries={[ '/login' ]} initialIndex={0}>
      <StoreProvider store={configureStore({ reducer, preloadedState: { user: 'john.doe' }, middleware })}>
        <LoginForm />
      </StoreProvider>
    </MemoryRouter> 
  </ThemeProvier>
)

// ...

it('shows home page when logged in already', async () => {
  renderComponent()
  await userEvent.click(screen.getByRole('button', { name: /login/i }))
  expect(screen.getByRole('heading', { name: /home/i })).toBeInTheDocument()
})

Another downside of the renderComponent() test helper function above is: The test becomes harder to read.

It is unclear what component is actually rendered by just looking at the test itself. What component is the test testing?

The solution

This library provides a customized render function that accepts the component under test and a set of elegant decorators that can be used to wrap a rendered component:

it('shows home page when logged in already', async () => {
  render(<LoginForm />, withStore({ user: 'john.doe' }), withLocation('/login'), withTheme())
  await userEvent.click(screen.getByRole('button', { name: /login/i }))
  expect(screen.getByRole('heading', { name: /home/i })).toBeInTheDocument()
})

Here are some benefits of using render decorators:

  • more maintainable code (one line vs. many lines of boilerplate code; easier to read)
  • autocompletion (just type with and pick a decorator)
  • flexibility (pick only the providers needed for the test case at hand)
  • less mocking (decorators mock what needs to be mocked)
  • simplicity (some providers make non-trivial testing aspects, like navigation, easy to test)
  • customization (decorators can often be configured further with additional arguments)
  • improved performance (no need to render providers that are not needed)

Note: This solution is partly inspired by Storybook Decorators.

Wrapping Order

The order of the decorators determines how the rendered component will be wrapped with providers.

The decorator listed first (closest to the component) will be first to wrap a provider around the component. The decorator listed last will be responsible for the outermost provider.

For instance:

render(<LoginForm />, withStore({ user: 'john.doe' }), withLocation('/login'), withTheme())

will result in:

<ThemeProvier theme='light'>
  <MemoryRouter initialEntries={[ '/login' ]} initialIndex={0}>
    <StoreProvider store={configureStore({ reducer, preloadedState: { user: 'john.doe' }, middleware })}>
      <LoginForm />
    </StoreProvider>
  </MemoryRouter>
</ThemeProvier>

Note: It works a bit like Matrjoschka 🪆 puppets.

Decorators

Here's an (incomplete) list of libraries that provide decorators for some known libraries:

API

Note: This API reference uses simplified types. You can find the full type specification here.

function render(ui: ReactElement, ...decorators: Decorator[]): RenderResult

Wraps the element (usually the component under test) in providers using the given list of decorators and renders the final result. Providing no decorators will simply render the element.

function configureRender(...defaultDecorators: Decorator[]): typeof render

Creates a render function that wraps the component under test in providers using the given list of default decorators and (if applicable) in providers using the list of decorators passed to the created render itself.

function decorate(ui: ReactElement, ...decorators: Decorator[]): ReactElement

Wraps the given element in providers using the given list of decorators. Providing no decorators will simply return the element.

type Decorator = (ui: ReactElement) => ReactElement;

A Decorator wraps an element in a provider and returns the resulting element.

Issues

Looking to contribute? PRs are welcome. Checkout this project's Issues on GitHub for existing issues.

🐛 Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

💡 Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on.

See Feature Requests

📚 More Libraries

Please file an issue on the core project to suggest additional libraries that would benefit from decorators. Vote on library support adding a 👍. This helps maintainers prioritize what to work on.

See Library Requests

❓ Questions

For questions related to using the library, file an issue on GitHub.

See Questions

Changelog

Every release is documented on the GitHub Releases page.

Contributors

Thanks goes to these people:

cultivate(software)
cultivate(software)

💼 💵
David Bieder
David Bieder

💻 ⚠️ 📖 👀 🚇 🚧 🤔
Jerome Weiß
Jerome Weiß

📖 🚇 🚧
Maurice Reichelt
Maurice Reichelt

📖 🚇 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT

Package Sidebar

Install

npm i @render-with/decorators

Weekly Downloads

454

Version

2.0.11

License

MIT

Unpacked Size

19.4 kB

Total Files

5

Last publish

Collaborators

  • mauricereichelt
  • davidbieder