Skip to content

Chapter II - Writing Tests

(avr. time for this chapter: 2 days)

Now that you understand Cypress basics, this chapter covers advanced patterns for writing maintainable, reliable tests. You'll learn about custom commands, fixtures, intercepts, and best practices.

Custom Commands

Custom commands extend Cypress with reusable functionality.

Creating Custom Commands

Add to cypress/support/commands.js:

// Login command
Cypress.Commands.add('login', (email, password) => {
  cy.get('[data-testid="email-input"]').type(email);
  cy.get('[data-testid="password-input"]').type(password);
  cy.get('[data-testid="login-button"]').click();
});

// Login via API (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('token', response.body.token);
  });
});

// Custom assertion
Cypress.Commands.add('shouldBeVisible', { prevSubject: true }, (subject) => {
  cy.wrap(subject).should('be.visible');
});

Using Custom Commands

describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('test@example.com', 'password123');
  });

  it('shows user dashboard', () => {
    cy.get('[data-testid="dashboard"]').shouldBeVisible();
  });
});

Fixtures

Fixtures are external data files used in tests.

Creating Fixtures

Create cypress/fixtures/users.json:

{
  "validUser": {
    "email": "test@example.com",
    "password": "password123",
    "name": "Test User"
  },
  "adminUser": {
    "email": "admin@example.com",
    "password": "admin123",
    "name": "Admin User"
  },
  "invalidUser": {
    "email": "invalid@example.com",
    "password": "wrongpassword"
  }
}

Create cypress/fixtures/products.json:

{
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99,
      "category": "Electronics"
    },
    {
      "id": 2,
      "name": "Headphones",
      "price": 149.99,
      "category": "Electronics"
    },
    {
      "id": 3,
      "name": "Coffee Mug",
      "price": 12.99,
      "category": "Kitchen"
    }
  ]
}

Using Fixtures

describe('Login', () => {
  beforeEach(() => {
    cy.fixture('users').as('users');
  });

  it('logs in with valid credentials', function() {
    cy.visit('/login');
    cy.login(this.users.validUser.email, this.users.validUser.password);
    cy.url().should('include', '/dashboard');
  });

  it('shows error with invalid credentials', function() {
    cy.visit('/login');
    cy.login(this.users.invalidUser.email, this.users.invalidUser.password);
    cy.get('[data-testid="error-message"]').should('be.visible');
  });
});

Network Interception

Cypress can intercept and modify network requests.

Intercepting Requests

describe('Products Page', () => {
  beforeEach(() => {
    // Intercept API call and return fixture data
    cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('getProducts');
    cy.visit('/products');
  });

  it('displays products from API', () => {
    cy.wait('@getProducts');
    cy.get('[data-testid="product-card"]').should('have.length', 3);
  });

  it('shows loading state', () => {
    cy.intercept('GET', '/api/products', {
      fixture: 'products.json',
      delay: 1000
    }).as('getProductsDelayed');

    cy.visit('/products');
    cy.get('[data-testid="loading"]').should('be.visible');
    cy.wait('@getProductsDelayed');
    cy.get('[data-testid="loading"]').should('not.exist');
  });
});

Mocking Error Responses

it('handles API errors gracefully', () => {
  cy.intercept('GET', '/api/products', {
    statusCode: 500,
    body: { error: 'Internal Server Error' }
  }).as('getProductsError');

  cy.visit('/products');
  cy.wait('@getProductsError');
  cy.get('[data-testid="error-message"]')
    .should('be.visible')
    .and('contain', 'Something went wrong');
});

it('handles network failure', () => {
  cy.intercept('GET', '/api/products', { forceNetworkError: true }).as('networkError');

  cy.visit('/products');
  cy.get('[data-testid="error-message"]').should('contain', 'Network error');
});

Modifying Requests

it('adds authentication header', () => {
  cy.intercept('GET', '/api/products', (req) => {
    req.headers['Authorization'] = 'Bearer test-token';
  }).as('authenticatedRequest');

  cy.visit('/products');
  cy.wait('@authenticatedRequest').its('request.headers')
    .should('have.property', 'Authorization', 'Bearer test-token');
});

Page Object Model

Organize tests using the Page Object pattern.

Creating Page Objects

Create cypress/support/pages/LoginPage.js:

class LoginPage {
  // Selectors
  get emailInput() {
    return cy.get('[data-testid="email-input"]');
  }

  get passwordInput() {
    return cy.get('[data-testid="password-input"]');
  }

  get loginButton() {
    return cy.get('[data-testid="login-button"]');
  }

  get errorMessage() {
    return cy.get('[data-testid="error-message"]');
  }

  get successMessage() {
    return cy.get('[data-testid="success-message"]');
  }

  // Actions
  visit() {
    cy.visit('/login');
  }

  fillEmail(email) {
    this.emailInput.clear().type(email);
    return this;
  }

  fillPassword(password) {
    this.passwordInput.clear().type(password);
    return this;
  }

  submit() {
    this.loginButton.click();
    return this;
  }

  login(email, password) {
    this.fillEmail(email).fillPassword(password).submit();
    return this;
  }

  // Assertions
  assertErrorVisible() {
    this.errorMessage.should('be.visible');
    return this;
  }

  assertSuccessVisible() {
    this.successMessage.should('be.visible');
    return this;
  }
}

export default new LoginPage();

Using Page Objects

import LoginPage from '../support/pages/LoginPage';

