Building NomadHair: A Journey in Component Driven Development Part III— UI Tests and Automating Testing Workflow

Luke Nam
9 min readJul 8, 2024

--

Photo by Hans Reiners

In the previous chapter, I have covered how I built reusable components, making them more customizable so that they can be composed into larger pieces of UIs.

Despite such flexibility and extensibility, maintaining those components is definitely a tricky task. Imagine having to test UIs for each browsers, device types, or accessibility standards every time you update them. It would be unrealistic!

Image from Storybook.js.org

Solving these issues was also another major challenge of this project.

Fortunately, Storybook provided ways to streamline various UI testing into a more productive workflow. It supports plugins for some of the well-known testing tools, such as Testing Library, Jest, Cypress, and Axe core.

In this final chapter of the series, I would like to discuss a few different types of tests I used to test my UI components, as well as how I have used Github Actions to integrate these into a more efficient workflow.

Table of Content

Accessibility test

Storybook supports an addon called a11y, which is powered by Axe. The plugin will test for accessibility on each story you write. It does this by auditing the rendered DOM tree against WCAG rules and other industry-accepted best practices.

Storybook catching accessibility issues for Radix-UI’s Toast component.

Of course, this method alone is not going to detect every possible accessibility issue on the app — there are still parts that require manual testing.

However, the value of the a11y addon is that it makes accessibility testing as easy as linting your files for a syntax error. With this, I was able to catch accessibility issues, such as color contrast ratio, semantic errors, and ARIA labels, which are easy to miss otherwise.

In fact, Storybook claims that using Axe finds about 57% of WCAG issues automatically.

Interaction test

Simulating user interaction is also another important aspect of testing the components.

Can a user fill out the form as intended?

Does the form display proper error messages when invalid input is submitted?

These are the types of questions that interaction tests try to answer.

You may have already used some popular testing tools like Testing Library and Jest to simulate user behaviors and observe how it affects the DOM structures.

However, one downside to using these tools is that debugging UI can get quite clunky, as you have to sift through a blob of HTML in CLI and try to visualize in your head where things are going wrong on the UI.

Storybook solves this issue by providing a Storybook-instrumented version of both Testing Library and Jest — storybook/test. One key difference is that these tests will run in the browser instead of JSDOM.

This makes debugging much easier as you can visualize the parts that are failing.

Writing interaction tests is similar to mocking user behavior with Testing Library and Jest. So you can expect to work with familiar syntax to interact with the DOM.

Here is an example of interaction for my <Form /> component:

// form.stories.tsx
import { within, userEvent, expect } from "@storybook/test";

export const FormValidation: Story = {
...
// Interaction testing using `play` functions.
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nameInput = canvas.getByLabelText("Name", { selector: "input" });

expect(nameInput).toBeInTheDocument();
...
const submitBtn = canvas.getByRole("button", { name: "Submit" });
await userEvent.click(submitBtn);
...

expect(nameError).toBeVisible();
},
};

Visual test

Spotting UI changes is easy early in development, but as the project progresses and more developers get involved, visual bugs increase and monitoring the UI becomes time-consuming.

This is where visual testing plays a crucial role. As the name implies, the main purpose of visual testing is to make sure your UI components are rendered correctly on the screen.

The challenge of visual testing is that it’s not possible to cover every UI detail with code alone. This is the area where human judgment is more reliable than the pre-written test code.

In practice, visual testing using Storybook comes in two parts:

  1. Render the components in Storybook and verify them across a set of defined test states (hover, loading, error, etc.).
  2. Run a regression test by capturing a snapshot of the UI and comparing it against the baseline.

i. Verifying UI — does it look right?

In Storybook, each story is treated as a test case. That means that every story you write will be rendered in the browser, based on the particular state of the component, and you’ll be able to see how the component renders.

Here, by creating a story file for the <Button /> component, you can register a component to Storybook.

// Button.stories.tsx
import Button from "./button";

...

// A test case for default state
export const Default: Story = {
args: {
variant: "contained",
},
render: ({ variant, intent, size }) => (
<Button variant={variant} intent={intent} size={size}>
Call To Action
</Button>
),
};

And then you can spin up the Storybook server to see how your component renders.

You can add additional test cases by simply adding stories that take different set of props or state:

// Button.stories.tsx
import Button from "./button";

...

export const Default: Story = {
...
}

// Additional test case for rendering Icon Button
export const Icon: Story = {
render: ({ variant, intent, size, iconPosition }) => (
<div className="inline-flex gap-4">
<Button
icon={<PlusIcon />}
variant={variant}
intent={intent}
size={size}
iconPosition={iconPosition}
>
Call To Action
</Button>
<Button
icon={<PlusIcon />}
aria-label="add button"
variant={variant}
intent={intent}
size={size}
/>
</div>
),
};

