Raphael Ugwu Writer, Software Engineer and a lifelong student.

How React Storybook can simplify component testing

5 min read 1486

Introduction

Every developer wants to build software that works. We can make sure our software’s code works flawlessly by isolating it and showing its behavior in a series of tests. The same can’t be said for our components as they are tested within the context of our app.

Storybook lets you view and interact with your components in an isolated manner. It’s just like unit testing but for UI components. In the words of Storybook’s documentation:

Storybook is a user interface development environment and playground for UI components. The tool enables developers to create components independently and showcase components interactively in an isolated development environment.

In this post, we will find out how Storybook can help us create UI components and improve our component testing.

Getting started with Storybook

Let’s start with bootstrapping a new React project and in it, we’ll install Storybook via CLI:

npx create-react-app my-storybook-app
cd my-storybook-app
#install storybook
npx -p @storybook/cli sb init
yarn storybook

When running yarn storybook, you should see Storybook’s testing page on the local address http://localhost:9009/:

storybook ui

 

For the purpose of testing, we’ll create a basic component – a button called CoffeeButton. It displays the number of cups of coffee to be served.

// /src/stories/CoffeeButton.js
import React, { useState } from 'react';
const ButtonStyle = {
    backgroundColor: 'lightgrey',
    padding: '10px',
    borderRadius: '5px',
    fontSize: '15px',
    border: '#66c2ff 3px solid',
    cursor: 'pointer'
};
const CoffeeButton = ({children}) => {
    const [count, setCount] = useState(1);
    return (
        <button style = {ButtonStyle} onClick = {() => setCount(count + 1)} >
        {new Array(count).fill(children)}
        {(count < 2)
        ? <div> Please serve 1 cup of coffee </div>
        : <div> Please serve {count} cups of coffee </div>
        }
        </button>
    );
};
export default CoffeeButton;

Storybook works by using “stories”. A story is a function that contains the single state of one component and renders that component to the screen for testing purposes. Let’s write a story for our CoffeeButton component. In src/stories create a file and name it CoffeeButtonStory.js:

import React from 'react';
import { storiesOf } from '@storybook/react';
import CoffeeButton from './CoffeeButton';
storiesOf('CoffeeButton', module)
  .add('Black', () => (
    <CoffeeButton>
      <span role="img" aria-label="without-milk">
         🏿
      </span>
    </CoffeeButton>
  ))
  .add('White', () => (
    <CoffeeButton>
      <span role="img" aria-label="with-milk">
        🏼
      </span>
    </CoffeeButton>
));

Here’s how our component looks in Storybook:

 

UI testing

Storybook offers different techniques for testing UI components. Components need to undergo tests for a variety of reasons some of which are:

  • Detection of bugs
  • Tests can be documented to serve as guidelines for other developers who will work on the project
  • To prevent stuff from breaking during new commits

Let’s go on to examine some of the ways Storybook can make component testing seamless.

Structural testing

Structural testing involves the testing of a component based on the knowledge of its internal implementation. Storybook implements structural testing through storyshots – an add-on that works by comparing snapshots of code. To install storyshots run:

npm i -D @storybook/addon-storyshots react-test-renderer

react-test-renderer renders React components to pure JavaScript objects without depending on the DOM. This makes it possible to grab the screenshot of the DOM tree rendered by a React DOM.

After installing, let’s create a test file storyshots.test.js, in it we’ll initialize storyshots:

// src/storyshots.test.js
import initStoryshots from '@storybook/addon-storyshots';  
initStoryshots({ /* configuration options */ });

To test a component we run npm test. What this does is generate a snapshot where you can inspect the component’s output. Every time you run a test, a snapshot is automatically generated and compared with snapshots generated from previous tests. If storyshots spots any differences, the test will fail. Below is a snapshot generated on testing for the first time:

passed test

Our tests were successful, now let’s try to change something in our CoffeeButton component. Change line 16 of CoffeeButton.js to:

? <div> Please DO NOT serve 1 cup of coffee </div>

On running tests we get the following errors:

failed test

A more detailed view:

Automated visual testing

