@render-with/decorators
v5.0.0
Published
A render function that enables the use of decorators that elegantly wrap a component under test in providers.
Downloads
2,423
Readme
Render decorators 🪆 for React Testing Library
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
- Setup
- The problem
- The solution
- Wrapping Order
- Decorators
- API
- Issues
- Changelog
- Contributors
- LICENSE
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
:
and supports the following node
versions:
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:
@render-with/react-router
@render-with/react-intl
@render-with/redux
@render-with/material-ui
@render-with/backstage
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.
💡 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.
📚 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.
❓ Questions
For questions related to using the library, file an issue on GitHub.
Changelog
Every release is documented on the GitHub Releases page.
Contributors
Thanks goes to these people:
This project follows the all-contributors specification. Contributions of any kind welcome!