The ultimate component testing suite - Cypress, Storybook and Mock Service Worker

How to

Software development

Written By

Alex Harvey

Date

2 years ago

Read time

7 minutes

If you’re a JavaScript developer, it’s more than likely you’ve previously used Cypress for writing and running end-to-end tests. The Cypress team are big fans of using Storybook to create a catalog of their components, and recently they’ve released a new feature for Cypress called Cypress Component Testing, which runs tests on component stories.

In this blog post, we’re going to look at how we can use Cypress Component Testing along with Storybook and Mock Service Worker (MSW) to create the ultimate component testing suite. We’ll be using each of these tools for the following:

    Storybook: To create a story for our components that allow us to see the visual aspects without worrying about data concerns or network requests

    MSW: To mock network requests that are triggered by our component so that our story has realistic data and functions like it would when the site is running

    Cypress Component Testing: To write tests for our stories to ensure the functionality is working correctly

We’ll be testing the following Pokedex page which has been created using a NextJS app with a Node/GraphQL server that displays a list of Pokemon:

2022-10-14 16.34.57

Storybook

From the team’s website, Storybook is “a frontend workshop for building UI components and pages in isolation.” It’s a great tool that allows you to develop the visual parts of your components or pages without having to worry about business logic or data concerns. Let’s have a look at how we set up Storybook.

Setting Up

The first thing we’re going to do is set up Storybook. All that we need to do for now is run the following command in our (NextJS) app directory to initialise Storybook:

npx storybook init

You’ll see that some packages have been added and a folder called .storybook has been added your app directory, containing two files, main.js and preview.js. We’ll be updating the preview.js file after we set up MSW.

Creating Stories

Before we get into setting up MSW, we’re going to create some stories so we can check our mock server is working once it’s set up. Here’s a basic story for our Pokedex page component:

import React, { ComponentProps } from 'react'; import { Story } from '@storybook/react/types-6-0'; import { Pokedex } from './Pokedex'; export default { component: Pokedex, title: 'Pages/Pokedex', }; export const Template: Story<ComponentProps<typeof Pokedex>> = () => <Pokedex />; Template.storyName = 'Pokedex';

Mock Service Worker

Mock Service Worker (MSW) is an “API mocking library that uses the Service Worker API to intercept actual requests.” It provides us with an easy way to mock data for display purposes and testing, which is why it integrates with Storybook and Cypress Component Testing perfectly. Let’s take a look at how we can set up MSW to get it working with Storybook.

Setting Up

We’ll start by adding the MSW as a dev dependency to our project:

yarn add msw --dev

Now let’s create a folder called __mocks__ inside our app/src folder. Inside this folder we’ll create a new file, handlers.ts, which will store our handlers to mock our REST API/GraphQL calls. We’ll add to this later, but for now we’ll just export this as an empty array:

// src/__mocks__/handlers.ts export const handlers = [];

Now we need to set up MSW. There are two places where MSW can run - on the browser and on Node. Since we are going to be testing using Cypress Component Testing, which uses Storybook, our tests are going to run on the browser, so we need to follow the steps to set it up there. First we’ll initialise MSW:

npx msw init <PUBLIC_DIR> --save

where <PUBLIC_DIR> is your public directory. This can differ depending on the framework you’re using, so check out this list of common public directories if you’re unsure. We now need to create a file to configure and start our Service Worker:

// src/__mocks__/browser.ts import { setupWorker } from 'msw'; import { handlers } from './handlers'; export const worker = setupWorker(...handlers);

Note that if you are planning on using MSW on Node (e.g. for Jest tests), you can create a similar file for it to run on the server:

// src/__mocks__/server.ts import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers);

Creating Handlers

Handlers in MSW are the functions we use to mock each REST API call of GraphQL query. Our component uses a query, Pokemon, to fetch a list of Pokemon from the backend. Here’s what our query in GraphQL looks like:

import { gql } from 'graphql-tag'; export const pokemon = gql` query Pokemon($where: PokemonWhereInput) { findManyPokemon(where: $where) { name pokedex_number { id } } } `;

Screenshot 2022-11-11 at 11.19.01

Let’s now mock this response using the following handler:

// src/__mocks__/handlers.ts import { graphql } from 'msw'; export const handlers = [ graphql.query('Pokemon', (req, res, ctx) => { const pokemon = [ { name: 'Bulbasaur', pokedex_number: { id: 1, }, }, { name: 'Ivysaur', pokedex_number: { id: 2, }, }, { name: 'Venusaur', pokedex_number: { id: 3, }, }, ]; return res( ctx.data({ pokemon, }) ); }), ];

If we were using a REST architecture, assuming we had an endpoint /pokemon that returned a list of Pokemon to us, we can achieve the equivalent of the above using:

// src/__mocks__/handlers.ts import { rest } from 'msw'; export const handlers = [ rest.get('/pokemon', (_req, res, ctx) => { const pokemon = [ { name: 'Bulbasaur', pokedex_number: { id: 1, }, }, { name: 'Ivysaur', pokedex_number: { id: 2, }, }, { name: 'Venusaur', pokedex_number: { id: 3, }, }, ]; return res( ctx.status(200), ctx.json({ pokemon }) ); }), ];

Let’s add some additional functionality to our mock request. Our Pokedex component allows the user to search for a Pokemon by passing the search term they input into our GraphQL query like so:

