Cypress Tutorial for Beginners: Write Your First End-to-End Test
Go from an empty folder to a running browser test. This Cypress tutorial covers installation, your first test, stable selectors, smart waiting, API stubbing and headless runs.
- Cypress is a JavaScript end-to-end testing tool that runs inside the browser and retries assertions automatically.
- Install it with
npm install cypress --save-devand open it withnpx cypress open. - Prefer
data-cyattributes over CSS classes, and never use fixedcy.wait(5000)sleeps. - Use
cy.interceptto control API responses andnpx cypress runto run headless in CI.
Selenium made browser testing possible, but many teams find it slow to set up and flaky to maintain. Cypress was built to make the same job simpler for web developers: one install, a visual test runner and automatic waiting.
This Cypress tutorial for beginners assumes you know basic JavaScript. If not, start with our JavaScript tutorial for beginners.
What Cypress is
Cypress is an open-source end-to-end test tool for web apps. Your tests are written in JavaScript or TypeScript and run in the same browser as your app, so Cypress can watch every network call and DOM change. It retries many commands and assertions until they pass or a timeout is reached, which removes most manual waiting.
Install and set up
You need Node.js. Then create a project and add Cypress.
mkdir cypress-demo && cd cypress-demo
npm init -y
npm install cypress --save-dev
npx cypress open
The first launch opens the Cypress app. Choose E2E Testing, pick a browser and let it create the default files. You get a cypress.config.js file and a cypress/ folder with e2e for your specs and support for shared commands. Test files end in .cy.js.
Your first test
Create cypress/e2e/first.cy.js. This test uses Cypress’s own practice site, so it works anywhere.
describe("Cypress practice site", () => {
it("opens the Actions page and types in a field", () => {
cy.visit("https://example.cypress.io");
cy.contains("type").click();
cy.url().should("include", "/commands/actions");
cy.get(".action-email")
.type("hello@stackquora.com")
.should("have.value", "hello@stackquora.com");
});
});
Read it as a story: visit the site, click the link that contains the word “type”, check that the URL changed, then type into the email field and check its value. describe groups tests and it defines one test. Click the spec in the Cypress app and watch it run, with a snapshot for every step.
Choosing stable selectors
A test is only as stable as its selectors. CSS classes change for styling reasons, so tests that depend on them break. Add dedicated test attributes to your HTML.
<button data-cy="submit-order">Place order</button>
cy.get("[data-cy=submit-order]").click();
| Selector | Stability |
|---|---|
[data-cy=submit-order] |
Best: exists only for tests |
Visible text via cy.contains() |
Good for user-facing labels |
#order-form (an ID) |
Fair: can change with refactors |
.btn.btn-primary.large |
Poor: tied to styling |
Waiting the right way
Because Cypress retries, you almost never need a sleep. Write the assertion and let it wait.
// Bad: always waits 5 seconds, still can fail
cy.wait(5000);
// Good: retries until the element appears (default 4 seconds)
cy.get("[data-cy=result]").should("be.visible");
Compare this with explicit waits in older tools in our guide to Selenium waits. For slow pages, raise the timeout on one command, for example cy.get(".chart", { timeout: 10000 }).
Stubbing API calls with cy.intercept
End-to-end tests become fast and reliable when you control the data. cy.intercept catches a request and can answer it with a fixture file.
cy.intercept("GET", "/api/users", { fixture: "users.json" }).as("getUsers");
cy.visit("/users");
cy.wait("@getUsers");
cy.get("[data-cy=user-row]").should("have.length", 3);
Here cy.wait("@getUsers") waits for the named request, which is the correct kind of wait. Put users.json in cypress/fixtures. Check the real API separately with API testing with Postman.
Run headless and in CI
Set a base URL once so tests can use short paths.
// cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: { baseUrl: "http://localhost:3000" },
});
Then run everything without the UI.
npx cypress run
npx cypress run --browser chrome --spec "cypress/e2e/first.cy.js"
cypress run exits with a failure code when a test fails, so a CI server can stop the build. See how to wire it up in our GitHub Actions CI/CD guide.
Limits to know before you commit
- Tests are written in JavaScript or TypeScript only.
- Cypress controls one browser tab per test and has constraints with multiple origins and multiple windows.
- It is aimed at web apps, not native mobile apps.
If these matter to you, compare tools in Playwright vs Cypress, and follow our end-to-end testing best practices to keep your suite stable.
Final thoughts
Start with one valuable user journey, such as signing in or checking out. Use data-cy selectors, let Cypress retry, stub the slow parts and run it on every pull request.
Frequently asked questions
Is Cypress good for beginners?
Yes. Setup is a single npm install, the test runner shows each step visually, and automatic waiting removes a lot of the flakiness beginners meet in other tools.
Which language does Cypress use?
Cypress tests are written in JavaScript or TypeScript.
Do I need to install a browser driver for Cypress?
No. Cypress works with the browsers installed on your computer, and includes Electron, so there is no separate driver to download.
What is the difference between cypress open and cypress run?
cypress open starts the interactive app for writing and debugging tests. cypress run runs tests headless in the terminal, which is what you use in CI.
Can Cypress test APIs?
Yes. cy.request sends HTTP requests directly, and cy.intercept lets you spy on or stub the requests your app makes.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.