Automated visual testing involves automatically verifying that our UI visually appears as intended. This is useful in cross-browser testing as it can detect lapses that escaped the observations of developers. Storybook tests UI visually via an add-on called storyshot-puppeteer . Same as storyshots, this add-on works by comparing screenshots – only this time it takes screenshots of the browser and not code. To install storyshot-puppeteer run:

npm i -D @storybook/addon-storyshots-puppeteer

Once installed, to make it compare UI and not code we’ll need to override the test comparison with imageSnapshot from the puppeteer add-on. We can do this by editing the initStoryshots function we created when carrying our structural testing. We’ll also need to specify the URL where our storybook will be running:

// src/storyshots.test.js
import initStoryshots from '@storybook/addon-storyshots';
import {imageSnapshot} from '@storybook/addon-storyshots-puppeteer';
initStoryshots({
    test: imageSnapshot({storybookUrl: 'http://localhost:9009/'}),
});

Below is a snapshot generated when we test our images for the first time:

Should we change any UI property in our component, our tests will fail and puppeteer will return the difference in the form of snapshots. Let’s change a part of our UI. In line 3 of CoffeeButton.js, change the background color from lightgrey to lightblue:

backgroundColor: 'lightblue',

Now when we run the tests:

Below is a generated snapshot of the difference noticed by puppeteer in our UI:

In the diffing above, the original image is on the left, the modified image is on the right and the difference between both of them is in the middle.

Interaction testing

With Interaction testing, Storybook lets you display tests and their results alongside your stories in the DOM. It does this via an addon – react-storybook-specifications. To install this addon, run:

npm install -D storybook-addon-specifications

Then add this line to your addons.js file:

import 'storybook-addon-specifications/register';

react-storybook-specifications does not work alone, we still need to install the following:

enzyme: JavaScript’s testing utility for React.
enzyme-adapter-react-16: Enzyme’s adapter corresponding to the version of React you’re using.
expect: Jest’s inbuilt method used for checking that values meet certain conditions when writing tests.

To install these add-ons, run:

npm install -D enzyme expect enzyme-adapter-react-16

In our config.js file, we’ll import configure and Adapter from enzyme and enzyme-adapter-react-16. Notice that we’ll be having two instances of configure now so we’ll need to specify both like this:

import { configure as configure1 } from '@storybook/react';
import {configure as configure2} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
function loadStories() {
  require('../src/stories');
}
configure1(loadStories, module);
configure2({ adapter: new Adapter() });

Let’s see how this works by testing part of CoffeeButton component. In CoffeeButtonStory.js, input the following block of code:

import React from 'react';
    import { storiesOf } from '@storybook/react';
    import { action } from '@storybook/addon-actions';
    import { specs, describe, it } from 'storybook-addon-specifications';
    import {mount} from "enzyme";
    import expect from "expect";
    import CoffeeButton from './CoffeeButton';
    const stories = storiesOf('CoffeeButton', module)
    stories
    .add('Black', () => {
        const story =
        <CoffeeButton onMouseOver={action('click')}>
        <span role="img" aria-label="without-milk">
        🏿
        </span>
      </CoffeeButton>;
      specs(() => describe('Black', () => {
         it('Should have the following text: 🏿Please serve 1 cup of coffee', () => {
             let output = mount(story);
             expect(output.text()).toContain('🏿Please serve 1 cup of coffee');
         }); 
      }));
      return story;
    })

Now save and run the app. In our browser, we should see this:

Let’s alter our tests expectations. Change line 20 of CoffeeButtonStory.js to:

expect(output.text()).toContain('🏿Please serve a cup of coffee');

Now when we run the app and check out our browser, this is what we get:

As can be seen, changing our expected output will render an error to the DOM. Interaction testing via storybook-addon-specifications enables us to have a living documentation, where we can interact with our components and their test results side by side.

Conclusion

Storybook provides a great way to test our UI components. It may seem like we are doing away with unit testing but that’s not the case. Unit testing aims at finding out what went wrong with code. In this case, we’re testing React components and if something went wrong with our UI we would still ask questions about what markup we have to fix.

This implies that integration and snapshot tests are as good as unit tests – in this context. Should you want to play around with code, you can always check out the source code here on Github.

 

Plug: , a DVR for web apps

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Raphael Ugwu Writer, Software Engineer and a lifelong student.

Leave a Reply