How to Test a Full React App Using Nothing But Storybook
- Redefining Integration Tests
- Storyshots for Structural Tests
- Storyshots and Puppeteer for Visual Tests
- Storyshots Cartesian for 10x Testing Impact
- Interaction Tests: The Lone Survivor
- Conclusion
In my previous article about how to test everything in your Redux app with Jest Iâve discussed how you can use snapshot testing to test Redux actions, reducers, views and side effects.
Weâll divide this subject into a two-part series:
- Part 1 (youâre reading it!): Testing theory, what changed in the world, and how to use storybook to cover your bases with Storyshots, Puppeteer, and Storybook-Cartesian
- Part 2 (follow this publication to be notified when its out!): Applying the same practices to multi-browser testing with Storybook-Selenium.In this article weâll focus on the React side of things, and showcase part of the testing approach at HiredScore.
Hereâs a question: if you had the constraint of picking just one of the common test layers you usually use with every feature, which one would you choose that would give you the most ROI if it is the only one that exists (return on investment)?
- Unit tests, with a good coverage
- Integration tests, visual regression
- End-to-end tests, cross-browser visual regression
What if I told you that itâs possible to write no test at all and get (3), (2) and most of (1)?
In fact, youâll be writing a Storybook story, not a test. And if you already are in the very healthy habit of writing Storybook stories for your development and design handoff workflowâââthen youâd be writing no tests at all.
This means, youâll have much more time on your hands to do proper TDD for domain logic!### The Testing Pyramid
The testing pyramid was a concept that got a limelight during the gold-rush of testing; years 2006 to 2009 gave birth to BDD (Behavior Driven Development), cucumber, modern and humane automated testing, and sparked the culture of testing that was getting a healthy boost from the Ruby community, which kept looking at software as craft and quality as a first-class citizen.
One example was Ruby on Rails and its massive opinionated push towards testing and test strategy and tooling right out of the box. Pretty much shaping everything we know about good testing today.
At the base of the pyramid we have our unit tests; small, quick, isolated tests that can run en-masse. As we go up, the complexity and run time of tests goes up as well, and so integration tests and then end-to-end tests come next, and then manual tests or the so called âeyeball testsâ come last.
Whatâs important here is that as you go upâââyou should ideally write less tests of that certain level you went up to.
With that metaphor comes its arch-enemyâââthe Ice Cream Cone:
In this model, you have a massive amount of manual tests and close to zero unit tests. The worst kind of situation to be in for agility and reliability.### Introducing: The Testing Diamond
Having React coupled with Storybook and Storyshots, unlocks a different model: the Diamond model.
The diamond model for your UI/App means: little to zero unit tests, massive amount of integration tests, and zero manual tests.
What changed? Integration tests were avoided in the early days because they had a reputation of running slowly; grantedâââwith most technologies this is still very true.
With Jest, React, and Storybook/Storyshots, this is (arguably) no longer the case. No longer must you bring up a browser for each test that leaves its traces in your test environment, or have flaky test suites run and fail randomly, using a not-so-smart test runner that forces you to run everything exactly when you didnât want to. Itâs an era where frontend tooling really does work, and hard becomes easy.
Redefining Integration Tests
Before we see how Storybook and friends checks all of our testing boxes, it makes sense to align our testing terminology in the context of React. A unit test may mean many different things to different people or different teams, and even more so integration tests may mean different things as well.
For example, I view a unit tests as something that tests a module in isolationâââand I donât really care about if that module is a class, a function or a set of highly cohesive classes that sit together in a module. Others might argue differently, which is fine.
I also view an integration test as anything that connects one or a number of such modules with the external world for a wide definition of âexternal worldâ. For example, rendering a single React component naively is a unit test in my book. But rendering that same component with various dependencies such as CSS, fonts, wrapper component, theme/styling support and so on (still just showing that single component) is an integration testâââeven if we donât combine multiple components with one another, we are combining a single component with some infrastructure facilitiesâââwhich are external to it.
Letâs line up our solutions and keep track of this table as we go:
### Storybook Driven Development
In a modern React workflow, you have Storybook integrated and driving your work. Which means you build a component storybook-first, in a storybook-driven-development fashion.
React components are largely a functional beast. They have inputs and outputs and no side effects. What this means is that for a process that takes an input and produces an outputâââthe effective way to test would be to set up a test harness; something that takes a module (test subject), automatically provides input, runs the module and automatically verifies the output.
Jest snapshots is such a test harness. Jest snapshots in the context of Storybook are what Storyshots is.
Storyshots for Structural Tests
If you already write stories for every component, you already are writing tests, and you just donât know it yet. Given the thesis above, each of your stories can automatically become a tests:
- Input is your story
- Processing is simply rendering a story (which storybook already does)
- Output is a generated snapshot
And this is what Storyshots does. Storyshots will verify that a React component renders correctly; and if you build multiple stories with a number of different properties then Storyshots can snapshot those as well, and those would be verified on every test run.
Storyshots and Puppeteer for Visual Tests
With Storyshots, weâve covered the DOM. However, in some cases snapshots are not enough; these cases are often browser drivenâââan example would be an unexpected cascading (CSS) set of rules that step on each other, leading to a different rendering from what we expected.
Rendering a story to pixels is beneficial, especially when it too comes for free. With Storyshots Puppeteer this easily becomes the case.
Storyshots Cartesian for 10x Testing Impact
Now that we have three layers of defense ready to snapshot and use stories from your storybook set up, why not give it more stories? For example, looking at this component:
<Button disabled={false}
highlighted={false}>
Click Me
</Button>
Reveals that we have a few creative ways to verify that this button is correct:
- A button thatâs disabled should look like itâs disabled
- Same for when itâs highlighted
- A button that has an empty text, should be still visible and look like a button (i.e. will not squash down to be one pixel wide), there should be some minimal width for it
And each of these can be a story. This works great because the moment we add a story, we have three snapshot tools that go off:
- âRegularâ snapshotsâââtake care of snapping the DOM
- Puppeteer snapshotsâââmakes sure a browser renders correctly, visual regression
- Multibrowser snapshotsâââvisual regression on old browsers, old IE, a combination of Mobile+Browser variants that your customers use frequently, and so on.
But, what about a combination of the stories? what about a button thatâs highlighted and disabled?
For this, we employ Storybook Cartesian. With it, we can state the various single-property options for a prop, and it will generate all of the required combinations as fully functional stories; and so, these too will be immediately picked up by our multiple testing stages.
Which creates:
And the resulting table gets a little bit extra for everything!
Interaction Tests: The Lone Survivor
By using our existing stories and these techniques weâve got a layers on top of layers of defense. What none of those cover is interaction tests and stateful tests, which you can now do sparingly and treat as an edge case and not the norm.
Conclusion
What do we lose by doing a little amount of unit tests but a great amount of integration test?
Well, on âpaperâ we lose the ability to pinpoint failing code very quickly. A unit test effectively splits our code to small units (when done right), and so when a test fails we should get a good intuition for what unit failed and what line of code was it that triggered the failure.
We donât get that with integration tests, but if we think about React components, it doesnât matter. In any case these are mostly rendering tests so even if we had such unit tests we would still ask a question that relates to rendering of a componentââââwhat CSS did we get wrong?â or âwhat markup should we fix?â. In that senseâââintegration tests and snapshot tests are as effective in fixing a failing test as a unit test.
That said, you should definitely keep your âclassicâ unit tests for logic, library and domain model code. All these things you put in /lib
, external packages that deal with your domain model and so on.
Using Flow or Typesript is another good thing you can do when doing this kind of testing. Null safety, and type errors kind of test failures would not even get a chance to become a failing test and so increases the effectiveness of a healthy integration layer.
As with anything, donât apply these principles blindly to every situationâââalthough it should work quite well in the context described (React components, rendering, views, browsers).