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())
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
:
and supports the following node
versions:
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.
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:
MemoryRouter
StoreProvider
ThemeProvider
IntlProvider
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?
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:
with
and pick a decorator)Note: This solution is partly inspired by Storybook Decorators.
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.
Here's an (incomplete) list of libraries that provide decorators for some known libraries:
@render-with/react-router
@render-with/react-intl
@render-with/redux
@render-with/material-ui
@render-with/backstage
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.
Looking to contribute? PRs are welcome. Checkout this project's Issues on GitHub for existing issues.
Please file an issue for bugs, missing documentation, or unexpected behavior.
Please file an issue to suggest new features. Vote on feature requests by adding a π. This helps maintainers prioritize what to work on.
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.
For questions related to using the library, file an issue on GitHub.
Every release is documented on the GitHub Releases page.
Thanks goes to these people:
cultivate(software) πΌ π΅ |
David Bieder π» β οΈ π π π π§ π€ |
Jerome WeiΓ π π π§ |
Maurice Reichelt π π π§ |
This project follows the all-contributors specification. Contributions of any kind welcome!