Artwork by Alan B Smith

Testing styled-components with Jest Snapshots

Alan B Smith
đź’… styled-components

--

An Update

I cannot recommend testing UI with Jest Snapshots, as I believe it is a testing anti-pattern. Please opt for visual regression tests instead. Justin describes this sentiment much more succinctly in this tweet. Please don’t @ him though.

I’ll leave the article below for now, but please advocate for more effective testing patterns on your teams and organizations.

Overview

Writing UI tests can be challenging. This article describes helpful patterns for testing styled-components with Jest snapshots. Snapshot tests provide really helpful validation to ensure styles stay consistent. I’ve added a companion repo on Github for this article, if you’d like to code-along. To pull the repo down locally:

Clone Example Repo Instructions

While this article focuses on Snapshot tests, the example repo has a handful of other useful tools as well. You can find a list of them in the project’s README here. I think you’ll find these helpful as you write your own tests below.

The Problem

The React ecosystem has a lot of great test utilities for validating component business-logic. However, this starts to break down with logic-less components. To help illustrate this, let’s look at an example using Expect and Enzyme.

This is our example styled component, a simple button.

Simple Button Example

Now let’s look at a test with Enzyme and Expect:

This test isn’t validating anything useful. We know it’s a button. That’s not what we want to test. What we actually want to validate is Button’s styling.

This test would never fail if the styles were altered, and we don’t have access to those styles with Enzyme. We need more precise tooling for our validation to be meaningful. This is where snapshot tests come in handy.

A Basic Snapshot Test

We can write a simple snapshot test like this:

Here we are rendering a tree representation of our component and validating it matches a local snapshot. When we run our test, Jest automatically generates a snapshot of the rendered component and puts it inside the __tests__ directory.

If we change the Button styles, they would no longer match our snapshot and cause our tests to fail. This will help us catch any unanticipated changes to our components.

Diving Deeper

Now that we have the basics down, let’s take a look at a more advanced scenario. We’ll still use our Button component, but now it has a bit more going on.

NOTE:
Our team uses BEM concepts, (Blocks, Elements, and Modifiers) to structure our components. We’ve found that this allows us to create flexible and maintainable components. But it comes with some terminology that might be helpful to explain.

Elements are UI components that do not render any child components. An example would be a Label or Text. Sometimes these Elements are general purpose and can be used in a variety of contexts.Others belong to a specific Block context, such as Button.Text.

Blocks are UI components that render child components. Button is a good example, as it can have both Text and Icon child components within its context.

Modifiers are functions that allow us to predictably manipulate Block or Element styles. We’ll talk about these more later.

If you’d like to read more, take a look at Structuring Our Styled Components

Below, the Button Block contains two child Elements, Text and Icon, as you can see in our file structure below. Note that Icon is also a general-purpose Element (lib/elements/Icon) that we’re reusing within the Button block context.

TESTING THE BUTTON BLOCK

Let’s take a look at our Button styles:

Great! Now let’s write a snapshot test for it:

Now when we run our tests (yarn test), they should pass as expected:

Jest will generate our snapshots here:

ADDING JEST-STYLED-COMPONENT

I think the #000 text for this button is getting washed out by the grey background color. I think we should change it to #FFF instead.

Cool. Now when we run our tests again, we should see them fail.

Well, the test did fail, but the output isn’t very clear. We don’t get any helpful feedback on what style changes caused it to fail. All we can see is that the className changed. This is where jest-styled-components comes in handy. It gives us better test feedback and some useful helper functions.

$ yarn add jest-styled-components --dev

Now we need to import the lib at the top of our test file:

And when we re-run our tests, we get much better feedback about the failure. One of the biggest criticisms of snapshot tests is that the test’s intent and feedback is not clear. But jest-styled-components allows us to see a diff of the exact style changes. That makes our lives a lot easier.

NOTE:
To learn more about jest-styled-components, you read find the docs here.

We still need to update our snapshots to reflect our changes. To do that, run jest --updateSnapshot. Once your snapshots are updated, your tests should pass as expected.

Cool! If you’re following along in the example repo, you probably noticed there’s a bit more going on in Button, specifically applyStyledModifiers().

TESTING BUTTON MODIFIERS

