Story driven development

You may have heard of the concept "test driven development". It's a practice that, in theory, will produce high quality, testable, and reliable code. But it is not always easy to adopt this mindset, especially when writing frontend code. However, if we take a UI development tool like Storybook and treat the stories as tests, then suddenly this widely hailed practice suddenly becomes more accessible to us.

A quick recap of TDD

Test driven development is the idea that you write your tests first and that those tests describe the behavior you want from your code. This allows you to think about the usage of the code and to cover all different scenarios with tests, focusing on the desired functionality rather that being distracted by the actual implementation. Since you haven't written the code yet all the tests will fail at first. Now you write your code, and one by one the tests should start passing, and when all tests pass you can be sure that you have written code that takes into account all the different scenarios you tested for.

Check out the Wikipedia article on TDD or do an Internet search for millions of more resources on the subject if you want to know more.

TDD and frontend

Utilizing the TDD pattern is not always so easy, especially when it comes to frontend development. Consider what we try to achieve with our frontend code: a specific look, a specific feel, a specific interaction pattern. It's not just hard data.

But we do have tools to test (or at least manually evaluate) these aspects of our websites and applications. This article will focus on the tool Storybook, where we can display parts of our interface (components) on isolated pages and make sure that they look and feel the way we want before we use them in a larger context. We can then use these stories, or tests, which is what they actually are, to do something similar to TDD by writing the stories first and the code second, hence the concept "Story Driven Development".

Treating stories as sets of data

The first thing we have to do is take a look at how we approach stories when using Storybook. A story is really two things: a component and a set of arguments (args). Together this results in a unique render in the browser.

But what are args? Well, they're just data, aren't they? Properties on a JavaScript object. Strings and numbers. And so they can be treated just like any other data in any other JavaScript program. The data can be exported, reused, destructured, and manipulated in any way. This is what allows us to compose args, a powerful pattern that can greatly simplify your Storybook code.

Having this mindset is essential when it comes to treating stories as tests, because if we write each story individually we will never be able to get good test coverage in an app of any size other than tiny, without massive amounts of work.

Treating stories as tests

So then, when it's time to write a component, what steps should you take, and in what order?

Step 1: Put yourself in the mind of the user and figure out the API of your component.

The user can be another person, or it can be future you. Either way, take into account the requirements for the functionality and try to imagine the nicest way to interact with the component. Ask yourself "what data would I want to send to this component if someone else had developed it?". Exactly how you do good API design depends a lot on the context and is a topic for another article, or possibly an entire book series.

Example:

I want a button and I want it to have a default color, and I want to be able to send it a custom color via a color prop. I also want to be able to disable the button, at which point it should always be gray. Finally, the button should have an id.

Step 2: Write an empty component

This is equivalent to the TDD step of setting up an empty/non functional piece of code that we can run our tests against and see that they fail. Set up an empty component that just renders an empty div or something, but have it accept all the props that you defined in the API. This prepares us for the next step.

This component is written using React, but you can use any framework/technology.

// button.jsx

import React from "react";

export function Button({ id, color, disabled }) {
return <button></button>;
}

Step 3: Write stories for all permutations of the API

That's right, one story per variation. This is where args composition can come in real handy since the number of stories increase exponentially with each property. The idea is to have a complete set of all the possible visual variations that the component can be in. This is equivalent to the TDD step of writing all the tests and then see them fail.

I should clarify: you don't have to take into account props that doesn't affect the visuals. Like an id or a hidden label. We're after the visual variations only.

// Button.stories.jsx

import React from "react";
import { Button } from "./Button";

export default {
title: "Components/Button",
component: Button,
};

const Template = (args) => <Button {...args}>Click me</Button>;

export const Default = Template.bind({});
Default.args = {};

export const Disabled = Template.bind({});
Disabled.args = { disabled: true };

export const CustomColor = Template.bind({});
CustomColor.args = { color: "hotpink" };

export const CustomColorDisabled = Template.bind({});
CustomColorDisabled.args = { disabled: true };

All stories will now display a tiny, empty button. Nothing looks like it should - all tests fail, just what we want!

An instance of Storybook running locally. Screenshot.

Write the component.

Now you are ready to write the component. Step by step you should then be able to see each story display the component as it should look in that state. This is the equivalent of the TDD step of seeing your tests pass one by one.

Be sure to look over all the stories and when they are all good, that means all your visual tests have passed, and the component is complete!

// button.jsx

import React from "react";

export function Button({ id, color = "tomato", disabled, children }) {
const style = {
backgroundColor: color,
padding: "1rem",
borderRadius: "1rem",
border: 0,
};

return (
<button id={id} style={style}>
{children}
</button>
);
}

An instance of Storybook running locally. Screenshot. An instance of Storybook running locally. Screenshot.

Now the color works, but not yet the disabled state...

// button.jsx

import React from "react";

export function Button({ id, color = "tomato", disabled, children }) {
const style = {
backgroundColor: disabled ? "lightgray" : color,
padding: "1rem",
borderRadius: "1rem",
border: 0,
};

return (
<button id={id} disabled={disabled} style={style}>
{children}
</button>
);
}

An instance of Storybook running locally. Screenshot. An instance of Storybook running locally. Screenshot.

Now all stories checks out and the component is complete!

Writing story driven code

The benefits of this method of story driven development are several.

Conclusion

Treating Storybooks stories as tests brings TDD to the frontend in a way that was previously hard or complicated. And since it's really just a matter of mindset, story driven development is something that anyone who already uses Storybook can adopt in a matter of minutes.

By the way...

Storybook just released the beta version of their interactive stories. This means that you can test behavior, as well as looks. You Story driven development just got even more powerful!