Back to blog

Testing user flows

Verify that your UI works end-to-end

loading
Varun Vachhar
โ€” @winkerVSbecks
Last updated:

Debugging in production is a nightmare. You have to check every layer of your app. Is it a component bug, a misfiring event, styling, app state, or perhaps a broken API? It could be any of the above, and you have to untangle why.

UIs help folks navigate a sequence of steps on multiple pages to accomplish their goals. Storybook makes it easy to isolate each of those pages and run visual, accessibility and interaction tests on them. But to verify the entire flow and catch integration issues, you need end-to-end (E2E) UI tests.

I researched ten leading front-end teamsโ€”Peloton, Shopify, O'Reilly and moreโ€”to understand how they apply E2E tests to check user flows. This article summarizes my findings. You'll also learn about the tooling involved and how E2E tests fit into your UI testing strategy.

Does your UI work end-to-end?

User flows arenโ€™t contained to only one component, they involve multiple components working in tandem. Each interaction triggers state updates, route changes, and API calls that affect whatโ€™s rendered on screen. With all of these points of failure, it can be tough to QA them one-by-one.

Teams use E2E tests to ensure that the user experience works as intended. To run an E2E test, you start by spinning up a complete instance of an application. Then use tools like Cypress, Playwright, or Selenium to verify the user flow by simulating user behavior.

Apps are assembled by connecting components to data, business logic and APIs

Testing the complete application comes with trade-offs

On the surface, E2E and interaction tests seem pretty similar. But remember, your users interact with an app in addition to its constituent components. E2E tests run at the app level, which allows them to uncover integration issues between the frontend and backend. But that also requires you to maintain test infrastructure for more layers of your tech stack (time-consuming!).

Component level testing is done by self-contained tools which can mount, render and test components. With E2E tests, you're responsible for spinning up the application. For which, you have two options:

  1. Maintain a full test environment: this includes front-end, back-end, services, and seeded test data. For example, O'Reilly team uses Docker to spin up their entire app infrastructure and run E2E tests.
  2. Maintain a front-end only test environment paired with a mock back-end. For example, Twilio tests flows by using Cypress to stub out network requests.

Either way, the complexity ramps up with the scale of your system. The larger the system the more cumbersome it is to replicate the setup on a continuous integration server and then connect to a cloud browser to run the tests.

Given this trade-off, most teams use a hybrid approach to balance effort and value. E2E tests are limited to only critical user flows. Interaction tests are used to verify all other behavior.

In this tutorial, weโ€™re E2E testing using Cypress with the mocked back-end approach. Here's a summary of the workflow:

  1. โš™๏ธ Setup: spin up the application and mock network requests (reuse data from stories)
  2. ๐Ÿค– Action: use Cypress to visit pages and simulate interactions
  3. โœ… Run assertions to verify that the UI updated correctly

Tutorial

Letโ€™s see the workflow action with the Taskbox app that I introduced in an earlier post. Weโ€™ll write an E2E test for the authentication flow: navigate to the login page and fill in the user credentials. Once authenticated, the user should be able to see a list of their tasks.

Grab the code and load up the app in development mode by running yarn start. Then open http://localhost:3000 and youโ€™ll be presented with the login screen.

Set up Cypress

Run: yarn add cypress --dev to install the Cypress package. Then add the Cypress command to the scripts field of your package.json file.

 "scripts": {
   "cypress": "cypress open"
 }

Next, add a cypress.json file at the root of your project. Here we can configure the base URL for our application so that we donโ€™t have to repeat ourselves when writing out actual test commands.

// cypress.json
{
 "baseUrl": "http://localhost:3000"
}

And finally, run yarn cypress to finish up the setup process. This will add a cypress folder to your project. All the test files will live here. It will also start the Cypress test runner.

Testing the auth flow

Cypress test structure is quite similar to other types of testing you might be familiar with. You start by describing what youโ€™re going to test. Each test lives in an it block where you run assertions. Hereโ€™s what the authentication user flow test looks like:

// cypress/e2e/auth.spec.js

describe('The Login Page', () => {
  it('user can authenticate using the login form', () => {
    const email = 'alice.carr@test.com';
    const password = 'k12h1k0$5;lpa@Afn';

    cy.visit('/');

    // Fill out the form
    cy.get('input[name=email]').type(email);
    cy.get('input[name=password]').type(`${password}`);

    // Click the sign-in button
    cy.get('button[type=submit]').click();

    // UI should display the user's task list
    cy.get('[aria-label="tasks"] li').should('have.length', 6);
  });
});

