Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Unit testing with React and Cypress

6 min read 1924

Unit Testing With React And Cypress

Every developer wants to ship a bug-free production application. To achieve this, we need to consider how we can integrate tests into our apps. There are many testing tools, frameworks, and types of tests available to us.

Cypress is a modern, automated testing suite. It’s a fully open source testing framework based on JavaScript and is built with libraries like Mocha and Chai that support BDD and TDD assertion styles. Further, using Cypress will be easy if you’re familiar with writing tests in JavaScript.

Cypress covers two major styles of writing tests: end-to-end testing and unit testing. In this article, we’ll be going over unit testing with Cypress. We’ll overview unit testing, discuss the benefits of using Cypress, Cypress’ installation, commands, and assertions, run Cypress, and write our tests.

What is unit testing?

Unit testing is used by developers to verify that an application runs well. It involves checking every module of the application independently.

The term “unit” refers to a single testable part of an application. This process helps testers and developers catch and understand early bugs in a development environment. The staff then has enough time to change any defect code.

It’s not uncommon to see confusion between the different types of testing. Below is a table covering the main differences between end-to-end and unit testing:

End-to-end (E2E) testing Unit testing
Used to test the entire application from start to end. Used to test the individual units of the application.
Checks the behavioral flow of the application. Checks functional verification of the unit.
Generally a costly means of testing. Generally a cost-efficient means of testing.
Mostly performed manually. Mostly automated.
Tests end-to-end parts of the application. Modules are tested separately.

Since Cypress covers both worlds of testing as mentioned earlier, it has been an excellent testing choice for the modern applications we build today. Let’s take a look at how Cypress helps us to build better testable products.

Benefits of testing with Cypress

In general, these are the biggest benefits of testing with Cypress that I’ve found so far:

  • Time travel debugging
  • Live code reloading
  • Current and previous state differentiation
  • The option between running headless tests or testing in a browser
  • Ability to take screenshots and video recordings of tests
  • Asynchronous testing
  • In-browser debugging environment

Cypress tests take snapshots and can provide readable errors and stack tracing for easier debugging. We also don’t need to add sleep timeout events to our test, as it has an automatic waiting functionality that ensures tests wait for commands and assertions before moving forward.

I’ve found that Cypress tests are less flaky and much faster than traditional testing suites. Cypress supports parallel execution, imports external libs, manipulates objects, stubs API calls and network traffic control, and has awesome learning documentation.



Moreover, Cypress can test every layer of our application, including the UI, database, API, frontend stores, cross-browser functions, XHR requests, and more.

Installing Cypress

Cypress runs on a Node.js server that frequently communicates with the browser (a test runner), instrumentalized by Cypress. It concurrently runs two iFrames, one of which is our test code. The other is an iFrame that looks at the tests in action.

We’ll be creating a React and Vite project from scratch with the command:

npm create [email protected]

Inside our application, let’s add Cypress to our dev dependency by running the following command:

npm i cypress --save-dev

Please make sure you choose the option for the unit testing available to us. With this in place, we will generate a couple of folders:

├── cypress
│   ├── downloads
│   └── component
│   └── fixtures
│   └── support
├── cypress.config.js

downloads will come in handy when we want to use screenshots or video recordings of our test being executed.

component is the location where we’ll be writing our unit test. We’ll create new files that can be selected later in the iFrame.

fixtures will be loading up all the JSON data we need to test.

support is where we can add our custom commands. We can import them later and use them in our test.

cypress.config.js is where we can dynamically modify configuration values and environment variables from our Cypress configuration.


More great articles from LogRocket:


Cypress automatically closes the browser once the test is executed. We don’t explicitly need to close the browser window.

A full, working code repository can be found on my GitHub link here.

Commands and assertions in Cypress

Commands and assertions are important concepts in Cypress, so let’s go over them before we continue.

First, we have commands. Cypress provides an API and methods to interact with the UI of the application. The commands available to us will have an inbuilt method that we can invoke in our test blocks. Essentially, we can simulate a user trying to perform an operation that is used to create an assertion.

In the example snippet below, we use a get command to get an element. We then use should and a chained assertion passed on to assert something we expect from the test.

it('sample testing ', () => {
 cy.visit('/')
 cy.get('#name').type('Elon Musk')
 cy.get('#password').type('programming-rocks')
 cy.get('#submit').click()
 cy.get('.messages--error').should('be.visible').and('contain', 'Unrecognized username or password. Forgot your password?')
})

Assertions are the checkpoints of our test block that confirm if an automated test has passed or failed. Cypress bundles the Chai, jQuery, and Sinon.JS libraries for assertions. They check the desired, expected application in which the test is running. A complete listing of assertions can be found in the documentation here.

Running Cypress in our application

It would be really handy if we could open Cypress from the command-line interface (CLI). And we can! To do this, we can add:

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

This’ll allow us to open a default Cypress iFrame where we can select the test files to execute. We can run this with the command npm run cypress.

Setting up our sample component

