I have read Testing React Components with react-test-renderer and the Act API by Valentino Gagliardi and thought it was a great post. I wanted to see how the same tests could be written using Cypress and cypress-react-unit-test. You can find my source code in repo bahmutov/testing-react-example
Let's get a React component working in the repository. The simplest case is to use react-scripts.
# We need react-scripts to build and run React components
npm i -S react react-dom react-scripts
# We need Cypress test runner and
# React framework adaptor
npm i -D cypress cypress-react-unit-test
Button component
Let's test the Button
component in the src
folder. Let's write the spec first, and we can code the Button
component directly inside the spec file before factoring it out into its own file.
testing-react-example/
cypress/
fixtures/
integration/
plugins/
support/
src/
Button.spec.js
package.json
cypress.json
The cypress.json
file has all Cypress settings. In our case we want to enable the experimental component testing feature.
{
"experimentalComponentTesting": true,
"componentFolder": "src",
"specFiles": "*spec.*"
}
The src/Button.spec.js
looks like this:
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
function Button(props) {
return <button>Nothing to do for now</button>;
}
describe("Button component", () => {
it("Matches the snapshot", () => {
mount(<Button />);
});
});
We run this test in interactive mode with command
npx cypress open
and clicking Button.spec.js
filename.
The test passes - and at first it does not look like much.
Look closer - this is real browser (Electron, Chrome, Edge or Firefox) running the Button
component as a mini web application. You can open DevTools and inspect the DOM just like you would with a real web application - because it is real.
Button with state
Now that we have the component and a corresponding component test, let's make the component a little more interesting.
import React from "react";
import { mount } from "cypress-react-unit-test";
class Button extends React.Component {
constructor(props) {
super(props);
this.state = { text: "" };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(() => {
return { text: "PROCEED TO CHECKOUT" };
});
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.text || this.props.text}
</button>
);
}
}
describe("Button component", () => {
it("it shows the expected text when clicked (testing the wrong way!)", () => {
mount(<Button text="SUBSCRIBE TO BASIC" />);
cy.get('@Button')
});
});
Hmm, how do we check the state value of the component? We don't! The state is an internal implementation detail of the component. Instead we want to test the component using events from the user, like Click.
describe("Button component", () => {
it("it shows the expected text when clicked", () => {
mount(<Button text="SUBSCRIBE TO BASIC" />);
cy.contains('SUBSCRIBE TO BASIC')
.click()
.should('have.text', 'PROCEED TO CHECKOUT')
});
});
The test does change - we can see it in the browser, and we can see the DOM change by hovering over CLICK
command.
The time-traveling debugger built-into Cypress makes going back and inspecting what the component does in response to the user events very simple.
Change implementation
Testing against the interface and not the implementation allows us to completely rewrite the component, and still use the same test. Let's change our Button
component to use React Hooks. Notice the test remains the same:
import React, { useState } from "react";
import { mount } from "cypress-react-unit-test";
function Button(props) {
const [text, setText] = useState("");
function handleClick() {
setText("PROCEED TO CHECKOUT");
}
return <button onClick={handleClick}>{text || props.text}</button>;
}
describe("Button component", () => {
it("it shows the expected text when clicked", () => {
mount(<Button text="SUBSCRIBE TO BASIC" />);
cy.contains('SUBSCRIBE TO BASIC')
.click()
.should('have.text', 'PROCEED TO CHECKOUT')
});
});
Mocking methods
Let's continue. Imagine the component is fetching a list of users. The component is running in the same environment as the spec, sharing the window
object and thus it can stub its method fetch
.
import React, { Component } from "react";
import {mount} from 'cypress-react-unit-test'
export default class Users extends Component {
constructor(props) {
super(props);
this.state = { data: [] };
}
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => {
// make sure to check for errors
return response.json();
})
.then(json => {
this.setState(() => {
return { data: json };
});
});
}
render() {
return (
<ul>
{this.state.data.map(user => (
<li key={user.name}>{user.name}</li>
))}
</ul>
);
}
}
describe("User component", () => {
it("it shows a list of users", () => {
const fakeResponse = [{ name: "John Doe" }, { name: "Kevin Mitnick" }];
cy.stub(window, 'fetch').resolves({
json: () => Promise.resolve(fakeResponse)
})
mount(<Users />)
cy.get('li').should('have.length', 2)
cy.contains('li', 'John Doe')
cy.contains('li', 'Kevin Mitnick')
});
});
The test passes and you can see the individual elements
Notice we did not have to tell the test to wait for the users to be fetched. Our test simply said "mount the component, there should be 2 list items"
mount(<Users />)
cy.get('li').should('have.length', 2)
In the Cypress test, every command is asynchronous, and almost every command will retry until attached assertions pass. Thus you don't need to worry about synchronous or asynchronous differences, fast or slow responses, etc.
Give cypress-react-unit-test a try. Besides this example bahmutov/testing-react-example, there are lots of examples and my vision for component testing is described in this blog post.
Top comments (3)
Hey, The blog is great and explanation is so simple and accurate..
i got struck here while i execute the npx cypress open. is it supported by create-react-app or should i eject it and do webpack config separately? cypress browser says
[cypress-react-unit-test] 🔥 Hmm, cannot find root element to mount the component. Did you forget to include the support file? Check https://github.com/bahmutov/cypress-react-unit-test#install please
PS: thanks in advance.
add
import "cypress-react-unit-test/support";
tocypress/support/index.js
Very nice big thank you for your passion and work