In this article, we will discuss strategy that I like to help keep Storybook stories up-to-date. For those that are unfamiliar, Storybook is a UI component workspace that I find helpful in building front-end applications.
It allows you to develop your components in isolation, interact with these components individually, and see quality issues at a much more granular level than the screen or page level. Additionally, this workspace serves as communication to other developers, noting the reusable components that exist in a system. Learn more about Storybook here.
One criticism I often hear is that it's easy for a Storybook project to get out-of-date when developers forget to add their components to Storybook. I often find that Storybook speeds up development but understand the tendency to build new components in the context of the live, running application. It's easy to forget about adding stories when you have an otherwise working and tested feature.
How do we keep this "building components in isolation" mindset top-of-mind in a way that provides immediate value to others may not necessarily develop their components in Storybook first? In this scenario, I often like to treat stories as part of my testing strategy.
Traditional Testing
Let's assume we are building an activity feed. On our activity feed item component, we want to ensure that we're rendering the correct text, and the button onClick event fires as expected. We could use react-testing-library to establish confidence that our component is working as anticipated.
We'll use the render
utility to render the component that we wish to test. We'll check the text and onClick functionality to ensure that everything is working.
// ActivityFeedItem.js
export default function ActivityFeedItem({ name, text, onClick }) {
return (
<Card>
<Heading>{name}</Heading>
<Text>{text}</Text>
<Button onClick={onClick}>See Details</Button>
</Card>
);
}
// ActivityFeedItem.test.js
import { render } from '@testing-library/react';
...
it("shows the correct text", () => {
const { getByText } = render(
<ActivityFeedItem
name="This is the heading!"
text="Nostrud tempor ullamco aute nostrud commodo cillum amet ad velit veniam officia minim."
/>
);
expect(
getByText(
"Nostrud tempor ullamco aute nostrud commodo cillum amet ad velit veniam officia minim."
)
).toBeInTheDocument();
});
When we run our tests, we'll see that all is working as expected.
Test Suites: 7 passed, 7 total
Tests: 9 passed, 9 total
Snapshots: 5 passed, 5 total
Time: 2.62s
Ran all test suites.
Debugging failing tests
What happens if our test is failing, and we want to dive in to debug?
Test Suites: 1 failed, 6 passed, 7 total
There are a couple of options but, one I use a lot is the debug
utility from React testing library. This utility illuminates the HTML for the rendered element.
We could update our test as follows to leverage debug
:
const { getByText, debug } = render(
<ActivityFeedItem
name="This is the heading!"
text="Sit enim irure pariatur nostrud id non deserunt laboris veniam velit."
/>
)
debug()
The debug
utility will log the HTML for our components. This strategy would work well for our trivial example component, but on a more substantial component, this can get unwieldy pretty quickly.
Instead of defining our elements to render directly in our test, we can leverage Storybook stories for this. We'll use stories written in Storybook's component story format to serve as the element we wish to render
in our test.
We'll create the story metadata first. This metadata provides information to Storybook about how we should display our stories within the utility. Next, we'll create a story with the component story format. You may notice that we're creating an arrow function, which is not unique to Storybook. We can export this arrow function and import it in our test.
// ActivityFeedItem.stories.js
export default { title: "ActivityFeedItem" }
export const standard = (callback = undefined) => {
return (
<ActivityFeedItem
name="This is the heading"
text="Nostrud tempor ullamco aute nostrud commodo cillum amet ad velit veniam officia minim."
onClick={callback}
/>
)
}
Using the story in our test
Before where we rendered
our component in the test, we'll use the imported story instead. Now, if we want to debug our test, we have a story we can use in addition to the other debugging strategies we may traditionally use.
import { standard } from "./ActivityFeedItem.stories"
it("shows the correct text", () => {
const { getByText } = render(standard())
expect(
getByText(
"Nostrud tempor ullamco aute nostrud commodo cillum amet ad velit veiam officia minim."
)
).toBeInTheDocument()
})
We now have a way to visualize and interact with the component we're testing.
Wrapping up
Storybook provides many benefits beyond testing, but sometimes it's easy to forget when we're trying to get features out the door. I've found that using Storybook as a tool to help ensure quality helps avoid this situation where stories become out-of-date or neglected.
Top comments (6)
Excellent work! I had no idea that story book existed. But now I'm aware of what could be a weakness and how to get around that with a team. Thank you for sharing! Edit- I noticed you create music in your free time, did you happen to create the outro music in your video?
Thank you - I really appreciate you checking this out!
Music is a fun hobby - I did make the intro drone and outro clip on this. :D
Nice!
I sent this article to my team lead, we are using Angular 8 right now on a project and are feeling the pangs of keeping up with components. Hopefully we can implement it in fairly quickly.
I've been meaning to get a full design pipeline in place so a designer and coder can collab at high velocity, and I always knew storybook was the answer; I need to take some time to really dig into this.
Thanks for writing this up and sharing!
Thank you for reading through this! I LOVE Storybook and really find the workflow benefits it brings are massive. I have another post on the overall philosophy here ryanlanciaux.com/blog/2019/09/20/t... if thatβs helpful. ππ
Very cool tool. Maybe worth switching to ππ