Testing React Components using Storybook and Cypress
I’m a big fan of Storybook and I’ve written in the past about how to combine Storybook, Jest and TypeScript to test React components with “Storyshots”.
I’ve got a couple of component libraries that, in turn, extensively leverage different visualization libraries. However, many of the visualization libraries I’m leveraging (either directly or via React wrappers around them) are framework agnostic and build visuals via direct manipulation of the DOM.
In trying to test these, I’ve had all kinds of problems because while you can wrap them as a React component and implement the various lifecycle hooks, when you try to test them, you aren’t really testing them against a real DOM if you are using something like Jest. For this reason, I wanted to build tests that leverage real DOM. In looking around, I heard lots of good things about Cypress so I thought I’d give it a try.
The first issue I ran into was how to really construct the tests that I wanted. Generally speaking, I’m dealing with libraries of components here. Cypress is largely an integration testing tool built for testing a single app. I don’t have a single app in these cases, I have lots of components.
Storybook is a wonderful way to interactively “play” with components. But if you think about it, Storybook is an app too. So, I thought, could I combine Cypress with Storybook as a way to do “integration testing” on my Storybook stories. I Googled around, but really didn’t find any discussion of this topic so I’m writing this to document my experience.
What I found, with this experiment, is that you really can treat Storybook as an app to perform “integration testing” on. Just launch Storybook like you normally would (e.g.,
start-storybook -p 6006) and then write your Cypress tests. The main steps, which I’ll demonstrate below, are visiting your app, finding the story you are interested in, getting the iframe associated with your components and then writing your assertions.
I’ve done this for my own stories, but showing tests in the context of my stories probably isn’t that useful. So I’m including a Cypress script that tests a public site so you can really understand what it is doing. The following script tests the
react-dates components hosted at http://airbnb.io/react-dates.
I’ve added comments so you can understand what is going on. As I said, the first step is to visit the site. We do this in the
beforeEach function associated with the
describe of our whole test suite. I then chose to use a
context to describe each collection of stories. In all but the default case, you need to also add an additional
beforeEach to navigate to the story collection in the UI (and note, as indicated in the script, this is done differently for different versions of Storybook). I use each
it to test a particular component. First we need to select the story for that component and then we grab the
iframe associated with that story and continue testing in the context of that
iframe. Note that in Chrome you can get pretty good selectors for most elements by just inspecting the element, clicking on the element in the DOM and doing
Copy > Copy selector. But, as shown above, using an attribute selector works quite well with Storybook since it actually injects labels as attributes in many cases (N.B., Chrome won’t leverage attribute selectors, so you have to build these yourself).
That is pretty much it. What I miss is the ability to do snapshot testing of the DOM for the components. That would be really nice for detecting UI regressions. I was wondering whether Cypress had plans to add that, but when asked about this their response was that since Cypress is mainly for end-to-end testing, they didn’t think it was that useful. I can’t say I blame them, but I’m not sure they considered a (crazy?) use case like this.
There is one last “wrinkle” here. Normally when doing testing I run Storybook at a shell prompt and just let it run. I then run Cypress (i.e.,
cypress open) and keep Cypress running so it reruns my tests (against the running Storybook) each time I update my tests.
But in order to automate this process for continuous integration, we need to do a few simple additional things. The setup I just described is mainly for interactive stuff. But what I want is to run a script that finishes with an exit status. To do that, I need to have a setup that starts Storybook, then runs Cypress non-interactively (i.e.,
cypress run) and then shuts everything down once I get an exit status from Cypress.
I was able to achieve all of this pretty easily using
package.json file looks like this:
"test": "jest && concurrently 'npm run storybook:run' 'npm run cypress:test' -k -s first",
"storybook:run": "start-storybook -p 6006",
"cypress:test": "wait-on http://localhost:6006 && cypress run",
"cypress:run": "cypress run",
"cypress:open": "cypress open"
Two things to note here. First, the
wait-on command is there to give Storybook time to get up and running. Since it is running
webpack behind the scenes, this can take a while. Also, note that I included
jest at the start of my
test script. That is because I may very well have additional tests that do not require a browser to perform. In this way, it will perform all the
mocha) tests first and then run Cypress if no failures were found.
It is also worth noting that Cypress produces a video (like the one above) of the tests as they are run. I was using CircleCI and I was able to easily configure CircleCI to store the video as an artifact of the build. This allows me to go back and view the video if there are errors. I can imagine that when there are failures this could be quite handy.
The basic idea here is to use Storybook to package up a component gallery into a single application and then use Cypress’ integration testing capability to recast unit/functional testing as a bunch of integration tests. It is, admittedly, a rather odd use case. But it has the advantage that if you are already using Storybook, you can leverage it to do all the packaging work you need in order to jump right in and use Cypress to interact with your components with a real DOM.
Honestly, I’m much happier writing tests with
jest that do snapshots of (shallow) rendering. But I’ve run into a few use cases where that doesn’t work very well because you need the real DOM which is what led me here. I still think that writing these kinds of browser/DOM based integration tests is a bit tedious, but Cypress and Storybook at least made it pretty easy to get things setup.
I’m writing this mainly as a reference for myself, but hopefully other people will find this useful as well.