Day 2: Writing Your First Test
What You'll Learn Today
- Basic test file structure (describe, it, before, beforeEach)
- Opening pages with cy.visit()
- Selecting elements with cy.get()
- Searching for text with cy.contains()
- Basic assertions (should, expect)
- Running tests (cypress open, cypress run)
- Hands-on: E2E testing a Todo app
Test File Structure
Cypress test files are placed in the cypress/e2e/ directory with a .cy.js (or .cy.ts) extension.
// cypress/e2e/sample.cy.js
describe('Test Suite Name', () => {
before(() => {
// Runs once before the entire test suite
})
beforeEach(() => {
// Runs before each test
})
afterEach(() => {
// Runs after each test
})
after(() => {
// Runs once after the entire test suite
})
it('test case name', () => {
// Write test code here
})
it('another test case', () => {
// Another test
})
})
Role of Each Block
flowchart TB
subgraph Lifecycle["Test Lifecycle"]
B["before()\nRuns once"]
BE1["beforeEach()\nRuns every time"]
T1["it('Test 1')"]
AE1["afterEach()\nRuns every time"]
BE2["beforeEach()\nRuns every time"]
T2["it('Test 2')"]
AE2["afterEach()\nRuns every time"]
A["after()\nRuns once"]
end
B --> BE1 --> T1 --> AE1 --> BE2 --> T2 --> AE2 --> A
style B fill:#8b5cf6,color:#fff
style A fill:#8b5cf6,color:#fff
style BE1 fill:#3b82f6,color:#fff
style BE2 fill:#3b82f6,color:#fff
style AE1 fill:#f59e0b,color:#fff
style AE2 fill:#f59e0b,color:#fff
style T1 fill:#22c55e,color:#fff
style T2 fill:#22c55e,color:#fff
| Block | When It Runs | Common Use |
|---|---|---|
before() |
Once at the start of the suite | Database initialization, shared data setup |
beforeEach() |
Before each test | Navigate to page, reset state |
it() |
The test itself | Actual test logic |
afterEach() |
After each test | Cleanup tasks |
after() |
Once at the end of the suite | Final cleanup |
Nesting describe Blocks
You can nest describe blocks to logically group your tests.
describe('User Management', () => {
describe('Login', () => {
it('can log in with valid credentials', () => {
// ...
})
it('shows an error with an incorrect password', () => {
// ...
})
})
describe('Registration', () => {
it('can create a new user', () => {
// ...
})
})
})
Opening Pages with cy.visit()
The first step in any test is opening the page under test. Use cy.visit() for this.
// Specify an absolute URL
cy.visit('http://localhost:3000')
// If baseUrl is configured, you can use relative paths
// With baseUrl: 'http://localhost:3000' in cypress.config.js
cy.visit('/') // http://localhost:3000/
cy.visit('/about') // http://localhost:3000/about
cy.visit('/login') // http://localhost:3000/login
visit() Options
cy.visit('/', {
// Timeout (milliseconds)
timeout: 30000,
// Allow non-2xx status codes
failOnStatusCode: false,
// Basic authentication
auth: {
username: 'admin',
password: 'password123',
},
// Query parameters
qs: {
page: 1,
sort: 'name',
},
})
Common Pattern: Opening a Page in beforeEach
describe('Home Page', () => {
beforeEach(() => {
// Open the home page before each test
cy.visit('/')
})
it('displays the title', () => {
// Page is already open β run your test
})
it('displays the navigation', () => {
// Page is already open β run your test
})
})
Selecting Elements with cy.get()
cy.get() retrieves DOM elements using CSS selectors. It is one of the most frequently used commands in Cypress.
// Select by ID
cy.get('#submit-button')
// Select by class name
cy.get('.nav-link')
// Select by tag name
cy.get('h1')
// Select by attribute
cy.get('[type="email"]')
// Select by data attribute (recommended)
cy.get('[data-testid="login-form"]')
// Compound selector
cy.get('form.login-form input[type="email"]')
Why Use data-testid?
flowchart TB
subgraph Bad["Selectors to Avoid"]
B1["cy.get('.btn-primary')\nBreaks when styles change"]
B2["cy.get('#main > div:nth-child(2)')\nBreaks when DOM structure changes"]
B3["cy.get('button')\nToo ambiguous"]
end
subgraph Good["Recommended Selectors"]
G1["cy.get('[data-testid=submit]')\nTest-specific attribute"]
G2["cy.get('[data-cy=submit]')\nCypress-recommended attribute"]
end
style Bad fill:#ef4444,color:#fff
style Good fill:#22c55e,color:#fff
| Selector Type | Stability | Reason |
|---|---|---|
data-testid / data-cy |
High | Test-specific. Won't break unless intentionally changed |
id |
Moderate | Unique, but may change during refactoring |
class |
Low | Easily breaks with CSS changes |
| DOM structure | Very low | Frequently breaks with UI changes |
Interacting with Elements
Once you've selected an element, you can perform various actions on it.
// Click
cy.get('[data-testid="submit-btn"]').click()
// Type text
cy.get('[data-testid="email-input"]').type('user@example.com')
// Clear input
cy.get('[data-testid="email-input"]').clear()
// Clear and type
cy.get('[data-testid="email-input"]').clear().type('new@example.com')
// Select from a dropdown
cy.get('[data-testid="country-select"]').select('United States')
// Check a checkbox
cy.get('[data-testid="agree-checkbox"]').check()
// Uncheck a checkbox
cy.get('[data-testid="agree-checkbox"]').uncheck()
Searching for Text with cy.contains()
cy.contains() finds elements that contain the specified text. It's useful for text-based element selection.
// Find an element containing text
cy.contains('Log in')
// Search within a specific tag
cy.contains('button', 'Log in')
// Regular expressions work too
cy.contains(/^Welcome/)
// Find and click
cy.contains('Submit').click()
Choosing Between cy.get() and cy.contains()
flowchart LR
subgraph GetCmd["cy.get()"]
G["Identify elements\nby CSS selector"]
end
subgraph ContainsCmd["cy.contains()"]
C["Identify elements\nby text content"]
end
subgraph UseCase["When to Use"]
U1["Form inputs β cy.get()"]
U2["Button labels β cy.contains()"]
U3["Error messages β cy.contains()"]
U4["data attributes β cy.get()"]
end
style GetCmd fill:#3b82f6,color:#fff
style ContainsCmd fill:#8b5cf6,color:#fff
style UseCase fill:#22c55e,color:#fff
| Method | When to Use | Example |
|---|---|---|
cy.get() |
When you want to target elements by attribute or selector | Form inputs, specific components |
cy.contains() |
When you want to find elements by their text content | Buttons, messages, links |
Basic Assertions
Assertions verify that test expectations are met. In Cypress, you primarily use .should().
Assertions with should()
// Verify an element is visible
cy.get('[data-testid="welcome"]').should('be.visible')
// Verify an element exists
cy.get('[data-testid="header"]').should('exist')
// Verify an element does not exist
cy.get('[data-testid="error"]').should('not.exist')
// Verify exact text content
cy.get('h1').should('have.text', 'Welcome')
// Verify text is contained
cy.get('.message').should('contain', 'Success')
// Verify a CSS class is present
cy.get('.alert').should('have.class', 'alert-success')
// Verify an attribute value
cy.get('a').should('have.attr', 'href', '/about')
// Verify an input value
cy.get('input').should('have.value', 'test@example.com')
// Verify the number of elements
cy.get('li').should('have.length', 5)
// Verify an element is disabled
cy.get('button').should('be.disabled')
// Verify an element is enabled
cy.get('button').should('not.be.disabled')
Chaining should()
You can chain multiple assertions together.
cy.get('[data-testid="status"]')
.should('be.visible')
.and('have.text', 'Complete')
.and('have.class', 'status-complete')
.and('have.css', 'color', 'rgb(34, 197, 94)')
Common Assertions Reference
| Assertion | Description | Example |
|---|---|---|
be.visible |
Element is visible | .should('be.visible') |
exist |
Element exists in the DOM | .should('exist') |
have.text |
Exact text match | .should('have.text', 'Hello') |
contain |
Contains text | .should('contain', 'Hello') |
have.value |
Input value matches | .should('have.value', 'test') |
have.length |
Number of elements matches | .should('have.length', 3) |
have.class |
Has a CSS class | .should('have.class', 'active') |
have.attr |
Has an attribute | .should('have.attr', 'href') |
be.disabled |
Element is disabled | .should('be.disabled') |
be.checked |
Checkbox is checked | .should('be.checked') |
Running Tests
Cypress provides two ways to run tests.
flowchart LR
subgraph Open["cypress open (GUI)"]
O1["Browser selection"]
O2["Test file selection"]
O3["Real-time execution"]
O4["Time-travel\ndebugging"]
end
subgraph Run["cypress run (CLI)"]
R1["Headless execution"]
R2["Runs all tests"]
R3["CI/CD friendly"]
R4["Auto-saves videos\n& screenshots"]
end
style Open fill:#3b82f6,color:#fff
style Run fill:#8b5cf6,color:#fff
cypress open (Interactive Mode)
# Launch the GUI
npx cypress open
- Ideal for writing and running tests during development
- Inspect each test step with DOM snapshots (time travel)
- Automatically re-runs when test files change
cypress run (Headless Mode)
# Run all tests
npx cypress run
# Run a specific file
npx cypress run --spec "cypress/e2e/todo.cy.js"
# Specify a browser
npx cypress run --browser chrome
# Record video
npx cypress run --config video=true
- Ideal for CI/CD pipelines
- Runs entirely from the command line β no GUI needed
- Outputs test results to the terminal
Adding Scripts to package.json
// package.json
{
"scripts": {
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:chrome": "cypress run --browser chrome"
}
}
Hands-On: E2E Testing a Todo App
Let's put everything we've learned into practice by writing E2E tests for a Todo app. We'll use the official Cypress sample app at https://example.cypress.io/todo.
Creating the Test File
// cypress/e2e/todo.cy.js
describe('Todo App', () => {
beforeEach(() => {
// Open the Todo app before each test
cy.visit('https://example.cypress.io/todo')
})
it('displays two default todos', () => {
// Verify the number of todo items
cy.get('.todo-list li').should('have.length', 2)
// Verify the text of each todo
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
})
it('can add a new todo', () => {
// Type a new todo and press Enter
cy.get('[data-test="new-todo"]').type('Buy groceries{enter}')
// Verify the list now has 3 items
cy.get('.todo-list li').should('have.length', 3)
// Verify the text of the newly added todo
cy.get('.todo-list li').last().should('have.text', 'Buy groceries')
})
it('can mark a todo as completed', () => {
// Click the checkbox on the first todo
cy.get('.todo-list li').first().find('.toggle').check()
// Verify the completed class is applied
cy.get('.todo-list li').first().should('have.class', 'completed')
})
describe('Filtering', () => {
beforeEach(() => {
// Mark one todo as completed before each test
cy.get('.todo-list li').first().find('.toggle').check()
})
it('can show only active todos', () => {
// Click the "Active" filter
cy.contains('Active').click()
// Only 1 active todo should be visible
cy.get('.todo-list li').should('have.length', 1)
cy.get('.todo-list li').first().should('have.text', 'Walk the dog')
})
it('can show only completed todos', () => {
// Click the "Completed" filter
cy.contains('Completed').click()
// Only 1 completed todo should be visible
cy.get('.todo-list li').should('have.length', 1)
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
})
it('can show all todos', () => {
// Click the "All" filter
cy.contains('All').click()
// All todos should be visible
cy.get('.todo-list li').should('have.length', 2)
})
})
})
Visualizing the Test Flow
flowchart TB
subgraph Test1["Test: Default Display"]
T1A["visit() opens the page"]
T1B["get() selects the todo list"]
T1C["should() verifies count"]
end
subgraph Test2["Test: Add Todo"]
T2A["visit() opens the page"]
T2B["get() selects the input"]
T2C["type() enters text"]
T2D["should() verifies count"]
end
subgraph Test3["Test: Complete Todo"]
T3A["visit() opens the page"]
T3B["get().find() selects checkbox"]
T3C["check() checks it"]
T3D["should() verifies state"]
end
T1A --> T1B --> T1C
T2A --> T2B --> T2C --> T2D
T3A --> T3B --> T3C --> T3D
style Test1 fill:#3b82f6,color:#fff
style Test2 fill:#22c55e,color:#fff
style Test3 fill:#8b5cf6,color:#fff
Reading Test Success and Failure Output
Successful Output
$ npx cypress run --spec "cypress/e2e/todo.cy.js"
Todo App
β displays two default todos (1250ms)
β can add a new todo (980ms)
β can mark a todo as completed (870ms)
Filtering
β can show only active todos (920ms)
β can show only completed todos (890ms)
β can show all todos (850ms)
6 passing (5.8s)
Failed Output
When a test fails, Cypress provides detailed error information.
1) Todo App
can add a new todo:
AssertionError: expected 2 to equal 3
+ expected - actual
-2
+3
at Context.eval (cypress/e2e/todo.cy.js:20:44)
Debugging a Failure
| Information | Description |
|---|---|
| Test name | Which test failed |
| Error message | What differed from expectations |
| File and line number | Where in the code the failure occurred |
| Screenshot | Browser screenshot at failure (saved to cypress/screenshots/) |
| Video | Recording of the test run (saved to cypress/videos/, if configured) |
Understanding Command Chains
Cypress commands are connected through chains, similar to a jQuery-like pattern.
cy.get('[data-testid="todo-form"]') // Select parent element
.find('input') // Find child element
.type('New todo') // Type text
.should('have.value', 'New todo') // Verify value
Parent Commands and Child Commands
flowchart LR
subgraph Parent["Parent Commands (Starting point)"]
P1["cy.visit()"]
P2["cy.get()"]
P3["cy.contains()"]
end
subgraph Child["Child Commands (Chained)"]
C1[".find()"]
C2[".click()"]
C3[".type()"]
C4[".should()"]
end
P2 --> C1
C1 --> C3
C3 --> C4
style Parent fill:#3b82f6,color:#fff
style Child fill:#22c55e,color:#fff
| Type | Description | Example |
|---|---|---|
| Parent command | Starting point of a chain. Begins with cy. |
cy.get(), cy.visit(), cy.contains() |
| Child command | Operates on the result of the previous command | .find(), .click(), .type(), .should() |
| Dual command | Can be used as either parent or child | cy.contains() / .contains() |
Summary
| Concept | Description |
|---|---|
| describe / it | Blocks that define test suites and test cases |
| beforeEach | Setup logic that runs before each test |
| cy.visit() | Opens a page at the specified URL |
| cy.get() | Selects DOM elements using CSS selectors |
| cy.contains() | Finds elements by their text content |
| .should() | Assertions that verify element state |
| cypress open | Runs tests in GUI mode for interactive debugging |
| cypress run | Runs tests in headless mode (for CI/CD) |
Key Points
- Use
beforeEachto set up preconditions for each test, keeping tests independent - Use
data-testidordata-cyselectors to make tests more resilient - The auto-retry behavior of
.should()handles asynchronous DOM changes gracefully
Practice Exercises
Exercise 1: Basic
Write Cypress code to perform the following:
- Open
http://localhost:3000 - Verify that the
h1tag contains the text "Welcome" - Click the button with
data-testid="login-btn"
Exercise 2: Applied
Structure the following test using describe and it blocks:
- Enter an email address and password into a login form
- Click the submit button
- Verify navigation to the dashboard page
- Verify that a welcome message is displayed
Challenge Exercise
Write additional tests for the official Cypress sample app (https://example.cypress.io/todo):
- Add 3 new todos
- Mark the second todo as completed
- Verify that the "Active" filter shows 4 active todos
- Verify that the "Completed" filter shows 1 completed todo
References
- Cypress Official - Writing Your First Test
- Cypress Official - Best Practices (Selectors)
- Cypress Official - Assertions
Next Up: In Day 3, we'll dive into element selection and manipulation. You'll master advanced selectors and DOM interaction commands.