Confident React App – Part 4

Welcome to the continuation of the Confident React App blog post series. We’ve covered some considerable ground so take your time reading the series (part 1 | part 2 | part 3).

In this post, we’ll add a tool to our arsenal for testing components in the browser without much hassle: Storybook.

Telling component stories

We already used some important tools and techniques to our code base to increase our confidence:

  • Automated tests
  • Static type checking
  • Behavior Driven Development

But can you spot what’s missing so far? Well, it’s the application running in a browser, just like the end user will. A dummy app for testing the components would be fairly easy to create but there a few downsides for this approach:

  • The real app depends on external APIs/resources so it’s slower.
  • It could be impossible to test some features due to the unavailability of the external APIs/resources.
  • The app code is more complex since it integrates many parts which makes it hard to verify components in isolation.
  • Apps can have many complex states which makes it hard to verify all use cases, especially error states.
  • It’s harder to iterate fast in the UI when there are too many dependencies.

A common way to avoid the pitfalls above is to create a separate app for the purpose of showcasing components and app states in a more structured and isolated way. Enter Storybook: the tool of choice for the question at hand. Storybook is quite powerful and versatile so I recommend reading through its examples and documentation. Let’s add it to our project:

npx -p @storybook/cli sb init

yarn flow-typed install @storybook/[email protected] @storybook/[email protected]

If everything goes well after the long install, there will be a bunch of new files in our project. Storybook is also already running with some demo content.

Storybook uses an original language for its API: you are supposed to showcase the features of your application in stories by module. For example, we want to showcase the <ListGroup> component so a story is added for it with “chapters” that shows all it can do.

Edit the file src/stories/index.js and add the following code:

// @flow

import React from 'react';
import { storiesOf } from '@storybook/react';
import { ListGroup, ListGroupItem } from '../components/listGroup';

storiesOf('ListGroup', module)
.add('with items', () => (
  <ListGroup>
    <ListGroupItem>item 1</ListGroupItem>
    <ListGroupItem>item 2</ListGroupItem>
  </ListGroup>
));

Now, if you go to http://localhost:9009/?path=/story/listgroup–with-items, you can actually see and try for the first time the component!

… And it sucks. Even though the generated HTML is perfectly valid, it doesn’t look at all to the Bootstrap version:

This is exactly why we always need to test the application, or parts of it, in the browser; even with all the tools and techniques added so far, we’re still very far from the desired outcome.

You gotta have style

We’ve been talking a lot about Bootstrap and even implemented some code that relies on it, but we never added it as a dependency to our project.

There are many ways of fulfilling this step. For simplicity, we’re adding Bootstrap as a runtime dependency so only its production CSS is loaded by the page. To do so, we have to add custom tags to Storybook. Create the file .storybook/preview-head.html and add this:

<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

Bootstrap’s CSS will be loaded from a CDN inside of Storybook. Just restart Storybook and profit!

Enriching the leaves

OK, we have a very simple list of items but there are still two important functionalities missing:

  1. Active list items.
  2. Clickable items so that they are interactive.

Let’s jump straight into the red test:

// src/components/listGroup.spec.js

it('renders active when active prop is truthy', () => {
  const wrapper = shallow(<ListGroupItem active>item 1</ListGroupItem>);

  expect(wrapper)
    .toContainExactlyOneMatchingElement('li.list-group-item.active');
});

And implement it to get a green test:

// src/components/listGroup.js

type ListGroupItemProps = {
  children: string,
  active?: boolean
};

export function ListGroupItem (props: ListGroupItemProps) {
  const classes = "list-group-item" + (props.active ? " active" : "")
  return <li className={classes}>{props.children}</li>;
}

Finally, a new story needs to be added so we can see the final result:

// src/stories/index.js

.add('with active items', () => (
  <ListGroup>
    <ListGroupItem active>item 1</ListGroupItem>
    <ListGroupItem active>item 2</ListGroupItem>
  </ListGroup>
));

For the clicking, the specification defines two options: either an <a> or <button>. Since we don’t need page navigation yet, the <button> is the right choice. Also, whenever an item is clicked, it must execute the given callback if any. The red tests:

// src/components/listGroup.spec.js