Our team developed a library, styled-components-modifiers, to help us predictably modify component styles. And you’ll notice that Button has three modifiers, primary, secondary, and disabled. These modifiers add styles to the component with ${applyStyleModifiers()}, as you can see here:

As you’ve been running the tests, you probably noticed the Jest test coverage percentage is failing (below 90%). We should test these modifiers to bump up our coverage. Let’s do that now!

Now when we run our tests, Jest will generate more snapshots, and our coverage percentage will increase! Follow the same pattern to write some snapshot tests for Button/Icon.js and Button/Text.js. Repeating that process will help solidify those concepts and get your coverage to 💯! 🎉

Great! Now that we have some solid coverage, let’s add a theme to our Button and update our tests.

TESTING THE BUTTON’S THEME

Theming is a really powerful tool built into styled-components. It allows us to easily keep styles consistent for all our components. styled-components uses a ThemeProvider component to pass our theme to every styled component via context. This works really well in our app. We can simply call props.theme.property in our styles, and have the correct value applied. Lets update Button to use styles from our theme:

Great! And since our theme values match our previous values, the tests should just pass, right?

TypeError: Cannot read property 'chrome500' of undefined

Hmm, except everything blows up. Why? Our shallow-rendered component doesn’t know what theme is. This makes sense, as it has no access to a ThemeProvider component. You can read more about this here.

We can fix this in one of two ways. We could either pass the theme to every component as a prop in our tests:

Or we could create a tests helper function in __tests__/helpers/index.js

I prefer the latter, but either works fine. Once you have that added, let’s update our tests.

Now, when you run your tests, everything should pass as expected! Okay, you’re doing great! There’s one more aspect we should cover before we wrap up: third-party components.

TESTING THIRD-PARTY COMPONENTS

Third-party components can be a little tricky to setup and test at first, but we’ll follow a pattern that keeps it simple. For our example, we’ll use the Icon Element in lib/elements/Icon/index.js.

We currently have a placeholder that looks like this:

We’re going to replace this with FontAwesome icons by using react-fontawesome. If Icon was only extending styles and didn’t need any props, we could write it like this:

But if we want to add our own props, it would throw a console warning:

Warning: Unknown prop `modifiers` on <span> tag. Remove this prop from the element.

To avoid this, we could build our component this way:

Now that we’ve rewritten our component, we can add a test:

Awesome! Now we have a solid test suite that will help keep our component styles consistent. 🎉 If you’d like to run a final check, you can run yarn review. This will run a style-linter and the test suite.

Additional Tips

SNAPSHOTS ARE NOT TDD

Snapshots are intended for regression testing. We want them to be a fail-safe in case something breaks unexpectedly, but don’t expect them to lead your component design process or validate component behavior.

KEEP SNAPSHOTS SMALL AND FOCUSED

Snapshots provide detailed feedback, but it can be overwhelming and difficult to diagnose for large, complex components. A recent article, Structuring Our Styled Components, describes helpful patterns for keeping your components manageable.

USE A TEST:WATCH SCRIPT

Jest has built-in watch scripts for tests: jest foo/bar/** --watch. This immediate failure feedback is incredibly helpful for pinpointing the issue. It’s challenging to remember context after making large changes. Short feedback loops require less mental load.

FOCUS ON UNDERSTANDING FAILURES

One of the strongest criticisms against snapshots is unclear intent, and that’s a fair critique. This obscurity can lead to frustration and tempt you to blow away the snapshot. Resist that urge and dive in. Keep in mind that you can minimize this frustration by following the previous two tips.

TRACK TEST COVERAGE

Jest also has a built-in test coverage functionality. You can quickly set it up by adding the config to the package.json. I have a handy example here. This feedback is really valuable for ensuring all edge cases are covered.

SNAPSHOTS + CI

Integrating CI is especially valuable when building a component library. A final 👍 is really helpful for the reviewer when a PR is submitted. I use a Github integration with CircleCI for my personal projects, and it’s been very helpful (and free).

Conclusion

We love using Jest snapshots for validating our styled-components. They help us maintain consistent styling and avoid unintended side-effects. I hope you found this article helpful! If you enjoyed this, please share with others. Also, you should subscribe to my newsletter! https://tinyletter.com/alanbsmith

Thanks for reading! You can find me on Twitter and GitHub.

--

--