Now, we’ll begin testing a simple movie listing component we created. It takes in the name of a movie and adds it to our UI when we click a button. We also can click on the movie listing and mark it as seen, as well as dismiss the all-seen movies.

import { useState } from 'react';

function Movielist() {
 const [userInput, setUserInput] = useState('');
 const [movieList, setMovieList] = useState('');

 const handleChange = (e) => {...}

 const handleSubmit = (e) => {...}

 const handleClick = (e) => {...}

 const handleSeen = (e) => {...}

 return (
   <div className="wrapper">
     <form onSubmit={handleSubmit}>
       <input value={userInput} type="text" onChange={handleChange} placeholder="Movie wishlist" />
       <button>+Add movies</button>
     </form>
     <div className="movieList">
       {!movieList && <span data-cy='empty' className="empty">No movies here</span>}
       <ul data-cy='movie-list'>
         {movieList && movieList.map(m => {
           return (
             <li onClick={handleClick} className={m.seen ? "strike movie-list" : "movie-list"} id={m.id} key={m.id}>
               <span>
                 {m.movie}
            'X'
               </span>
             </li>
           )
         })}
       </ul>
     </div>
     {movieList.length > 0 && <><button data-cy='clear-movie' className="outline-btn" onClick={handleSeen}>Clear seen movies</button></>}
   </div>
 )
}

export default Movielist

With this component in place, we can start testing this component in isolation.

Writing unit tests in Cypress

Inside our component folder, we’ll create a new file called Movielist.cy.js. This will contain a simple test execution that visits the application we are running locally.

import Movielist from '../../src/components/Movielist';

describe('<Movielist>', () => {
 it('Mount the component', () => {
   cy.mount(<Movielist />);
 })
})

The above import statement is supported and we also can mount the functions for every supported framework from the Cypress package. If we run the command npm run cypress, we should see our component ready to test in a browser.

describe('<Movielist>', () => {
 it('Component mounts', () => {
   cy.mount(<Movielist />);
 })
 it('Component mounts', () => {
   cy.mount(<Movielist />);
   cy.get('[data-cy=empty]').contains('No movies here');
 })
})

We need to mount our component in each it() block we create for our test. To do so, we can make use of the beforeEach() method provided to us. This’ll look cleaner and easier altogether:

describe('<Movielist>', () => {
 beforeEach(() => {
   cy.mount(<Movielist />);
 });
 it('Check no movies', () => {
   cy.get('[data-cy=empty]').contains('No movies here');
 })
})

This definitely looks better than before. Now, we’ll move ahead and add a test to check whether or not we can make the movie wishlist work.

describe('<Movielist>', () => {
 beforeEach(() => {
   cy.mount(<Movielist />);
 });

 it('The List of movies appends', () => {
   cy.get('[data-cy=empty]').contains('No movies here');
   const formInput = cy.get('form input');
   formInput.should('have.value', '');
   formInput.type('Monster Inc.')
     .should('have.value', 'Monster Inc.');
   cy.get('form button').click();
   formInput.clear();
   formInput.type('Circle of eight')
     .should('have.value', 'Circle of eight');
   cy.get('form button').click();
   cy.get('[data-cy=movie-list]').children().should('have.length', 2);
 })
 })

Up until this point, we have passed all the tests we’ve written and checked that we’ve achieved the behavior we expected from the components.

We’ll now test the last piece of functionality attached to this component that dismisses the seen movies with a Clear movie button. This sounds similar to something we have written before, let’s see what it looks like:

 it('uncheck movie', () => {
   const lastListitem = cy.get('[data-cy=movie-list]:nth-child(1) li:last-child');
   lastListitem.click();
   lastListitem.should('have.class', 'strike');
   cy.get('[data-cy=clear-movie]').click();
   cy.get('[data-cy=movie-list]').children().should('have.length', 1);
   cy.get('[data-cy=clear-movie]').click();
   cy.get('[data-cy=movie-list]').children().should('have.length', 1);
 })

We checked whether we can mark the movie as seen, which then adds a strike through the movie name. We cleared out the seen movies and tested the number of movies available to us in the interface. We’ve also tested the component we’ve written.

It’s always better to have ample code test coverage. You may have noticed that it looks just like a real click interaction and simulation in the Cypress UI test. This is also one of the major benefits of using Cypress: visible interfaces, time traveling, and many others.

Testing our components of the application is much faster than doing end-to-end testing. Our component tests perform similarly to component tests executed within Node-based runners, like Jest and Mocha.

Conclusion

And there we have it! We have successfully written a unit test with Cypress for a single component of our app. We can now ship fully working, bug-free code to our users.

Cypress is language agnostic so you can use it with any other programming language, not just JavaScript. With this, we have implemented fully automated testing that addresses the pain points of traditional testing tools. Cypress is a great choice for frontend developers and test automation engineers to write automated web tests.

You can learn more about Cypress in their official docs. They’re an excellent resource to learn more about Cypress and ways to use it.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ishan Manandhar Ishan is a passionate product designer and frontend developer. He likes learning and implementing new tech stacks. He frequently writes blogs and also runs his YouTube channel, For Those Who Code.

Leave a Reply