describe('when onClick prop is defined', () => {
  it('renders as an action button', () => {
    const wrapper = shallow(
      <ListGroupItem onClick={() => {}}>
        item 1
      </ListGroupItem>
    );

    expect(wrapper).toContainExactlyOneMatchingElement(
     'button[type="button"].list-group-item.list-group-item-action'
    );
  });
  
  it('renders as an active action button when active prop is truthy', () => {
    const wrapper = shallow(
      <ListGroupItem active onClick={() => {}}>
        item 1
      </ListGroupItem>
    );
 
    expect(wrapper).toContainExactlyOneMatchingElement(
      'button[type="button"].list-group-item.list-group-item-action.active'
    );
  });

  it('calls the given callback when clicked', () => {
    const onClickSpy = jest.fn();
    const wrapper = shallow(
      <ListGroupItem onClick={onClickSpy}>item 1</ListGroupItem>
    );

    wrapper.simulate("click");

    expect(onClickSpy).toHaveBeenCalled();
  });
});

To make the tests pass/green:

// src/components/listGroup.js
type ListGroupItemProps = {
  children: string,
  active?: boolean,
  onClick?: Function
};

export function ListGroupItem (props: ListGroupItemProps) {
  let classes = "list-group-item" + (props.active ? " active" : "");

  if(props.onClick) {
    classes += " list-group-item-action";
    return (
      <button type="button" className={classes} onClick={props.onClick}>  
        {props.children}
      </button>
    );
  } else {
    return <li className={classes}>{props.children}</li>;
  }
}

The last step is to verify it in the browser with a story:

// src/stories/index.js

import { action } from '@storybook/addon-actions';

.add('with actionable items', () => (
  <ListGroup>
    <ListGroupItem active onClick={action("Item 1 clicked!")}>
      action item 1
    </ListGroupItem>
    <ListGroupItem onClick={action("Item 2 clicked!")}>
      action item 2
    </ListGroupItem>
  </ListGroup>
));

There you go: our React implementation of the Bootstrap’s list group component is finally complete!

But what do we do with it? The answer lies in the app’s requirements: selecting an item when it’s clicked, multiple or single selections, deselecting…

In the next post, we’ll implement the behavior with some state and do a deeper discussion on how to test components in a way that makes us confident.

Cheers!

Flattr this!

Confident React App – Part 3

This is the third post on the series about Confident React App where I show how I build up the confidence in the source code of my React apps. Here’s the beginning of the story.

In this post we’ll finally start coding some React components but following some vital rules that will help us make reliable code.

Coding confidently with BDD + TDD

Research has shown that Test Driven Development is one of the most effective measures for increasing the perceived quality of the developed software: from ~40% to ~80% increase. They also measured the increase in effort/cost for applying it.

We can’t skip such an effective technique, right? But how do we do it? Well, there’s the TDD bible to study and also
“The Three Laws of TDD” to follow:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

That’s much easier said than done. After months of practice, it still takes me a while to get into the TDD flow so I need to be very aware to not break the rules. Because of the accrued learning curve and effort, it’s easy to make mistakes and misuse it which causes some resentment. The most common issue I observe is test code that is too coupled with implementation details, especially in React apps.

I suspect that this happens because of the mindset the developer is in when doing tests first: you get framed on testing everything! However, 100% test coverage is counterproductive since not every part of your code is equally worth the test hassle and it’s also an illusion.

The Behavior Driven Development approach is my favorite because it sets the best mindset: it builds on top of TDD focusing on business/user goals and drive towards shared understanding through conversations. In other words, it’s a high-level spec that fosters communication. When we read the test cases out loud, it feels like normal speech so it fits snugly into our minds. Good software is software that fits in our heads, which is easier and faster to create good mental models about it.

Our acceptance criteria are already written in BDD style, so the devs – we – have a perfect guide to start with.

Starting from the bottom

For this particular case, I think starting from the bottom/leave components will be easier since it’s basically the list-group component from Bootstrap. We won’t need to implement all the described features – but make sure to get familiar with it.

We quickly realize that list-group is composed of multiple list-group-item‘s. A list of items! So the leaf component should be <ListGroupItem>. Since the two are closely related, let’s keep their implementation in the same file.

So we create the two files and fire up the tests:

mkdir src/components && touch src/components/listGroup.js && touch src/components/listGroup.spec.js

Don’t forget to add // @flow.

I’ve picked Airbnb’s Enzyme test renderer because it has the necessary functionality for writing the kind of tests I prefer the most. Notably, Enzyme’s got a few pitfalls to be avoided. I’ll explain why I still use shallow soon enough.

To add the tools we need:

yarn add -D enzyme enzyme-adapter-react-16 jest-enzyme && yarn flow-typed install [email protected]

And add the new file src/setupTests.js:

// @flow
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'jest-enzyme';

configure({ adapter: new Adapter() });

A Component’s purpose