// src/components/pages/Pokedex.tsx export const Pokedex = () => { const [searchTerm, setSearchTerm] = useState(''); const { data, isLoading } = usePokemonQuery({ where: { name: { contains: searchTerm } }, }); ... }

We can check for this value in our mock query by using the req variable, then filter the data we return based on it, essentially mocking the search functionality on the backend purely in our frontend:

// src/__mocks__/handlers.ts import { graphql } from 'msw'; export const handlers = [ graphql.query('Pokemon', (req, res, ctx) => { const searchTerm = req.variables?.where?.name?.contains ?? ''; const pokemon = [ { name: 'Bulbasaur', pokedex_number: { id: 1, }, }, { name: 'Ivysaur', pokedex_number: { id: 2, }, }, { name: 'Venusaur', pokedex_number: { id: 3, }, }, ]; return res( ctx.data({ pokemon: searchTerm ? pokemon.filter( (p) => searchTerm === '' || p.name.toLowerCase().includes(searchTerm.toLowerCase()) ) : pokemon, }) ); }), ];

Not only does this mean that the search functionality for this component will work inside Storybook, but we’ll also be able to use it in our Cypress component tests, as we’ll see later on!

Initialising MSW in Storybook

OK so we’ve set up Storybook and MSW, so the next step is getting them to work together. To do so, all we need to do is start the worker we created in src/__mocks__/browser.ts inside the Storybook storybook/preview.js file. Add the following import to this file:

import { worker } from '../src/mocks/browser';

Then add this line to the bottom of the file:

worker.start();

You should now see your mock data in your component when you run Storybook:

2022-11-11 10.55.28

Cypress

Our next step is to set up Cypress so that we can test our components. Let’s start by adding Cypress as a dev dependency:

yarn add -D cypress

Next, start Cypress with the command:

yarn cypress open

Screenshot 2022-11-11 at 10.01.45

It should detect the framework you are using (see here for a list of support frameworks), then tell you if you need to install any dependencies. Finally it will add some configuration files for you, which we are now going to edit.

First we’ll edit the cypress.config.ts file to set the viewport dimensions of our tests:

// cypress.config.ts import { defineConfig } from 'cypress'; export default defineConfig({ component: { devServer: { framework: 'next', bundler: 'webpack', }, viewportWidth: 1920, viewportHeight: 1080, }, });

You can delete the cypress/support/commands.ts file unless you want to add any custom commands. Next we’ll update cypress/support/component.ts to the following:

// cypress/support/component.ts /// <reference types="cypress" /> import { mount } from 'cypress/react18'; import { setGlobalConfig } from '@storybook/testing-react'; import * as sbPreview from '../../.storybook/preview'; declare global { namespace Cypress { interface Chainable { mount: typeof mount; } } } Cypress.Commands.add('mount', mount); setGlobalConfig(sbPreview);

What we’ve done here is wrap each of our stories in the global decorators that have been applied in .storybook/preview.js when they run inside Cypress.

Creating Component Tests

We’re now ready to start writing our component tests! Let’s have a look at how we do so.

An easy way to access DOM elements in our Cypress component tests is using the data-testid html attribute. Once we have selected the required element, we can use assertions to check things like existence, visibility, expected text etc. Let’s add a data-testid to the outermost div in our Pokedex component:

// src/components/pages/Pokedex.tsx export const Pokedex = () => { ... // stateful stuff ... return ( <div className="h-full p-2" data-testid="pokedex"> ... </div> ); };

Now we can write a basic test checking that when our component is mounted, it is visible in the DOM:

// src/components/pages/Pokedex.test.cy.tsx import { composeStories } from '@storybook/testing-react'; import * as stories from './Pokedex.stories'; const { Template } = composeStories(stories); describe('Pokedex component', () => { it('mounts correctly', () => { cy.mount(<Template />); cy.get('[data-testid="pokedex"]').should('be.visible'); }); });

2022-11-11 10.21.04

Let’s now add another slightly more involved test, that will test that the component’s search functionality is working correctly. We’ve added a data-testid to the search bar and each of the Pokemon that are displayed in the list. Our test looks like this:

// src/components/pages/Pokedex.test.cy.tsx import { composeStories } from '@storybook/testing-react'; import * as stories from './Pokedex.stories'; const { Template } = composeStories(stories); describe('Pokedex component', () => { it('mounts correctly', () => { cy.mount(<Template />); cy.get('[data-testid="pokedex"]').should('be.visible'); }); it('displays only the pokemon searched for', () => { cy.mount(<Template />); cy.get('[data-testid="search-bar"]').type('Bulbasaur'); cy.get('[data-testid="Bulbasaur"]').should('be.visible'); cy.get('[data-testid="Ivysaur"]').should('not.exist'); cy.get('[data-testid="Venusaur"]').should('not.exist'); }); });

2022-11-11 10.25.20

And that’s it! Hopefully you can see the potential of using these three great tools together. Storybook provides us with a catalog that lets us see our components in isolation, MSW provides us with mock data we can use in our stories and tests, and Cypress Component Testing gives us a visual way to test our components.

Similar news to this

Blog Image

What is Hygen and how could it help speed up your project?

2 years ago

How to

Blog Image

Next.js: How to implement a dynamic sitemap

a year ago

How to

Subscribe to our newsletter

Be the first to know about our latest updates, industry trends, and expert insights

Your may unsubscribe from these communications at any time. For information please review our privacy policy.