Letโ€™s break down whatโ€™s happening here. cy.visit opens the browser to the login page of our application. Then we use the cy.get command to find and fill out the email and password fields. And finally, click the submit button to actually log in.

The last bit of the test runs assertions. In other words, we verify whether the authentication was successful. We do this by checking to see if the list of tasks is now visible.

Switch over to the Cypress window and you should see that the test is executed.

But, notice that the test failed. Thatโ€™s because weโ€™re only running the front-end of the application. All HTTP requests will fail since we donโ€™t have an active back-end. Instead of spinning up the actual back-end weโ€™ll use stubbed network requests.

Mocking requests

The cy.intercept method allows us to intercept network requests and respond with mock data. The auth user flow relies on two requests: /authenticate to log in and /tasks to fetch the userโ€™s tasks. To stub these requests we're gonna need some mock data.

In a previous post, I demonstrated how you can catalogue all the use cases of a component using Storybook. During that process we created mock data for the task list in the TaskList.stories.js file. We can reuse that in our Cypress test.

import React from 'react';
import { TaskList } from './TaskList';
import Task from './Task.stories';

export default {
  component: TaskList,
  title: 'TaskList',
  argTypes: {
    ...Task.argTypes,
  },
};
const Template = (args) => <TaskList {...args} />;

export const Default = Template.bind({});
Default.args = {
  tasks: [
    { id: '1', state: 'TASK_INBOX', title: 'Build a date picker' },
    { id: '2', state: 'TASK_INBOX', title: 'QA dropdown' },
    {
      id: '3',
      state: 'TASK_INBOX',
      title: 'Write a schema for account avatar component',
    },
    { id: '4', state: 'TASK_INBOX', title: 'Export logo' },
    { id: '5', state: 'TASK_INBOX', title: 'Fix bug in input error state' },
    { id: '6', state: 'TASK_INBOX', title: 'Draft monthly blog to customers' },
  ],
};

Letโ€™s go ahead and update the test to mock those two network requests.

// cypress/e2e/auth.spec.js

import { Default as TaskListDefault } from '../../src/components/TaskList.stories';

describe('The Login Page', () => {
  beforeEach(() => {
    cy.intercept('POST', '/authenticate', {
      statusCode: 201,
      body: {
        user: {
          name: 'Alice Carr',
          token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
        },
      },
    });

    cy.intercept('GET', '/tasks', {
      statusCode: 201,
      body: TaskListDefault.args,
    });
  });

  it('user can authenticate using the login form', () => {
    const email = 'alice.carr@test.com';
    const password = 'k12h1k0$5;lpa@Afn';

    cy.visit('/');

    // Fill out the form
    cy.get('input[name=email]').type(email);
    cy.get('input[name=password]').type(`${password}`);

    // Click the sign-in button
    cy.get('button[type=submit]').click();

    // UI should display the user's task list
    cy.get('[aria-label="tasks"] li').should('have.length', 6);
  });
});

Re-run the test and it should be passing now.

We booted up the full application. With Cypress, we were able to simulate user behaviour and test the login flow. In this one test, we checked data flow, form submission and API calls.

Conclusion

User experiences are assembled by composing components and wiring them up to data and APIs. Storybook makes it easy to test them at the component level. You capture use cases as stories and execute assertions using Jest and Testing Library. These tests are easy to maintain and provide a fast feedback loop. However, they don't allow you to test flows spanning multiple pages.

E2E tests, on the other hand, run on an entire instance of the app. You can test user flows and verify that all layers of the system are working as expected. The downside is the process requires significant time and effort.

Given this trade-off, most teams limit themselves to a small set of E2E tests for the core user flows. And use component-level tests for broader coverage.

Tests are only helpful if you are running them consistently. Leading engineering teams use a Continuous Integration (CI) server to run their full test suiteโ€”automatically on every code push. The next article will recap the complete UI testing strategy and show you how to automate that workflow. Join the mailing list to get notified as more UI testing articles are published.

Join the Storybook mailing list

Get the latest news, updates and releases

6,655 developers and counting

Weโ€™re hiring!

Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.

View jobs

Popular posts

How to automate UI tests with Github Actions

Speed up your workflow and ship higher quality of code
loading
Varun Vachhar

UI Testing Playbook

A testing workflow that doesnโ€™t slow you down
loading
Varun Vachhar

Interaction Testing sneak peek

Test connected components with Storybookโ€™s play function
loading
Dominic Nguyen
Join the community
6,655 developers and counting
WhyWhy StorybookComponent-driven UI
DocsGuidesTutorialsChangelogTelemetry
CommunityAddonsGet involvedBlog
ShowcaseExploreProjectsComponent glossary
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI