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.
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.
In general, these are the biggest benefits of testing with Cypress that I’ve found so far:
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.
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 vite@latest
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.
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 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.
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.
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.