You have Cypress installed, scaffolded, and a sanity test passing. This lesson is the first one with real, runnable code against a real app — three tests for an e-commerce product page, a tour of how describe, it, and beforeEach glue them together, and the two ways to run the suite. Every snippet is meant to be typed (or pasted) into your scaffolded project and run.
Anatomy of a Cypress test
Open cypress/e2e/home.cy.ts and write:
describe("Home page", () => {
it("should display the welcome message", () => {
cy.visit("/");
cy.get("h1").should("contain", "Welcome");
});
});Save. Cypress's runner reloads the spec list and the test runs in seconds. Read the file from the outside in:
describe("Home page", () => { ... })is a test suite — a labelled group of related tests. Cypress inherits this from Mocha. The string is what shows up in the runner UI and the CI output.it("should display the welcome message", () => { ... })is a single test case. Eachitis independent: it gets its own browser tab, its own clean cookie jar, its own clean local storage. The string is the assertion-friendly description that explains what you're checking.cy.visit("/")navigates the browser to a URL. BecausebaseUrlis set incypress.config.ts,"/"resolves tohttp://localhost:3000/(or whatever you configured).cy.get("h1")selects the first<h1>element on the page. Like every Cypress query, it auto-retries for up to four seconds while the DOM settles..should("contain", "Welcome")is the assertion — it keeps re-checking until the<h1>contains the text "Welcome" or the timeout fires.shouldis built on Chai under the hood.
Three commands, one assertion, one passing test. That's the whole shape.
Running tests two ways
Interactive mode — for authoring and debugging:
npx cypress openThe desktop app launches, you click your spec in the file list, and the test runs in a real visible browser. Every command is logged on the left. Save the file in VS Code and the runner re-executes the spec automatically. This is your day-to-day surface.
Headless mode — for CI and bulk execution:
npx cypress runCypress runs every spec in cypress/e2e/ in a headless browser, prints a per-spec summary table to stdout, and exits with code 0 (pass) or non-zero (fail). On every failure it captures a screenshot in cypress/screenshots/; on every spec it captures a video in cypress/videos/. CI pipelines use this exact command.
To run a specific spec:
npx cypress run --spec "cypress/e2e/home.cy.ts"To run in a specific browser:
npx cypress run --browser chromeIf you added the npm scripts from lesson 2, npm run cy:open and npm run cy:run are the shortcuts you'll actually type.
A real multi-test spec
A single test rarely tells you anything useful. Real specs cluster three or four related tests under one describe block and share their setup with beforeEach:
describe("Product search", () => {
beforeEach(() => {
cy.visit("/products");
});
it("should display products on the page", () => {
cy.get("[data-testid='product-card']")
.should("have.length.greaterThan", 0);
});
it("should filter products by category", () => {
cy.get("[data-testid='category-filter']").select("Electronics");
cy.get("[data-testid='product-card']").each(($card) => {
cy.wrap($card).should("contain", "Electronics");
});
});
it("should search for a product by name", () => {
cy.get("[data-testid='search-input']").type("Laptop");
cy.get("[data-testid='product-card']")
.should("have.length.greaterThan", 0);
cy.get("[data-testid='product-card']")
.first()
.should("contain", "Laptop");
});
});Save it as cypress/e2e/products.cy.ts. Three tests, each independent, each starting from a fresh navigation to /products. Walk through what's new:
beforeEach(() => cy.visit("/products"))runs before everyit()block. Use it for shared setup so the tests stay focused on what they're actually verifying. You'll see this pattern everywhere in real codebases.cy.get("[data-testid='product-card']")matches every element with thatdata-testid. Cypress chains queries, so the result is a collection you can keep working with..should("have.length.greaterThan", 0)is a Chai assertion in Cypress'sshouldsyntax — the dot path defines the assertion. There are dozens; the Cypress commands cheat sheet lists them..each(($card) => cy.wrap($card).should("contain", "Electronics"))iterates the matched collection.$cardis a jQuery-wrapped element;cy.wrap($card)rewraps it as a Cypress chainable so you can keep using.should. Every card on a filtered page should mention the category..first()narrows a multi-element selection down to the first match — the top result of the search.
Run this spec against any app with [data-testid] attributes on its product page. If your app doesn't have them, swap in selectors that match what's there — but stay disciplined: chapter 2 covers why data-testid is the only selector you should rely on long-term.
Test-execution flow
The two things to internalise: beforeEach runs before every it, not just before the first one. And every it starts from a clean browser session — Cypress wipes cookies, local storage, and session storage between tests by default, so one test can't accidentally pollute the next.
What if there's no real app to test against?
The spec above assumes a running e-commerce site at baseUrl. If you don't have one yet, three options:
- The Cypress example app — set
baseUrl: "https://example.cypress.io"and write tests against the public demo. Useful for syntax practice. - A public sandbox like
https://automationexerciseweb.comorhttps://www.saucedemo.com— both designed for automation practice. - Your own app running locally on
http://localhost:3000— the realistic scenario. This is what every real QA job looks like.
For the rest of this course, we'll assume an e-commerce target with [data-testid] attributes wherever it matters. If you're following along against a different app, the patterns transfer; just substitute selectors.
Hooks beyond beforeEach
beforeEach is the workhorse, but the full set is:
before()— runs once at the start of thedescribe, before anyit. Good for one-time expensive setup (seeding a test database).beforeEach()— runs before everyit. Good for navigation and per-test setup.afterEach()— runs after everyit, even if it failed. Good for cleanup that must always happen.after()— runs once at the end of thedescribe, after everyit. Rare; mostly for teardown of one-time setup.
Don't reach for before() to set up state your tests will mutate — Cypress doesn't reset between it blocks the way Jest does for module state, and you'll create flake-by-design. beforeEach is the safe default.
⚠️ Common mistakes
- Putting setup in the test body instead of
beforeEach. Three tests, three copy-pastedcy.visit("/products")lines at the top. Now you change the URL and have to update three places — or worse, miss one and create a bug.beforeEachexists precisely to centralise navigation. Use it from day one. - Selecting on volatile attributes (CSS class, nth-child, autogenerated IDs) instead of
data-testid. A test likecy.get(".btn-primary.large")breaks the moment a designer renames a utility class.cy.get("[data-testid='submit-order']")survives every CSS refactor your team will ever do. Chapter 2 covers this in depth, but start the habit now — every example in this course usesdata-testid. - Skipping
cy.visitand assuming the previous test's URL is still loaded. Cypress wipes the browser betweenitblocks. Without acy.visitinbeforeEach(or at the top of each test),cy.getruns againstabout:blankand fails with "no element found." This is one of the most common confusions for new Cypress users.
🎯 Practice task
Author and run a multi-test spec end to end. 25-30 minutes.
- In your scaffolded project, set
baseUrl: "https://www.saucedemo.com"incypress.config.ts. (Sauce Demo is a free public e-commerce sandbox designed for automation practice — credentialsstandard_user/secret_sauce.) - Create
cypress/e2e/login.cy.tswith adescribeblock named "Login" and threeittests:- "loads the login page" —
cy.visit("/")and assert the URL contains/. - "logs in with valid credentials" — type the user/password, click the login button, assert the URL contains
/inventory.html. - "shows an error for invalid credentials" — type the user, type "wrong" as the password, click login, assert that the error container is visible and contains "Username and password do not match".
- "loads the login page" —
- Move the
cy.visit("/")into abeforeEach. Confirm all three tests still pass. - Use
npm run cy:run --silent -- --spec "cypress/e2e/login.cy.ts"to run only this spec from the CLI. Inspect the screenshot/video output incypress/screenshots/(only on failure) andcypress/videos/. - Force a failure to see Cypress's debugging in action. Change the password assertion to expect "Welcome, Admin" (a string that won't appear). Re-run. Read the assertion-failure message and inspect the auto-captured screenshot.
- Stretch: add a fourth test that logs in, clicks "Add to cart" on one product, and asserts the cart badge shows "1". You'll use
cy.contains("Add to cart").first().click()andcy.get("[data-test='shopping-cart-badge']").should("have.text", "1"). This is your first complete user-flow test — the kind every product team has dozens of.
Once this works headlessly through cy:run and interactively through cy:open, you've completed the loop a real Cypress engineer runs daily. The next lesson breaks open the Test Runner UI itself — the Selector Playground, Time Travel, and the command log that turn a failed test into a five-second diagnosis.