Real World Testing with Cypress (2024)

Installing Cypress

Now that our store is up and running, we are going to install Cypress so that we can write end-to-end tests to test our Next.js front-end.

You can find our more about Cypress and how to install it on our docs site.

cd into the site/ directory and then install Cypress.

cd siteyarn add cypress --dev

Open up the package.json file and add the following scripts.

"scripts": { // ... "cypress:open": "cypress open", "cypress:run": "cypress run"}

The entire "scripts" object should look like this:

"scripts": { "dev": "next dev", "build": "next build", "start": "next start", "analyze": "BUNDLE_ANALYZE=both next build", "lint": "next lint", "prettier-fix": "prettier --write .", "find:unused": "npx next-unused", "cypress:open": "cypress open", "cypress:run": "cypress run" },

Make sure you have the Next.js development server running in another terminal window or tab.

yarn dev

Next, run the cypress:open command to launch Cypress in another terminal window or tab.

yarn cypress:open

Cypress should launch and look like this:

Real World Testing with Cypress (1)

Cypress by default creates several example spec files for demonstration purposes. In the blue alert at the top click on the "No thanks, delete example files" link. This will delete all of the default spec files that Cypress installs upon first launching it.

Writing Our First Test

Create a new spec file within cypress/integration and called it home.spec.js

Real World Testing with Cypress (2)

Next we will create a describe() method like so:

describe("Home Page", () => {})

Now that our spec file is setup, we can begin to write our first test.

Before we do that however, let's take a minute to think through what we need to test. We should be asking ourselves some questions like, "What are the most important features of this application?" In the case of our dev swag store, one of the most important features is the ability to purchase swag. Before we can make a purchase, however, our products need to be displayed in our store from Shopify.

Let's write a test that confirms that all of our products are being displayed in our store.

Update home.spec.js with the following:

describe("Home Page", () => { it("displays all 3 products on the home page", () => {})})

The first thing we need to do in our test is tell Cypress to navigate to the homepage of our application.

describe("Home Page", () => { it("displays all 3 products on the home page", () => { cy.visit("http://localhost:3000") })})

Next, we will need to use cy.get() to get our products. Before we do that however, let's learn a Cypress best practice.

Testing Specific Attributes

Most people will typically use a CSS class or ID to get their elements like so:

cy.get(".btn-large")cy.get("#hero")

While this is perfectly valid and will work, we do not actually recommend this. Why not? You see by using CSS classes or ID's you are tying your tests to things that are most likely going to change overtime. Classes and ID's are for design and layout, which are prone to change. If someone changes a class name or id your tests are going to break. To help make your tests less brittle and more future-proof we recommend you create special data attributes on your elements specifically for testing purposes.

We will be using the data-test attribute on our elements, like so:

Now that we have that covered, let's update one of the components displayed on our homepage with a data-test attribute. We want to confirm that the title and price being displayed on the homepage is correct. This data is being displayed by the ProductTag.tsx component.

Open that component up in your editor and paste the following.

import cn from 'clsx'import { inherits } from 'util'import s from './ProductTag.module.css'interface ProductTagProps { className?: string name: string price: string fontSize?: number}const ProductTag: React.FC<ProductTagProps> = ({ name, price, className = '', fontSize = 32,}) => { return ( <div className={cn(s.root, className)} data-test="product-tag"> <h3 className={s.name}> <span className={cn({ [s.fontsizing]: fontSize < 32 })} style={{ fontSize: `${fontSize}px`, lineHeight: `${fontSize}px`, }} data-test="product-name" > {name} </span> </h3> <div className={s.price} data-test="product-price"> {price} </div> </div> )}export default ProductTag

Notice how we have added three of these data-test attributes:

<div className={cn(s.root, className)} data-test="product-tag">
<span className={cn({ [s.fontsizing]: fontSize < 32 })} style={{ fontSize: `${fontSize}px`, lineHeight: `${fontSize}px`, }} data-test="product-name"> {name}</span>
<div className={s.price} data-test="product-price"> {price}</div>

Now that we have these in place, let's continue writing our test.

Testing Our Products

Update the home.spec.js file with the following:

describe("Home Page", () => { it("displays all 3 products on the home page", () => { cy.visit("http://localhost:3000") cy.get('[data-test="product-tag"]') })})

Now let's run Cypress to make sure everything is working so far.

Real World Testing with Cypress (3)

Click on the home.spec.js file to launch Cypress and run our test.

Real World Testing with Cypress (4)

So far so good.

We currently have three products and so when we use cy.get('[data-test="product-tag"]') Cypress will return all of our products. We only want the first one, so we can use the cy.eq() to grab it.

cy.get('[data-test="product-tag"]').eq(0)

Then, we will want to get the product name and product price to confirm they are displaying correctly within this element, like so:

describe("Home Page", () => { it("displays all 3 products on the home page", () => { cy.visit("http://localhost:3000") cy.get('[data-test="product-tag"]') .eq(0) .within(() => { cy.get('[data-test="product-name"]').should("contain", "Star Wars") cy.get('[data-test="product-price"]').should("contain", "$25.00 USD") }) })})

By using .within() we are limiting the scope of the subsequent cy.get() commands. Otherwise, cy.get() will search through the entire document. Since we know that the product name and price are children of the "product tag" we can limit the scope in which Cypress looks to find these elements.

Real World Testing with Cypress (5)

Now that we know our first product is displayed properly, let's confirm the other two. We can simply copy and past the first part of our test for the first product and modify the product name and title for the other products.

describe("Home Page", () => { it("displays all 3 products on the home page", () => { cy.visit("http://localhost:3000") cy.get('[data-test="product-tag"]') .eq(0) .within(() => { cy.get('[data-test="product-name"]').should("contain", "Star Wars") cy.get('[data-test="product-price"]').should("contain", "$25.00 USD") }) cy.get('[data-test="product-tag"]') .eq(1) .within(() => { cy.get('[data-test="product-name"]').should("contain", "SQL") cy.get('[data-test="product-price"]').should("contain", "$25.00 USD") }) cy.get('[data-test="product-tag"]') .eq(2) .within(() => { cy.get('[data-test="product-name"]').should("contain", "Code") cy.get('[data-test="product-price"]').should("contain", "$25.00 USD") }) })})

Real World Testing with Cypress (6)

Great all of our products are displaying the correct name and price!

Testing the Header

Let's now write some tests for the header of our application. We will write some tests to make sure that the links go to the correct pages and the search bar returns the correct results.

First, create a new spec file in cypress/integration called header.spec.js and add the following.

describe("Header", () => {})

Real World Testing with Cypress (7)

First, let's write a test to make sure that the links in the header go to the correct pages.

describe("Header", () => { it("links to the correct pages", () => {})})

We can add our data-test attributes to these links within the components/common/Navbar/Navbar.tsx component.

import { FC } from 'react'import Link from 'next/link'import s from './Navbar.module.css'import NavbarRoot from './NavbarRoot'import { Logo, Container } from '@components/ui'import { Searchbar, UserNav } from '@components/common'interface Link { href: string label: string}interface NavbarProps { links?: Link[]}const Navbar: FC<NavbarProps> = ({ links }) => ( <NavbarRoot> <Container clean className="mx-auto max-w-8xl px-6"> <div className={s.nav}> <div className="flex items-center flex-1"> <Link href="/"> <a className={s.logo} aria-label="Logo" data-test="logo"> <Logo /> </a> </Link> <nav className={s.navMenu}> <Link href="/search"> <a className={s.link} data-test="nav-link-search"> All </a> </Link> {links?.map((l) => ( <Link href={l.href} key={l.href}> <a className={s.link} data-test="nav-link-home-page"> {l.label} </a> </Link> ))} </nav> </div> {process.env.COMMERCE_SEARCH_ENABLED && ( <div className="justify-center flex-1 hidden lg:flex"> <Searchbar /> </div> )} <div className="flex items-center justify-end flex-1 space-x-8"> <UserNav /> </div> </div> {process.env.COMMERCE_SEARCH_ENABLED && ( <div className="flex pb-4 lg:px-6 lg:hidden"> <Searchbar id="mobile-search" /> </div> )} </Container> </NavbarRoot>)export default Navbar

First, let's confirm that clicking on the store logo will go to the home page.

describe("Header", () => { it("links to the correct pages", () => { cy.visit("http://localhost:3000") cy.get('[data-test="logo"]').click() cy.location("pathname").should("eq", "/") })})

Real World Testing with Cypress (8)

baseUrl and Cypress Custom Commands

Instead of having to write cy.visit('http://localhost:3000') to tell Cypress to go to the home page, we can tell Cypress our applications baseUrl and then simply use cy.visit('/') instead.

Within the cypress.json file in the root of the repo, add the following:

{ "baseUrl": "http://localhost:3000"}

Real World Testing with Cypress (9)

Now you can update our test like so:

describe("Header", () => { it("links to the correct pages", () => { cy.visit("/") cy.get('[data-test="logo"]').click() cy.location("pathname").should("eq", "/") })})

Our test is still passing

Real World Testing with Cypress (10)

Next, instead of having to write cy.get('[data-test="logo"]') we can simplify this as well with a custom Cypress command.

Add the following to the cypress/support/commands.js file.

Cypress.Commands.add("getBySel", (selector, ...args) => { return cy.get(`[data-test=${selector}]`, ...args)})

Real World Testing with Cypress (11)

Now we can update our test like so:

describe("Header", () => { it("links to the correct pages", () => { cy.visit("/") cy.getBySel("logo").click() cy.location("pathname").should("eq", "/") })})

And our test is still passing.

Real World Testing with Cypress (12)

Now let's do the same for the other links in the header.

describe("Header", () => { it("links to the correct pages", () => { cy.visit("/") cy.getBySel("logo").click() cy.location("pathname").should("eq", "/") cy.getBySel("nav-link-search").click() cy.location("pathname").should("eq", "/search") cy.getBySel("nav-link-home-page").click() cy.location("pathname").should("eq", "/search/frontpage") })})

When we re-run our test we see that it is failing, why?

Real World Testing with Cypress (13)

If we look closely at our application, we can see that those links do not appear in the header. This is because Cypress by default opens the viewport to "1000x660" which is too narrow. One of the breakpoints in our Next.js application is hiding these links when it is 1000px wide. We can modify the default viewport size by adding the following to the cypress.json file.

{ "baseUrl": "http://localhost:3000", "viewportHeight": 1000, "viewportWidth": 1280}

Now our test should be passing

Real World Testing with Cypress (14)

Testing the Search Bar

Let's write a test to make sure that the search bar in the header returns the correct results

Add the following new test inside of header.spec.js

describe("Header", () => { it("links to the correct pages", () => { cy.visit("/") cy.getBySel("logo").click() cy.location("pathname").should("eq", "/") cy.getBySel("nav-link-search").click() cy.location("pathname").should("eq", "/search") cy.getBySel("nav-link-home-page").click() cy.location("pathname").should("eq", "/search/frontpage") }) it("the search bar returns the correct search results", () => { cy.visit("/") })})

beforeEach() hooks

You will notice that within each test, we have to specifically tell Cypress where to navigate in our app before our test can be executed. We can remove this duplication by using a beforeEach() hook. This hook will run any code we put inside of it before each test is run, hence the name beforeEach()

describe("Header", () => { beforeEach(() => { cy.visit("/") }) it("links to the correct pages", () => { cy.getBySel("logo").click() cy.location("pathname").should("eq", "/") cy.getBySel("nav-link-search").click() cy.location("pathname").should("eq", "/search") cy.getBySel("nav-link-home-page").click() cy.location("pathname").should("eq", "/search/frontpage") }) it("the search bar returns the correct search results", () => {})})

Now Cypress will visit the home page before each and every test in this file.

Within components/common/Searchbar/Searchbar.tsx update the <input> element with the following data-test attribute data-test="search-input":

import { FC, memo, useEffect } from 'react'import cn from 'clsx'import s from './Searchbar.module.css'import { useRouter } from 'next/router'interface Props { className?: string id?: string}const Searchbar: FC<Props> = ({ className, id = 'search' }) => { const router = useRouter() useEffect(() => { router.prefetch('/search') }, [router]) const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => { e.preventDefault() if (e.key === 'Enter') { const q = e.currentTarget.value router.push( { pathname: `/search`, query: q ? { q } : {}, }, undefined, { shallow: true } ) } } return ( <div className={cn(s.root, className)}> <label className="hidden" htmlFor={id}> Search </label> <input id={id} className={s.input} placeholder="Search for products..." defaultValue={router.query.q} onKeyUp={handleKeyUp} data-test="search-input" /> <div className={s.iconContainer}> <svg className={s.icon} fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" clipRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" /> </svg> </div> </div> )}export default memo(Searchbar)

Now we can .get() the search bar like so:

// header.spec.jsit('links to the correct pages', () => {// ...it('the search bar returns the correct search results', () => { cy.getBySel('search-input').eq(0) })

tip

This search component is actually in the DOM twice, one for desktop and one for mobile. By using .eq(0) we are grabbing the one for desktop.

only()

Since, we now have two tests in this file, Cypress will always run both tests each time we save. We can use .only() to have it only run a single one like so:

it.only("the search bar returns the correct search results", () => { cy.getBySel("search-input").eq(0)})

Real World Testing with Cypress (15)

Let's try to search for the "Star Wars" by typing in the word "star"

it.only("the search bar returns the correct search results", () => { cy.getBySel("search-input").eq(0).type("star")})

We can also simulate pressing the enter key like so:

it.only("the search bar returns the correct search results", () => { cy.getBySel("search-input").eq(0).type("star{enter}")})

Real World Testing with Cypress (16)

Now let's confirm that the correct product is displayed in the search results. We can borrow some of the same code we used in our home.spec.js test.

it.only("the search bar returns the correct search results", () => { cy.getBySel("search-input").eq(0).type("star{enter}") cy.get('[data-test="product-tag"]').within(() => { cy.get('[data-test="product-name"]').should("contain", "Star Wars") cy.get('[data-test="product-price"]').should("contain", "$25.00 USD") })})

Real World Testing with Cypress (17)

Let's write one more test that confirms that our shopping cart is working.

Create a new file within cypress/integration called shopping-cart.spec.js and add the following:

describe("Shopping Cart", () => { it("users can add products to the cart", () => { cy.visit("/") })})

Real World Testing with Cypress (18)

First, we will need to click on a product to add it to the cart.

describe("Shopping Cart", () => { it("users can add products to the cart", () => { cy.visit("/") cy.getBySel("product-tag").eq(0).click() })})

Real World Testing with Cypress (19)

Next, we will need to click on the "Add to Cart" button.

describe("Shopping Cart", () => { it("users can add products to the cart", () => { cy.visit("/") cy.getBySel("product-tag").eq(0).click() cy.get('[aria-label="Add to Cart"]').click() })})

Notice how we are getting the element by using the aria-label attribute.

Real World Testing with Cypress (20)

Now we just need to confirm that our shopping cart has our item in it.

describe("Shopping Cart", () => { it("users can add products to the cart", () => { cy.visit("/") cy.getBySel("product-tag").eq(0).click() cy.get('[aria-label="Add to Cart"]').click() cy.get('[aria-label="Cart items: 1"]').contains("1") })})

Real World Testing with Cypress (21)

Conclusion

In this lesson, we learned how to install Cypress and how to write several different tests to ensure that our store is working as expected. We also learned how to customize the Cypress test runner within the cypress.json file as well has how to use custom Cypress commands.

Practice

Now would be a great time for you to practice writing more Cypress tests. Here are some ideas for tests you could write.

  • Write a test that confirms the shopping cart can contain multiple items.
  • Write a test that confirms your images have the correct alt tags.
  • Write a test that confirms the “Accept Cookies” popup displays on the bottom of the page.
  • Write a test that confirms the language selector in the footer, navigates to the correct page.
Real World Testing with Cypress (2024)

References

Top Articles
Latest Posts
Article information

Author: Nathanial Hackett

Last Updated:

Views: 6120

Rating: 4.1 / 5 (52 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Nathanial Hackett

Birthday: 1997-10-09

Address: Apt. 935 264 Abshire Canyon, South Nerissachester, NM 01800

Phone: +9752624861224

Job: Forward Technology Assistant

Hobby: Listening to music, Shopping, Vacation, Baton twirling, Flower arranging, Blacksmithing, Do it yourself

Introduction: My name is Nathanial Hackett, I am a lovely, curious, smiling, lively, thoughtful, courageous, lively person who loves writing and wants to share my knowledge and understanding with you.