Day 9: Debugging and Test Strategy
What You Will Learn Today
- Using cy.debug() and cy.pause()
- Debugging with DevTools Console
- Time Travel Debugging (Test Runner)
- Screenshots and Video Recording
- Test Retry Configuration
- Test Isolation Principles
- Proper Use of beforeEach / afterEach
- The Page Object Pattern
- Test Naming Conventions and File Organization
Debugging Fundamentals
When a test fails, quickly identifying the root cause is critical. Cypress comes with powerful built-in debugging tools to help you do just that.
flowchart TB
subgraph Tools["Cypress Debugging Tools"]
A["cy.debug()"]
B["cy.pause()"]
C["Time Travel"]
D["Screenshots"]
E["Video Recording"]
end
subgraph Flow["Debugging Flow"]
F["Test Failure"] --> G["Check Error Message"]
G --> H["Inspect State via Time Travel"]
H --> I["Pause with cy.pause()"]
I --> J["Investigate in DevTools"]
end
style Tools fill:#3b82f6,color:#fff
style Flow fill:#8b5cf6,color:#fff
cy.debug() and cy.pause()
cy.debug()
cy.debug() launches the browser's DevTools debugger during test execution. You can inspect the result of the previous command as subject.
cy.get('.user-name')
.debug() // Pauses in the DevTools debugger
.should('contain', 'John');
In the DevTools Console, type subject to inspect the element returned by cy.get().
cy.pause()
cy.pause() pauses test execution and allows you to step through commands manually.
cy.visit('/login');
cy.pause(); // Pauses here
cy.get('#username').type('testuser');
cy.pause(); // Check state after input
cy.get('#password').type('password123');
cy.get('#login-btn').click();
Use the "Resume" or "Next" button in the Test Runner to continue execution.
When to Use Each
| Method | Purpose | Pauses In |
|---|---|---|
cy.debug() |
Detailed inspection of elements and data | DevTools Debugger |
cy.pause() |
Step-by-step test execution | Test Runner UI |
Debugging with DevTools Console
Since the Cypress Test Runner runs in a Chromium-based browser, you have full access to DevTools.
Using Console Logs
cy.get('.item-list')
.then(($el) => {
// Log the jQuery element to the console
console.log('Element:', $el);
console.log('Text:', $el.text());
console.log('Length:', $el.length);
});
Test Logging with cy.log()
cy.log('--- Starting login test ---');
cy.get('#username').type('testuser');
cy.log('Entered username');
cy.get('#password').type('password123');
cy.log('Entered password');
cy.get('#login-btn').click();
cy.log('--- Clicked login button ---');
cy.log() output appears in the Test Runner's command log, making it easy to visually trace test execution.
Accessing Cypress Objects
You can access Cypress objects directly from the DevTools Console.
// Run these in the DevTools Console
Cypress.env() // View environment variables
Cypress.config() // View configuration
Cypress.spec // Current spec file info
Time Travel Debugging
One of Cypress's most powerful debugging features is Time Travel.
flowchart LR
subgraph Timeline["Command Log (Timeline)"]
C1["visit('/')"] --> C2["get('.btn')"] --> C3["click()"] --> C4["url()"] --> C5["should('include')"]
end
C3 -->|"Click to view<br/>DOM snapshot"| S["DOM Snapshot"]
style Timeline fill:#22c55e,color:#fff
style S fill:#f59e0b,color:#000
How to Use It
- Run a test
- Click any command in the command log on the left side of the Test Runner
- The DOM state at the time that command executed is displayed in the preview
- Toggle Before/After to see changes before and after the command
Pinning
Clicking a command "pins" it, freezing the DOM at that point in time. You can then use the DevTools Elements panel to inspect the DOM structure in detail.
// You can inspect the DOM at each command's execution point
cy.visit('/dashboard'); // Step 1: Navigate to page
cy.get('.sidebar').click(); // Step 2: Click sidebar
cy.get('.menu-item').first().click(); // Step 3: Select menu item
cy.get('.content').should('be.visible'); // Step 4: Verify content is visible
Screenshots and Video Recording
Screenshots
// Take a screenshot at any point
cy.screenshot('login-page');
// Capture only a specific element
cy.get('.error-message').screenshot('error-state');
// Screenshots are automatically saved on test failure
Screenshots are saved to cypress/screenshots/ by default.
Configuration
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
screenshotsFolder: 'cypress/screenshots',
screenshotOnRunFailure: true, // Auto-capture on failure
},
});
Video Recording
When running with cypress run (headless mode), test videos are recorded automatically.
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
video: true, // Enable video recording
videosFolder: 'cypress/videos', // Output directory
videoCompression: 32, // Compression level (0-51)
},
});
# Run in headless mode (video will be recorded)
npx cypress run
Screenshots vs. Video
| Feature | Screenshots | Video |
|---|---|---|
| Use case | Inspect state at a specific point | Review the entire test flow |
| Auto-save | Automatically on failure | Automatically during run |
| File size | Small | Large |
| CI/CD usage | Identifying failure causes | Reviewing the full flow |
Test Retries
Tests can become flaky due to network latency, animations, and other factors. The retry feature helps improve stability.
flowchart TB
subgraph Retry["Retry Flow"]
T["Run Test"] --> R{"Pass?"}
R -->|"Yes"| P["Pass"]
R -->|"No"| C{"Retry limit<br/>exceeded?"}
C -->|"No"| T
C -->|"Yes"| F["Fail"]
end
style P fill:#22c55e,color:#fff
style F fill:#ef4444,color:#fff
style Retry fill:#8b5cf6,color:#fff
Global Configuration
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
retries: {
runMode: 2, // Retries during cypress run
openMode: 0, // Retries during cypress open
},
});
Per-Test Configuration
// Set retries for an entire describe block
describe('Flaky API integration tests', { retries: 3 }, () => {
it('fetches and displays data', () => {
cy.visit('/dashboard');
cy.get('.data-table').should('be.visible');
});
});
// Set retries for a specific test
it('shows a notification', { retries: { runMode: 3, openMode: 1 } }, () => {
cy.get('.notification').should('be.visible');
});
Retry Pitfalls
// BAD: State is not reset on retry
it('counter test', () => {
cy.get('#increment').click(); // Clicked again on retry
cy.get('#count').should('have.text', '1');
});
// GOOD: Reset state with beforeEach
beforeEach(() => {
cy.visit('/counter'); // Reload the page each time
});
it('counter test', () => {
cy.get('#increment').click();
cy.get('#count').should('have.text', '1');
});
Test Isolation
Each test should be independent and able to run on its own without relying on other tests.
flowchart TB
subgraph Bad["Bad: Tests depend on each other"]
B1["Test 1: Create user"] --> B2["Test 2: Log in as user"] --> B3["Test 3: Edit profile"]
end
subgraph Good["Good: Independent tests"]
G1["Test 1: Create user"]
G2["Test 2: Log in<br/>(create user via API)"]
G3["Test 3: Edit profile<br/>(set up logged-in state via API)"]
end
style Bad fill:#ef4444,color:#fff
style Good fill:#22c55e,color:#fff
Bad Example
// BAD: Tests depend on each other
describe('User management', () => {
it('creates a user', () => {
cy.visit('/register');
cy.get('#name').type('John');
cy.get('#email').type('john@example.com');
cy.get('#submit').click();
});
// This test fails if Test 1 didn't run first!
it('logs in as the created user', () => {
cy.visit('/login');
cy.get('#email').type('john@example.com');
cy.get('#password').type('password');
cy.get('#login-btn').click();
});
});
Good Example
// GOOD: Each test is independent
describe('User management', () => {
it('creates a user', () => {
cy.visit('/register');
cy.get('#name').type('John');
cy.get('#email').type('john@example.com');
cy.get('#submit').click();
cy.url().should('include', '/dashboard');
});
it('logs in', () => {
// Create user via API (no UI dependency)
cy.request('POST', '/api/users', {
name: 'John',
email: 'john@example.com',
password: 'password',
});
cy.visit('/login');
cy.get('#email').type('john@example.com');
cy.get('#password').type('password');
cy.get('#login-btn').click();
cy.url().should('include', '/dashboard');
});
});
Proper Use of beforeEach / afterEach
beforeEach
Runs common setup logic before each test.
describe('Dashboard', () => {
beforeEach(() => {
// Set up logged-in state before each test
cy.request('POST', '/api/login', {
email: 'test@example.com',
password: 'password',
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
cy.visit('/dashboard');
});
it('displays a welcome message', () => {
cy.get('.welcome').should('contain', 'Welcome');
});
it('displays the sidebar', () => {
cy.get('.sidebar').should('be.visible');
});
it('displays statistics', () => {
cy.get('.stats').should('be.visible');
});
});
afterEach
Runs cleanup logic after each test.
describe('Data operations', () => {
afterEach(() => {
// Clean up data after each test
cy.request('DELETE', '/api/test-data/cleanup');
});
it('adds an item', () => {
cy.get('#add-btn').click();
cy.get('.item-list').should('have.length', 1);
});
});
Difference Between before/after and beforeEach/afterEach
| Hook | When It Runs | Use Case |
|---|---|---|
before |
Once per describe (at the start) | Heavy operations like DB initialization |
beforeEach |
Before every it block |
Page navigation, login |
afterEach |
After every it block |
Data cleanup |
after |
Once per describe (at the end) | Final cleanup |
The Page Object Pattern
The Page Object pattern encapsulates page-specific operations into classes or objects. It significantly improves test readability and maintainability.
flowchart TB
subgraph Without["Without Page Object"]
T1["Test A: cy.get('#email')..."]
T2["Test B: cy.get('#email')..."]
T3["Test C: cy.get('#email')..."]
end
subgraph With["With Page Object"]
PO["LoginPage<br/>- enter email<br/>- enter password<br/>- submit login"]
TA["Test A: loginPage.login()"]
TB["Test B: loginPage.login()"]
TC["Test C: loginPage.login()"]
PO --> TA
PO --> TB
PO --> TC
end
style Without fill:#ef4444,color:#fff
style With fill:#22c55e,color:#fff
style PO fill:#3b82f6,color:#fff
Creating a Page Object
// cypress/pages/LoginPage.js
class LoginPage {
// Selectors
get emailInput() {
return cy.get('#email');
}
get passwordInput() {
return cy.get('#password');
}
get loginButton() {
return cy.get('#login-btn');
}
get errorMessage() {
return cy.get('.error-message');
}
// Actions
visit() {
cy.visit('/login');
return this;
}
typeEmail(email) {
this.emailInput.clear().type(email);
return this;
}
typePassword(password) {
this.passwordInput.clear().type(password);
return this;
}
submit() {
this.loginButton.click();
return this;
}
login(email, password) {
this.typeEmail(email);
this.typePassword(password);
this.submit();
return this;
}
}
export default new LoginPage();
Using It in Tests
// cypress/e2e/login.cy.js
import loginPage from '../pages/LoginPage';
describe('Login page', () => {
beforeEach(() => {
loginPage.visit();
});
it('logs in with valid credentials', () => {
loginPage.login('test@example.com', 'password123');
cy.url().should('include', '/dashboard');
});
it('shows an error with incorrect password', () => {
loginPage.login('test@example.com', 'wrong');
loginPage.errorMessage.should('contain', 'Incorrect password');
});
it('shows an error when email is empty', () => {
loginPage.typePassword('password123').submit();
loginPage.errorMessage.should('contain', 'Please enter your email');
});
});
Benefits of the Page Object Pattern
| Benefit | Description |
|---|---|
| Maintainability | When the UI changes, only the Page Object needs updating |
| Readability | Tests clearly express their intent |
| Reusability | The same operations can be shared across multiple tests |
| DRY Principle | Eliminates duplicated selectors |
Test Naming Conventions and File Organization
Recommended Directory Structure
cypress/
βββ e2e/ # Test files
β βββ auth/
β β βββ login.cy.js
β β βββ logout.cy.js
β β βββ register.cy.js
β βββ dashboard/
β β βββ overview.cy.js
β β βββ settings.cy.js
β βββ products/
β βββ list.cy.js
β βββ detail.cy.js
β βββ cart.cy.js
βββ fixtures/ # Test data
β βββ users.json
β βββ products.json
βββ pages/ # Page Objects
β βββ LoginPage.js
β βββ DashboardPage.js
β βββ ProductPage.js
βββ support/ # Helpers and custom commands
β βββ commands.js
β βββ e2e.js
βββ downloads/ # Downloaded files
Naming Conventions
// describe: by feature or page
describe('Login page', () => {
// context: by condition or scenario
context('with valid credentials', () => {
// it: expected behavior
it('redirects to the dashboard', () => {
// ...
});
it('displays a welcome message', () => {
// ...
});
});
context('with invalid credentials', () => {
it('displays an error message', () => {
// ...
});
it('stays on the login page', () => {
// ...
});
});
});
File Naming Rules
| Pattern | Example | Use Case |
|---|---|---|
| feature.cy.js | login.cy.js | Tests for a single feature |
| feature-action.cy.js | product-search.cy.js | Tests for a specific action |
| page.cy.js | dashboard.cy.js | Tests scoped to a page |
Summary
| Concept | Description |
|---|---|
| cy.debug() | Pauses in the DevTools debugger for inspection |
| cy.pause() | Pauses test execution for step-by-step control |
| Time Travel | View past DOM states from the command log |
| Screenshots | Save the screen as an image at a specific point |
| Video Recording | Automatically record the entire test run |
| Retries | Automatically re-run flaky tests |
| Test Isolation | Design each test to be independent of others |
| Page Object | A pattern for encapsulating page operations |
| Naming Conventions | Hierarchical structure with describe/context/it |
Key Takeaways
- Time Travel is one of Cypress's greatest strengths
- Know when to use cy.pause() vs. cy.debug()
- Always keep test isolation in mind
- Use beforeEach for shared setup
- Improve maintainability with the Page Object pattern
Exercises
Basics
- Insert
cy.pause()in the middle of a test and try stepping through it in the Test Runner. - Use
cy.screenshot('my-screenshot')to capture a screenshot at any point during a test. - Set the retry count to
runMode: 2incypress.config.js.
Intermediate
- Create a Page Object for a login page and use it in your tests.
- Refactor your tests to use
beforeEachfor setting up initial state before each test. - Organize your tests using the hierarchical
describe/context/itnaming convention.
Challenge
- Design an E2E test spanning multiple pages (registration, login, profile editing) where each test runs independently. Use API calls to set up test preconditions.
References
Next up: In Day 10, you will learn about CI/CD and Best Practices. We will cover running tests automatically with GitHub Actions, parallel test execution, performance optimization, and other practical approaches to test operations!