loading...

Test React Component with cypress-react-unit-test Example

bahmutov profile image Gleb Bahmutov ・4 min read

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.

Button spec is passing

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.

Opening DevTools and inspecting the Button

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.

Click command changes the button text

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')
  });
});

The test against component that uses hooks

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

Mocking fetch response

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.

Posted on by:

bahmutov profile

Gleb Bahmutov

@bahmutov

JavaScript ninja, image processing expert, software quality fanatic

Discussion

markdown guide
 

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"; to cypress/support/index.js

 

Very nice big thank you for your passion and work