Skip to main content

Why Test Organization Matters

A well-organized test suite:
  • Makes tests easy to find and understand
  • Reduces duplication and maintenance overhead
  • Enables efficient test execution
  • Improves collaboration among team members
  • Scales as your application grows

Directory Structure

The framework follows a clear separation between test types:
cypress/
├── e2e/
   ├── api/                  # API tests
   └── GETSpec.cy.ts
   └── ui/                   # UI tests
       ├── LoginPageSpec.cy.ts
       ├── HomePageSpec.cy.ts
       └── CheckoutPageSpec.cy.ts
├── fixtures/
   ├── users.json
   ├── loginPageData.json
   └── bookings.json
├── support/
   ├── commands.ts
   ├── e2e.ts
   ├── pages/
   ├── BasePage.ts
   ├── LoginPage.ts
   └── HomePage.ts
   └── components/
       └── Navbar.ts

Organizing UI Tests

By Feature or Page

Group tests by the feature or page they test:
cypress/e2e/ui/
├── authentication/
   ├── LoginPageSpec.cy.ts
   ├── SignupPageSpec.cy.ts
   └── PasswordResetSpec.cy.ts
├── products/
   ├── ProductListSpec.cy.ts
   ├── ProductDetailSpec.cy.ts
   └── ProductSearchSpec.cy.ts
└── checkout/
    ├── CartSpec.cy.ts
    ├── CheckoutSpec.cy.ts
    └── PaymentSpec.cy.ts

By User Journey

Organize tests following user workflows:
cypress/e2e/ui/
├── guest-user/
   ├── BrowseProductsSpec.cy.ts
   └── CreateAccountSpec.cy.ts
├── registered-user/
   ├── LoginSpec.cy.ts
   ├── PurchaseSpec.cy.ts
   └── ProfileManagementSpec.cy.ts
└── admin-user/
    ├── UserManagementSpec.cy.ts
    └── InventoryManagementSpec.cy.ts
Choose the organization strategy that best fits your application’s structure and team’s mental model.

Organizing API Tests

By HTTP Method

Group API tests by HTTP method:
cypress/e2e/api/
├── GETSpec.cy.ts
├── POSTSpec.cy.ts
├── PUTSpec.cy.ts
└── DELETESpec.cy.ts

By Resource

Group tests by API resource:
cypress/e2e/api/
├── bookings/
   ├── GetBookingsSpec.cy.ts
   ├── CreateBookingSpec.cy.ts
   ├── UpdateBookingSpec.cy.ts
   └── DeleteBookingSpec.cy.ts
├── auth/
   ├── LoginSpec.cy.ts
   └── TokenSpec.cy.ts
└── users/
    ├── GetUsersSpec.cy.ts
    └── CreateUserSpec.cy.ts

By Test Type

Separate functional, integration, and contract tests:
cypress/e2e/api/
├── functional/
   └── BookingCRUDSpec.cy.ts
├── integration/
   └── BookingWorkflowSpec.cy.ts
└── contract/
    └── BookingSchemaSpec.cy.ts

Test File Naming Conventions

Descriptive Names

Use clear, descriptive names that indicate what’s being tested

Spec Suffix

End files with Spec.cy.ts or just .cy.ts

PascalCase

Use PascalCase for test file names

Avoid Abbreviations

Write full words for clarity

Good Examples

 LoginPageSpec.cy.ts
 UserAuthenticationSpec.cy.ts
 ProductSearchSpec.cy.ts
 CheckoutFlowSpec.cy.ts

Bad Examples

 test1.cy.ts
 login.cy.ts (too generic)
 usrAuth.cy.ts (uses abbreviations)
 spec.cy.ts (not descriptive)

Structuring Test Files

Single describe Block

Group related tests under one describe:
describe('User Authentication', () => {
  beforeEach(() => {
    // Common setup
  })

  it('Should log in with valid credentials', () => {})
  it('Should not log in with invalid credentials', () => {})
  it('Should show error for locked account', () => {})
  it('Should redirect after successful login', () => {})
})

Nested describe Blocks

Use nested describes for complex scenarios:
describe('Product Management', () => {
  beforeEach(() => {
    // Common setup for all product tests
  })

  describe('Product List', () => {
    it('Should display all products', () => {})
    it('Should filter products by category', () => {})
    it('Should sort products by price', () => {})
  })

  describe('Product Details', () => {
    it('Should show product information', () => {})
    it('Should add product to cart', () => {})
    it('Should show related products', () => {})
  })

  describe('Product Search', () => {
    it('Should search by product name', () => {})
    it('Should show no results message', () => {})
  })
})

Context Blocks

Use context as an alias for describe to add clarity:
describe('Login', () => {
  context('With valid credentials', () => {
    it('Should log in successfully', () => {})
    it('Should redirect to home page', () => {})
  })

  context('With invalid credentials', () => {
    it('Should show error message', () => {})
    it('Should remain on login page', () => {})
  })

  context('With locked account', () => {
    it('Should show locked account message', () => {})
    it('Should not allow login', () => {})
  })
})

