For my latest project, it made sense to use the very nice CSS in JS solution called styled-components, along with snapshot testing, which is a way to save and compare snapshots of your component's rendering (HTML and CSS) in order to prevent unintended changes. One of this tests can replace several others checking for particular attributes of the rendering.
Be noted that, in order to have nice snapshots using Jest and Styled Components, you have to install this additional package. Just read the docs and be sure to do the configuration required in case you use Enzyme, like me.
Finally, when I went ahead using shallow()
by the book to test my button component, I wrote this:
it("renders correctly", () => {
const renderedComponent = shallow(<Button {...props}>Click me!</Button>);
expect(renderedComponent).toMatchSnapshot();
});
The output of the snapshot didn't look like the HTML or CSS I expected:
<Styled(styled.button)
theme={
Object {
"bg": "white",
"fg": "black",
}
}
>
Click me!
</Styled(styled.button)>
This initially puzzled me but then I realized that our Button
component is a stateless functional higher order component (HOC) that works like this:
export const Button = props => {
// assemble some props
// decide which button to create
return props.primary
? <PrimaryButton {...ourProps}>
{props.children}
</PrimaryButton>
: <RegularButton {...ourProps}>
{props.children}
</RegularButton>;
};
Since we are using shallow()
rendering, we won't get to the second level of components! So, this is where dive()
comes in. This function:
"Shallow render the one non-DOM child of the current wrapper, and return a wrapper around the result."
Turns out that we have two layers under our Button component, one for primary, icon buttons and so, and a "base class" for all of them. This means that we ended up having to do a double dive, so we called it a deepDive()
:
const deepDive = c => shallow(c).dive().dive();
it("renders correctly", () => {
const renderedComponent = deepDive(<Button {...props}>Click me!</Button>);
expect(renderedComponent).toMatchSnapshot();
});
In fact, if you don't know how many layers your component will wrap, you can generalize the idea using recursion:
const dd = w => (typeof w.type() === "function" ? dd(w.dive()) : w); // recursive diving
const deepDive = c => dd(shallow(c));
Look at how the type()
function on the ShallowWrapper
tells us if we have reached the DOM level or we can still dive()
again.
After deep diving, snapshots came out containing only CSS and basic HTML. We now have the liberty to work at ease with the component or any of its subcomponents knowing that no visual alteration will pass unnoticed by our tests.
Interaction tests work (we're only doing basic ones, clicks and disabled and so) and we got 100% coverage because every component layer goes exercised.
Top comments (3)
I just added a note on generalizing the diving using recursion.
Potential enhancement to the dd function as well would be a graceful failure (I'm sure someone at some point would be confused as to why w is undefined in a recursive function).
Really interesting solution though, love it!
You're very right!