UI Testing Playbook
A testing workflow that doesn’t slow you down
UI testing workflows often spiral into maintenance nightmares. Your tests break whenever there’s an implementation tweak. You duplicate test cases for every tool.
It’s easy to find tools that test different parts of the UI. But knowing how to combine them into a productive workflow is tricky. If you get it wrong, the UI development process feels like a slog.
I interviewed ten teams from companies like Twilio, Adobe, Peloton and Shopify to see how they balanced UI testing effort and value. Despite differences in team size and tech stack, folks had similar tactics. This article combines those learnings into the pragmatic workflow described below.
- 📚 Isolate components using Storybook. Write test cases where each state is reproduced using props and mock data.
- ✅ Catch visual bugs and verify composition using Chromatic.
- 🐙 Verify interactions with Jest and Testing Library.
- ♿️ Audit accessibility of your components using Axe.
- 🔄 Verify user flows by writing end-to-end tests with Cypress.
- 🚥 Catch regressions by automatically running tests with GitHub Actions.
What works
It should be seamless to build and test UI components. That comes down to two considerations: reducing maintenance burden while adding flexibility in how you run tests.
Test at the component level to find bugs faster
The teams I surveyed also shared that they mostly run tests at the component level. Components allow you to break up the interface into isolated chunks. Testing in isolation makes it easier to pinpoint bugs.
Reuse stories to reduce maintenance
Each type of test uses different tools. This means you're often replicating the same component state over and over. That's a headache to set up and maintain. Storybook enables you to isolate a component and capture all test cases in a *.stories.js
file. You can then import them into tools such as Jest and Cypress. The end result, you only have to write your test cases once.
Test while you code for a faster feedback loop
During development, you're focused on a handful of components related to the feature you're working on. Therefore, you'll want to run targeted tests on just those components.
Run all checks before you merge
When you're getting ready to merge, you'll want to check for regression bugs. That means running your entire test suite automatically using a CI server.
UI Testing workflow tutorial
To demonstrate this process, I'll use the Taskbox app—a task management app similar to Asana. Previously, we looked at how to write all the different types of tests for this UI. Now, let's add in the ability to delete a task and step through the entire testing workflow.
For this demo, let's jump straight to the point where you're ready to test. If you're curious about the implementation, check out the project repo.
Visual & Composition tests
First, we just want to update the UI and make sure all the styles match the spec. We're going to add the delete button to the Task component.
During development
Instead of booting up the entire application, you can use Storybook to focus on just the Task component. Then cycle through all its stories to manually verify their appearance.
PR check
Tweaks to the Task UI can lead to unintended changes in other components where it's used: TaskList and InboxScreen. Running visual tests with Chromatic will catch those. It'll also ensure that everything is still wired up correctly.
Chromatic will be triggered automatically when you create a pull request. On completion, you'll be presented with a diff to review. In this case, the changes are intentional. Press the accept button to update the baselines.
Accessibility tests
During development
Run accessibility checks inside Storybook during development. The A11y addon uses Axe to audit the active story and displays the report in the addon panel. A quick glance confirms that none of our stories have any violations.
PR check
To catch regressions you need to run on all your components. You can do that by importing stories into a Jest and then running an accessibility audit using jest-axe. All violations will be reported back to the PR page.
Interaction tests
The user can delete a task by clicking on the trash can button, we’ll need to add in a test to verify that behaviour.
During development
During development, manually verify the interaction using the InboxScreen stories. If it’s working as expected, you can move on to adding in an interaction test using Jest and Testing Library.
// InboxScreen.test.js
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import {
render,
waitFor,
cleanup,
within,
fireEvent,
} from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { composeStories } from '@storybook/testing-react';
import { getWorker } from 'msw-storybook-addon';
import * as stories from './InboxScreen.stories';
expect.extend(toHaveNoViolations);
describe('InboxScreen', () => {
afterEach(() => {
cleanup();
});
afterAll(() => getWorker().close());
const { Default } = composeStories(stories);
it('should pin a task', async () => { ... });
it('should archive a task', async () => { ... });
it('should edit a task', async () => { ... });
it('Should have no accessibility violations', async () => { ... });
it('should delete a task', async () => {
const { queryByText, getByRole, getAllByRole } = render(<Default />);
await waitFor(() => {
expect(queryByText('You have no tasks')).not.toBeInTheDocument();
});
const getTask = () => getByRole('listitem', { name: 'Export logo' });
const deleteButton = within(getTask()).getByRole('button', {
name: 'delete',
});
fireEvent.click(deleteButton);
expect(getAllByRole('listitem').length).toBe(5);
});
Run yarn test
to confirm that all tests are passing. Notice how Jest runs in watch mode and only executes tests related to files that changed.
PR check
Github Actions will run Jest when the pull request is created and report status via PR checks.
User flow tests
Lastly, you'll need to run E2E tests to ensure that all your critical user flows are working as expected.
During development
You can run targeted E2E tests during development but, that requires you to spin up the complete instance of your application and a test browser. It can all get quite resource-heavy. Therefore, you can wait to run Cypress on the CI server unless you're updating a test.
PR check
Just like all your other tests, Github actions will also run E2E tests using Cypress.
Conclusion
Developers spend 21% of their time fixing bugs. Tests help reduce the amount of work you have to do by catching defects and speeding up debugging.
But every new feature introduces more UI and states that need tests. The only way to stay productive is to implement an intuitive testing workflow.
Start by writing test cases as stories. You can reuse them in testing tools such as Jest, Chromatic and Axe. Studies suggest that reusing code can shave 42-81% off dev time.
During development, test while you code to fix obvious bugs. That shortens your feedback loop. It also prevents bugs from hitting production which is 10x more expensive to fix.
Finally, use a CI server to run all your checks across the entire UI to prevent accidental regressions.
I hope distilling these learnings into a pragmatic workflow helps you implement a solid testing strategy of your own. Let this workflow be your starting point.