describe('Login Page', () => {
  beforeEach(() => {
    LoginPage.visit();
  });

  it('logs in successfully', () => {
    LoginPage
      .login('test@example.com', 'password123')
      .assertSuccessVisible();
  });

  it('shows error for invalid credentials', () => {
    LoginPage
      .login('wrong@example.com', 'wrongpassword')
      .assertErrorVisible();
  });

  it('supports method chaining', () => {
    LoginPage
      .fillEmail('test@example.com')
      .fillPassword('password123')
      .submit()
      .assertSuccessVisible();
  });
});

Handling Asynchronous Operations

Waiting for Elements

// Wait for element to appear
cy.get('[data-testid="modal"]', { timeout: 10000 }).should('be.visible');

// Wait for element to disappear
cy.get('[data-testid="loading"]').should('not.exist');

// Wait for specific condition
cy.get('[data-testid="counter"]').should(($el) => {
  expect(parseInt($el.text())).to.be.greaterThan(5);
});

Waiting for API Calls

it('waits for data to load', () => {
  cy.intercept('GET', '/api/users').as('getUsers');
  cy.visit('/users');

  cy.wait('@getUsers').then((interception) => {
    expect(interception.response.statusCode).to.equal(200);
    expect(interception.response.body).to.have.length.greaterThan(0);
  });
});

Retries and Timeouts

// Configure default timeout
// In cypress.config.js
module.exports = defineConfig({
  e2e: {
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
    responseTimeout: 30000,
  }
});

// Override for specific command
cy.get('[data-testid="slow-element"]', { timeout: 30000 });

Working with iframes

// Custom command for iframes
Cypress.Commands.add('getIframeBody', (iframeSelector) => {
  return cy
    .get(iframeSelector)
    .its('0.contentDocument.body')
    .should('not.be.empty')
    .then(cy.wrap);
});

// Usage
it('interacts with iframe content', () => {
  cy.getIframeBody('#payment-iframe')
    .find('[data-testid="card-number"]')
    .type('4242424242424242');
});

File Uploads and Downloads

File Upload

it('uploads a file', () => {
  cy.get('[data-testid="file-input"]').selectFile('cypress/fixtures/test-image.png');
  cy.get('[data-testid="upload-success"]').should('be.visible');
});

// Multiple files
it('uploads multiple files', () => {
  cy.get('[data-testid="file-input"]').selectFile([
    'cypress/fixtures/file1.pdf',
    'cypress/fixtures/file2.pdf'
  ]);
});

// Drag and drop
it('uploads via drag and drop', () => {
  cy.get('[data-testid="dropzone"]').selectFile('cypress/fixtures/test.pdf', {
    action: 'drag-drop'
  });
});

File Download

it('downloads a file', () => {
  cy.get('[data-testid="download-button"]').click();

  // Verify file was downloaded
  cy.readFile('cypress/downloads/report.pdf').should('exist');
});

Exercise: E-Commerce Test Suite

Create a comprehensive test suite for an e-commerce application.

Step 1: Create Page Objects

Create page objects for: - HomePage.js - ProductListPage.js - ProductDetailPage.js - CartPage.js - CheckoutPage.js

Step 2: Create Fixtures

Create fixtures for: - products.json - Product data - users.json - User credentials - orders.json - Order data

Step 3: Write Test Suites

Create test files:

// cypress/e2e/e-commerce/home.cy.js
describe('Home Page', () => {
  it('displays featured products');
  it('has working search functionality');
  it('navigates to product categories');
});

// cypress/e2e/e-commerce/products.cy.js
describe('Product Listing', () => {
  it('displays all products');
  it('filters by category');
  it('sorts by price');
  it('paginates results');
});

// cypress/e2e/e-commerce/cart.cy.js
describe('Shopping Cart', () => {
  it('adds product to cart');
  it('updates quantity');
  it('removes product');
  it('shows correct total');
  it('persists cart after page reload');
});

// cypress/e2e/e-commerce/checkout.cy.js
describe('Checkout Flow', () => {
  it('completes purchase as guest');
  it('completes purchase as logged-in user');
  it('validates required fields');
  it('handles payment errors');
});

Step 4: Implement Tests

Fill in each test with actual implementation using: - Custom commands - Fixtures - Network interception - Page objects

Best Practices

Selector Strategy

// Best: data-testid attributes
cy.get('[data-testid="submit-button"]');

// Good: Specific CSS selectors
cy.get('form.login button[type="submit"]');

// Avoid: Generic selectors that may change
cy.get('.btn-primary'); // May match multiple elements
cy.get('div > div > button'); // Fragile

Test Independence

// Bad: Tests depend on each other
it('creates a user', () => { /* ... */ });
it('logs in with created user', () => { /* depends on previous test */ });

// Good: Each test is independent
beforeEach(() => {
  cy.task('db:seed'); // Reset database
});

it('creates a user', () => { /* ... */ });
it('logs in with existing user', () => { /* uses fixture data */ });

Avoid Conditional Testing

// Bad: Conditional logic in tests
cy.get('body').then(($body) => {
  if ($body.find('.modal').length) {
    cy.get('.modal .close').click();
  }
});

// Good: Ensure consistent state
beforeEach(() => {
  cy.clearCookies();
  cy.visit('/');
});

Self-Assessment

After completing this chapter, you should be able to:

  • [ ] Create and use custom commands
  • [ ] Work with fixtures for test data
  • [ ] Intercept and mock network requests
  • [ ] Implement the Page Object Model
  • [ ] Handle asynchronous operations properly
  • [ ] Upload and download files
  • [ ] Follow Cypress best practices

Next Steps

Continue to Chapter III - Advanced Patterns to learn about visual testing, component testing, and CI/CD integration.