Hooks Organization

before vs beforeEach

describe('User Tests', () => {
  // Runs once before all tests
  before(() => {
    // Expensive operations (database seeding, etc.)
  })

  // Runs before each test
  beforeEach(() => {
    // Setup for each test (login, navigation, etc.)
    cy.fixture('users').as('userData')
    loginPage = new LoginPage()
    cy.visit('/')
  })

  // Runs after each test
  afterEach(() => {
    // Cleanup (logout, clear cookies, etc.)
  })

  // Runs once after all tests
  after(() => {
    // Final cleanup
  })
})

Shared Setup in Support Files

For setup used across multiple test files:
cypress/support/e2e.ts
// Global beforeEach
beforeEach(() => {
  // Runs before every test in every file
  cy.clearCookies()
  cy.clearLocalStorage()
})

// Global afterEach
afterEach(() => {
  // Runs after every test
  cy.screenshot({ capture: 'runner' })
})
Use global hooks sparingly. Prefer test-specific hooks for better test isolation.

Custom Commands for Reusable Logic

Define Custom Commands

cypress/support/commands.ts
Cypress.Commands.add('login', (username: string, password: string) => {
  cy.visit('/login')
  cy.get('#username').type(username)
  cy.get('#password').type(password)
  cy.get('#login-button').click()
})

Cypress.Commands.add('logout', () => {
  cy.get('#menu').click()
  cy.get('#logout').click()
})

Use in Tests

it('Should access protected page', () => {
  cy.login('standard_user', 'secret_sauce')
  cy.visit('/protected')
  cy.contains('Protected Content').should('be.visible')
})

TypeScript Declarations

cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(username: string, password: string): Chainable<void>
      logout(): Chainable<void>
    }
  }
}

Test Data Organization

Fixture Organization

Match fixture structure to test structure:
cypress/fixtures/
├── ui/
   ├── auth/
   ├── users.json
   └── credentials.json
   └── products/
       └── inventory.json
└── api/
    ├── bookings.json
    └── users.json

Shared vs Specific Data

cypress/fixtures/
├── shared/
   └── common-users.json    # Used across multiple tests
├── login/
   └── test-users.json      # Specific to login tests
└── checkout/
    └── payment-data.json    # Specific to checkout tests

Page Object Organization

One File Per Page

cypress/support/pages/
├── BasePage.ts
├── LoginPage.ts
├── HomePage.ts
├── ProductListPage.ts
├── ProductDetailPage.ts
├── CartPage.ts
└── CheckoutPage.ts

Group by Feature

cypress/support/pages/
├── BasePage.ts
├── auth/
   ├── LoginPage.ts
   ├── SignupPage.ts
   └── PasswordResetPage.ts
├── products/
   ├── ProductListPage.ts
   └── ProductDetailPage.ts
└── checkout/
    ├── CartPage.ts
    └── CheckoutPage.ts

Component Organization

cypress/support/components/
├── Navbar.ts
├── Footer.ts
├── SearchBar.ts
├── ProductCard.ts
└── Modal.ts

Test Tagging and Grouping

Use Descriptive Test Names

// ✅ Good - describes behavior
it('Should display error message when login fails', () => {})

// ❌ Bad - vague
it('Test login', () => {})

Tag Tests with Metadata

it('Should process payment', { tags: ['@critical', '@payment'] }, () => {
  // Test implementation
})

it('Should display terms', { tags: ['@ui', '@low-priority'] }, () => {
  // Test implementation
})

Group by Priority

describe('Critical User Flows', () => {
  it('Should complete purchase', () => {})
  it('Should process refund', () => {})
})

describe('Nice-to-Have Features', () => {
  it('Should show product recommendations', () => {})
  it('Should display wishlist', () => {})
})

Best Practices Summary

Keep test files focused on a single feature or page:
// ✅ Good - LoginPageSpec.cy.ts
describe('Login Page', () => {
  // All login-related tests
})

// ❌ Bad - AllTests.cy.ts
describe('All Application Tests', () => {
  // Tests for login, products, checkout, etc.
})
Use consistent naming across your test suite:
 LoginPageSpec.cy.ts
 HomePageSpec.cy.ts
 CheckoutPageSpec.cy.ts

 login-test.cy.ts
 home_page_spec.cy.ts
 checkout.cy.ts
Use describe blocks to create clear hierarchy:
describe('E-commerce Application', () => {
  describe('Authentication', () => {
    describe('Login', () => {
      it('Should login with valid credentials', () => {})
    })
  })
})
Ensure tests can run independently:
// ✅ Good - each test sets up its own state
it('Test 1', () => {
  cy.visit('/')
  // Test logic
})

it('Test 2', () => {
  cy.visit('/')
  // Test logic
})

Next Steps

Maintainability

Keep your tests maintainable long-term

TypeScript Usage

Leverage TypeScript for better organization

Running Tests

Execute your organized test suite

Project Structure

Understand the overall project layout

Build docs developers (and LLMs) love