Enhance your react-test-renderers with steroids
Do you like write component tests? Do you like react-test-renderer more than shallow renderer? Do you feel sometimes that it would be great to combine both shallow renderer and react-test-renderer to prevent rendering inner components? Do you feel headcache when writing tests for something like styled-components ?
If answer for these all questions is "Yes" then you come to the right place.
Much often, you want to mock/don't render external components. Almost always these external components are being imported through import statement import A from "./a"
. And very often you want to full render the inner (helpers or styled) components:
import Button from "./Button";import AnotherComp from "./AnotherComp"; const SomeText = <h1>SomeText</h1> const SomeLabel = styledlabel` font-size: 0.8em;`; const MyButton = ` font-size: 2em;` const Component = <div> <SomeText /> <AnotherComp /> <SomeLabel>Label</SomeLabel> <MyButton>Button</MyButton></div>
Here you probably want to fully render SomeText
, SomeLabel
and partially MyButton
(render styles but don't render Button
itself). AnotherComp
should remain non-rendered (so it's internals won't affect the component test).
This is not achievable by standard react-test-renderer, it will give you snapshot similar to this:
.SomeLabelCss {} .MyButtonCss {} // Shouldn't be here.ButtonCss {} // Shouldn't be here.AnotherCompCss {} SomeText // Shouldn't be here AnotherComp internals Label // Shouldn't be here Button
Shallow renderer also won't give you desired result:
// No styles since it doesn't unwrap styled-component HOC // Didn't unwrap the internal component // Didn't unwrap the internal component Label // Didn't unwrap the internal component Button
Ideally, you want this snapshot:
.SomeLabelCss {} .MyButtonCss {} SomeText> // External component shouldn't be rendered Label // Unwrap styled HOC but don't render further. Button already has dedicated test Button
This is achievable by mocking components using jest.mock()
but this is boring and repetetive task, especially when you have to mock many components.
Finally, now you can just use react-steroids-test-renderer
:
// same API as in react-test-rendererconst t = ;;
and it will you give the snapshot which you want with mocked external dependencies and fully-rendered internal components! and it's not shallow, so the lifecycle, refs etc will continue to work.
Installation and setup
yarn add react-steroids-test-renderer --dev
or
npm install react-steroids-test-renderer --save-dev
Add to your .babelrc
/ .babelrc.js
(you can use it without babel, see below)
plugins: [
"react-steroids-test-renderer/babel",
]
Make sure you apply it only for test runs, i.e. for env/NODE_ENV = test or similar
API
react-steroids-test-renderer
wraps react-test-renderer
so it has same API, only the create()
is slightly different:
options are the standard react-test-renderer
options (i.e. { createNodeMock () => {} }
) with few additions:
;
filter
allows you to control which elements you don't want to render in the tree. By default it blocks all imported components:
// Don't render elements created from imported components or SomeComponent const t = ;
imported
boolean flag is coming from babel transform. If you're not using babel-transform it will be always false
wrapperElement
allows you to additionally wrap the tree into some other component. Ideally for <ThemeProvider />
!:
const t = ;
Of course the wrapperElement
should render their children
How does it work
- The babel-transform adds
__imported={Ref}
prop to every JSX element which identifier has been referenced in import. This also works for HOC components - The
create()
substitutes every non filtered out component with fake one (with same type) with customrender()
, which calls originalrender()
to obtain react elements for the tree, then strips__imported
flag from the tree completely and callsfilter()
function for their children. - The
element.type
for filtered out components is being replaced with string with same name - The result of previous steps is being passed to original
react-test-renderer.create()
, wrapping the tree ifwrapperElement
was defined tree.root.findByType/find/etc...
are being replaced with custom lookup functions, sotree.root.findByType(MyCustomComponent /* Function */);
will be replaced totree.root.findByType("MyCustomComponent" /* string */)
for filtered out components.
Caveats
- Since element types were substituted, checking for type equality, for example
element.type === MyComponent
may not work. Use some static flag attached to the component or similar to do the check. - styled-components, emotion, etc... When wrapping another styled-component you may have a problems:
import Button from "./button"; const MyButton = ` color: green;`; <MyButton />;
If MyButton
renders Button
as child, i.e. MyButton -> Button -> Button internals
, you're lucky (when processing the rendered tree the imported
flag will be true for Button
). But few libraries, for example emotion, combines all styled HOCs into one component and renders first component directly (i.e. MyButton -> Button internals
). For this reason imported
flag in the filter will be always false for such components. You need to write custom filter for this:
const t = ;
This will hide <Button />
internal details from resulting snapshot
Tip: Wrap create()
with your custom filter to make it work across the whole project: export const testRenderer = element => create(element, { filter: ... })