We start by describing what we’re testing: the new <ListGroupItem /> component. Its sole purpose is to return valid JSX, compliant with the component contract. The HTML + CSS defined by our external dependency, Bootstrap, is the contract that our React component must fulfill. It should be fairly simple to test because our component is just a pure function.

For now, Enzyme’s shallow is the right tool for the job.

// src/components/listGroup.spec.js
// @flow
import React from 'react';
import { shallow } from 'enzyme';
import ListGroupItem from './ListGroup';

describe('<ListGroupItem />', () => {
  it('renders according to specification', () => {
    shallow(<ListGroupItem />)
  });
});

When we run the test watcher…

yarn test

… we get our first expected red of the cycle! This is obvious since there is no implementation yet. Keeping in mind the first rule of TDD, we implement our component to make this test pass:

// src/components/listGroup.js
// @flow
import React from 'react';

function ListGroupItem () {
  return null
}

export ListGroupItem;

Now we’re in the first expected green! Doesn’t that feel good? Let’s restart the cycle and get into red again by testing something more useful. The next step is to validate the generated structure. Red:

// src/components/listGroup.spec.js
it('renders according to specification', () => {
  expect(
    shallow(<ListGroupItem />)
  ).toContainExactlyOneMatchingElement('li.list-group-item');
});

The test code above takes advantage of the cool matchers from jest-enzyme to make our code more readable. To make it green, the implementation must return the proper markup:

// src/components/listGroup.js
function ListGroupItem () {
  return <li className='list-group-item' />
}

The specification determines that every list item can have a string or an anchor as children. Let’s start with the string and assume it’s required. Red:

// src/components/listGroup.spec.js
describe('<ListGroupItem />', () => {
  it('renders according to specification', () => {
    const wrapper = shallow(<ListGroupItem>item 1</ListGroupItem>);

    expect(wrapper).toContainExactlyOneMatchingElement('li.list-group-item');
    expect(wrapper).toHaveText('item 1');
  });
});

The implementation is more interesting now: in order to guarantee that the component is used correctly, we have to validate the given props. Flow comes in handy for this task. Green:

type ListGroupItemProps = {
  children: string,
};

function ListGroupItem (props: ListGroupItemProps) {
  return <li className="list-group-item">{props.children}</li>;
}

Note that the Jest watcher does not run Flow on file changes so type errors won’t break the tests. You can use flow-watch so Flow runs in every file change. Or, if you’re using an IDE, adding a Flow extension will come in handy.

This is enough features for this component for now. Let’s move on to the next component.

Red, Green, Red, Green, …

<ListGroup> is the parent component that has many <ListGroupItem>s. Just to save some time, here’s the whole component red tests:

describe('<ListGroup />', () => {
  it('renders according to specification', () => {
    const wrapper = shallow(
      <ListGroup>
        <ListGroupItem>item 1</ListGroupItem>
        <ListGroupItem>item 2</ListGroupItem>
      </ListGroup>
    );

    expect(wrapper).toContainExactlyOneMatchingElement('ul.list-group');
    expect(wrapper.find(ListGroupItem).length).toEqual(2);
  });
});

Now we have to put both components working together and since an empty list item didn’t make sense, a list group without list items doesn’t either. Green:

// src/components/listGroup.js
type ListGroupProps = {
  children: React.ChildrenArray<React.Element<typeof ListGroupItem>>
};

export function ListGroup (props: ListGroupProps) {
  return <ul className='list-group'>{props.children}</ul>
}

In the shallows, shallows…

shallow rendering is a powerful tool and having its documentation close by is quite handy. Never using it is a missed opportunity. Facebook has some more insights about it. Here are some reasons why I believe it’s the right tool for the component tests above:

  1. The components are just pure functions, no interactions with real DOM or higher-order components.
  2. Unit tests should be fast and shallow is faster than mount and render.
  3. Unit tests should be isolated. For <ListGroupItem> it’s easy because it’s completely independent but <ListGroup> depends on <ListGroupItem> as defined by the children argument type. Using mount/render would break the isolation of the component test because it renders the children.

Facebook’s react-dom/test-utils and react-test-renderer are testing libraries used by Enzyme. Which means that all the render stack from our test environment is mocked. The final result can be fully validated only when the code runs in a web browser. We’ll add another tool for validating components on the browser in another post.

Phew! What a ride. Thanks for reading all the way here. I really appreciate it. The series is not over though: the feature is far from done and the “leave components” are not completed yet.

Stay tuned for the next post in the series.
Cheers!

P.S.: All together now! In the shallow, shallow…

Flattr this!