Another perks of using Storybook is that by default, it will run smoke test for every story files, making sure that the components render as expected without errors.

You can also combine with the test runner to run all the interactions tests that is included in the story.

npm run test-storybook --watch

ii. Catching unintended UI change with visual regression testing

Another aspect of visual testing with Storybook is that you can run a visual regression test against your previous UIs.

UI can always break as we make changes, even if you got it right the first time. Catching these unintended changes manually is not a very feasible way to maintain the product in the long run.

Luckily, Storybook handles a lot of the heavy lifting of verifying UI changes using Chromatic. Chromatic is a visual testing tool developed by the Storybook team.

Chromatic captures an image snapshot of every story, and anytime I make a change, a new snapshot is captured and compared to the previous one.

Image from Storybook.js.org

This catches even the slightest changes in spacing and border radius. It’s a huge time-saver for me as it allows me to quickly identify the modifications I’ve made in the code.

Here’s the example of how I used Chromatic to spot visual bugs!

The examples that I used for this project is based on Storybook v7.6.1. With the newer version of Storybook (v8), you can now run regression testing within Storybook app. Be sure to checkout Storybook’s documentation, as they have changed the workflow of visual testing.

Automating test workflows with Github Action

To complete the circle, I needed a way to combine various UI tests into a more productive workflow. Although having these UI tests in place was vital for building reliable components, I also did not want these tests to become a bottleneck that disrupts the workflow.

For this, I used GitHub Action to build a testing pipeline that automates tests for accessibility, user interaction, and visuals.

Here’s a high-level overview of my development workflow, from building to testing the component.

Every time I push the code to dev branch, Github workflow will run three jobs:

  • Install and cache npm dependencies necessary to run the UI tests
  • Run interaction test and accessibility audit with Jest
  • Run visual test with Chromatic, which will deploy your Storybook to Chromatic for UI review.
name: "UI Tests"

on:
pull_request:
branches: [dev]

jobs:
# Install and cache npm dependencies
install-cache:
runs-on: ubuntu-latest
steps:
...
- name: Install dependencies if cache invalid
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci

# Run interaction and accessibility tests
interaction-and-accessibility:
runs-on: ubuntu-latest
needs: install-cache
steps:
...
- name: Build Storybook
run: npm run build-storybook --quiet

- name: Serve Storybook and run tests
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \\
"npx http-server storybook-static --port 6006 --silent" \\
"npx wait-on tcp:6006 && npm run test-storybook"

# Run visual and composition tests with Chromatic
visual-and-composition:
runs-on: ubuntu-latest
needs: interaction-and-accessibility
steps:
...
- name: Run Chromatic
uses: chromaui/action@latest
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

You can checkout the entire workflow file here.

If any of the test fails, it will stop executing the action and terminate the workflow.

Once bugs are fixed and all the tests passes, then it is now safe to merge code into the target branch. In my project, I have also set up Vercel’s CI/CD pipeline to run after UI testing, just so to make sure that the entire app renders as expected without bugs.

You can also find more details on how to set up Github Action with Storybook in their official document.

What I’ve learned along the way

Through this journey, I was able to create a service called NomadHair using a Component Driven Approach.

Although it was a small-scale project, I realized that beautiful UIs can be developed efficiently and systematically through Component Driven development. Furthermore, by going from design to actual service deployment, I developed a product-oriented mindset beyond just creating UIs.

Here are some key takeaways from this experience:

  • I gained a better understanding of the languages and processes used by designers, such as Design Systems, Variants, Color, and Typography, which helped me develop a more visually coherent UI.
  • I developed the habit of seeing the big picture from a product perspective and planning accordingly during development.
  • Setting up automated test pipeline allowed me to develop efficiently while also improving the quality of UI components.

Of course, there were some challenges along the way:

  • Testing server components with the current Storybook is still limited. I may need to upgrade the Storybook version or explore other ways to test server components.
  • Maintaining two separate development environments (Storybook and Dev) can add an extra layer of complexity.
  • Since this project was carried out independently, additional verification is needed to see how effective the Component Driven approach is when collaborating with actual designers and other developers. I plan to collaborate with others while maintaining the app in the future.

There are also many topics that I didn’t cover in this series, such as backend system, handling authentication, deployment, and hosting services. I plan to write a brief article about these as well.

There are still a lot of rooms for improvement and I have much more to learn, but I plan to gradually fill these gaps as I continue to improve the app. I hope this article provides some insights to those who wish to develop using the Component Driven approach.

Thank you for reading this long article.

--

--

Luke Nam

Taking one day at a time - cultivating my digital garden :) 🌳