This post was originally published in my Medium blog.
At work, we've recently been shifting our testing strategy to use Cypress. We've found it's a powerful tool that has enabled fast and reliable end-to-end and integration tests, but there's one piece that until recently was missing: unit tests. Cypress introduced some experimental features designed to help with unit testing and over the last few days Iโve spent some time familiarising myself with these to see if we can do all our testing in one framework.
๐ TL;DR: You can find my example repo with the finished code here ๐
Prerequisites
In this tutorial, I'm unit testing a Progress Bar component. It's built with React in TypeScript and styling with styled-components so we need all these dependencies.
Getting Started
To add and setup all these packages in package.json
:
Cypress
$ yarn add -D cypress cypress-react-unit-test
Enable experimentalComponentTesting (don't worry, it's quite stable now) and assign componentFolder to your preferred location for your tests. I also added a condition to only targetย .spec files but that's optional.
{
"experimentalComponentTesting": true,
"componentFolder": "cypress/component",
"testFiles": "**/*.spec.*"
}
We also need to setup Cypress to include the unit test plugin. Add the following to cypress/support/index.js
import 'cypress-react-unit-test/support';
TypeScript
๐ก You can skip this step if you are using create-react-app with TypeScript template because it doesn't require extra settings.
Cypress supports TypeScript as long as you have a tsconfig.json file. However, imports don't work unless you preprocess your TypeScript files.
Let's add the necessary packages to our dev dependencies if they're not already configured.
$ yarn add -D webpack ts-loader @cypress/webpack-preprocessor
In webpack.config.js
: ts-loader is needed to preprocess TypeScript files.
{
rules: [
{
test: /\.tsx?$/,
exclude: [/node_modules/],
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
},
},
],
},
],
}
Add the webpack preprocessor toย ./cypress/plugin/index.js
const preprocessor = require('@cypress/webpack-preprocessor');
module.exports = (on, config) => {
const webpack = require('./webpack.config.js');
on("file:preprocessor", preprocessor({ webpack }));
return config;
};
Then make sure TypeScript understands Cypress' types by
adding this to tsconfig.json
{
"compilerOptions": {
"types": ["cypress"]
}
}
And that's all the setup done, we are good to go!
Unit Testing Our First Component
I picked a Progress Bar component to unit test. You can view the Progress Bar Component on a Live Site and check how the functionality works in my Github repository.
What do we want toย test?
- A progress bar should be visible
- A progress bar should setup its state correctly based on passed data
- A progress bar should render correctly in different states
With unit tests, we're aiming to test functionality rather than styling itself. If you want to test all your styles then adding Snapshot or Visual Regression to your integration tests would be more suitable. In this example, we are testing state of the componentโ-โwhen the step changes, does the rendering change as we expect? This is also part of the functionality.
Add testย scripts
./cypress/component/ProgressBar.spec.tsx
/// <reference types="cypress" />
import React from "react";
import { mount } from "cypress-react-unit-test";
import ProgressBar from "../../src/ProgressBar";
import GlobalStyle from "../../src/globalStyle";
describe("Progress Bar", () => {
const mockSteps = ["Step 1", "Step 2", "Step 3", "Step 4"];
it("renders steps", () => {
mount(
<ProgressBar steps={mockSteps}><GlobalStyle /></ProgressBar>
);
cy.get("ul li").first().should("have.text", "Step 1")
.next().should("have.text", "Step 2")
.next().should("have.text", "Step 3")
.next().should("have.text", "Step 4");
cy.get("ul li").find("span")
.and("have.css", "background-color", "rgb(255, 255, 255)")
.and("have.css", "border-color", "rgb(0, 182, 237)");
});
it("renders active steps", () => {
mount(
<ProgressBar steps={mockSteps} current={3}>
<GlobalStyle />
</ProgressBar>
);
cy.get("ul li:nth-child(2)").find("span")
.and("have.css", "background-color", "rgb(0, 182, 237)")
.and("have.css", "border-color", "rgb(0, 0, 0)");
cy.get("ul li:nth-child(3)").find("span")
.and("have.css", "background-color", "rgb(255, 255, 255)")
.and("have.css", "border-color", "rgb(0, 182, 237)");
cy.get("ul li:nth-child(4)").find("span")
.and("have.css", "border", "3px solid rgb(198, 198, 198)");
});
});
There are two key concepts here:
- mount tells Cypress that we want it to render our React Component on its own rather than in the context of a whole application
- mockData is used so we can test the Component outside the context of our application.
Our first test "renders steps" simply checks that the component has correctly setup the mockData that we passed to it. We can do this by checking that the text for each step matches what we passed in.
In our second test "renders active steps" we also set the third step to be "active"ย . We then expect that the component will render this with a blue open circle. We also expect the first and second step to be "completed" (with a blue background colour and white tick) and the fourth step should be "inactive" (a grey open circle). It's a simple test but very effective, we covered both functionality and state of the component.
Note that we only tested the styles that are changed by the component on state change, not all the styling.
Run yarn cypress open
, you should see your browser load, the tests run and pass! ๐
But some of my styles are missing?
Cypress is running unit tests against our component in isolation. When using styled-components, the style is self contained, we don't need to load external css or separate stylesheet. However, in some cases, we will rely on global styling (i.e: fonts, font size, spacing..etc) from the top level to ensure our component displays correctly during test.
The simplest solution is to also mount GloablStyleโ-โwe use this helper function to generate a special StyledComponent that handles global styles by styled-components.
import GlobalStyle from "../../src/globalStyle";
...
mount(
<ProgressBar steps={mockSteps}><GlobalStyle /></ProgressBar>
);
While this is useful for us visually when we run the tests in our browser, it is not necessary; remember, we're only testing the functionality that is built into the component, not all of our styles!
Run Cypress unit testing with NPM scripts
I encountered an odd issue with NPM script. When adding cypress run into the script, it triggered all our tests including integrations. This is not ideal, we need to able to run unit tests on a specific component file or folder so we don't have to wait for everything to complete.
A useful tip I discovered is to add a separate command with yarn into our build script. Also for some reason two extra dashes are needed before -- spec to target file or folder.
"scripts": {
"test:unit": "yarn cypress run -- --spec 'cypress/component/*.spec.tsx'"
}
That's it!
This is a quick walkthrough to explain how Cypress can unit test a React component. Setting up Typescript can be a little fiddly, but once you've got that done the testing is really straight forward. If you want to see more of what you can do with Cypress unit tests, this is a really repo with lots of examples to start diving in further https://github.com/bahmutov/cypress-react-unit-test
Hopefully this tutorial helps! ๐
Top comments (1)
i like the idea of the ability to test each component(s) individually, with an actual browser running. but assertions in cypress feel a little bit weird