Mastering Cypress Hooks: A Deep Dive into `before`, `after`, `beforeEach`, and `afterEach`

July 28, 2025

In the world of end-to-end testing, efficiency and maintainability are not just goals; they are necessities. As a test suite grows, a common challenge emerges: repetitive setup and teardown logic cluttering your test files. You might find yourself visiting the same URL, logging in a user, or resetting application state at the beginning of every single test. This repetition not only bloats your codebase but also makes it fragile and difficult to manage. This is precisely the problem that Cypress hooks are designed to solve. By providing a structured way to run code at specific moments in the test lifecycle, these powerful functions allow you to write cleaner, more modular, and significantly more efficient tests. Understanding how and when to use before(), after(), beforeEach(), and afterEach() is a fundamental step in transforming your testing practice from functional to exceptional.

What Are Cypress Hooks and Why Are They Essential?

At their core, Cypress hooks are functions that allow you to execute code before or after certain test blocks. If you have experience with other testing frameworks, this concept might feel familiar. Cypress borrows its hook implementation directly from Mocha, the popular JavaScript test framework that underpins Cypress's structure. You can find hooks in many testing libraries because they solve a universal problem in software testing: managing state and side effects in a predictable manner.

The primary purpose of using Cypress hooks is to adhere to the Don't Repeat Yourself (DRY) principle. As noted by thought leaders in software engineering, duplicated code is a significant 'code smell' that can lead to maintenance nightmares. Imagine you have ten tests that all require a user to be logged in. Without hooks, you would write the login logic ten times. With a beforeEach hook, you write it once. This not only saves lines of code but also centralizes the logic. If the login process changes, you only need to update it in one place.

Beyond code reduction, hooks are essential for ensuring test isolation. A core tenet of reliable automated testing is that each test should be able to run independently without being affected by the state left over from previous tests. Research from Google on test flakiness highlights that state pollution is a leading cause of tests that randomly fail. Cypress hooks, particularly beforeEach and afterEach, provide the perfect mechanism to set up a clean environment before a test runs and tear it down afterward, dramatically increasing the reliability of your test suite. According to the JetBrains State of Developer Ecosystem 2023 report, test automation remains a top priority for development teams, making the mastery of tools like Cypress hooks a critical skill for modern developers.

The `beforeEach()` Hook: Your Go-To for Per-Test Setup

The beforeEach() hook is arguably the most frequently used of all the Cypress hooks. Its function is simple yet powerful: it runs a block of code before every single it() test case within its describe() block. This makes it the ideal tool for any setup action that needs to be performed repeatedly to ensure a consistent starting state for each test.

Consider the most common scenario in web testing: navigating to a specific page. Instead of placing cy.visit('/dashboard') inside every test, you can abstract it into a beforeEach() hook.

describe('Dashboard Features', () => {
  beforeEach(() => {
    // This code runs before each test in this describe block
    cy.visit('/dashboard');
  });

  it('should display the correct heading', () => {
    cy.get('h1').should('contain', 'My Dashboard');
  });

  it('should show the user profile widget', () => {
    cy.get('[data-cy=profile-widget]').should('be.visible');
  });
});

In this example, both tests start from a clean slate by visiting the /dashboard page. This isolation is crucial. If the second test were to fail, you know it's not because the first test left the application on a different page.

Beyond navigation, beforeEach() is perfect for actions like:

  • Seeding test-specific data: Using cy.request() or cy.task() to set up a specific database state needed for a particular test.
  • Setting cookies or local storage: Preparing the browser with session information or user preferences.
  • Mocking API responses: Intercepting network requests with cy.intercept() to provide consistent API responses for the UI to consume. The official Cypress documentation provides extensive examples of this powerful pattern.

However, it's important to use beforeEach() judiciously. If a setup action is computationally expensive (e.g., seeding a large database) and the data doesn't need to be reset for every test, running it before each test can significantly slow down your suite. In such cases, the before() hook is a more appropriate choice. Efficient test execution is a key factor in developer productivity, a trend highlighted in recent Forrester reports on test automation impact.

The `afterEach()` Hook: The Cleanup Crew

Just as beforeEach() handles setup, the afterEach() hook manages teardown. It executes after every single it() block within its scope, regardless of whether the test passed or failed. Its primary role is to clean up any state or artifacts created during a test, ensuring that the environment is pristine for the next test to run. This disciplined cleanup is a cornerstone of preventing test interdependency and flakiness.

Common use cases for the afterEach() hook include:

  • Logging out a user: If each test logs in, it's good practice to log out to ensure the next test starts with a clean session.
  • Deleting created data: If a test creates a new user, post, or product in the database, afterEach() can be used to remove that specific entry.
  • Resetting UI state: Closing modals or resetting filters to their default state.

Here is a practical example demonstrating cleanup:

describe('User Profile Management', () => {
  const testUser = {
    email: `test-${Date.now()}@example.com`,
    password: 'securePassword123'
  };

  beforeEach(() => {
    // Create a new user for each test to ensure isolation
    cy.task('db:createUser', testUser);
    cy.visit('/login');
    cy.get('#email').type(testUser.email);
    cy.get('#password').type(testUser.password);
    cy.get('button[type=submit]').click();
  });

  afterEach(() => {
    // Clean up the user from the database after each test
    cy.task('db:deleteUser', { email: testUser.email });
  });

  it('allows the user to update their name', () => {
    cy.visit('/profile');
    cy.get('#name').type('New Name').blur();
    cy.contains('Profile updated successfully').should('be.visible');
  });
});

In this suite, afterEach() guarantees that the user created for the test is removed, preventing the database from filling up with test data and ensuring subsequent test runs are not affected by this user's existence. Discussions on non-deterministic tests often point to leftover state as a primary culprit, making the role of afterEach() critical for robust test suites. A key consideration, as detailed in the Cypress documentation, is that if a test fails, subsequent commands in that test (and potentially in an afterEach hook) may not run. Therefore, cleanup actions should be idempotent and resilient.

The `before()` Hook: One-Time Setup for Your Test Suite

While beforeEach() is for repetitive setup, the before() hook is designed for tasks that only need to happen once before all the tests in a describe() block begin. This distinction is crucial for optimizing test suite performance. Executing expensive, time-consuming operations only once, instead of before every test, can shave significant time off your CI/CD pipeline runs. Industry analysis frequently points to slow feedback loops from testing as a major drag on development velocity, making optimizations with before() highly valuable.

Ideal use cases for the before() hook include:

  • Database Seeding: Populating the database with a large, static set of data that all tests in the suite can use without modification.
  • Starting a Server: If your tests require a mock server to be running, before() is the place to start it.
  • Fetching a Reusable Auth Token: For APIs that use tokens with a long expiry, you can fetch one token in before() and reuse it across all tests in the suite.

Here’s an example of using before() to seed a database:

describe('Product Catalog Search', () => {
  before(() => {
    // This runs ONCE before all tests
    // Wipes the database and seeds it with a standard product set
    cy.task('db:reset');
    cy.task('db:seed', 'product-catalog.json');
  });

  beforeEach(() => {
    // This still runs before each test
    cy.visit('/products');
  });

  it('should find products by name', () => {
    cy.get('#search').type('Laptop');
    cy.get('.product-card').should('have.length', 3);
  });

  it('should filter products by category', () => {
    cy.get('#category-filter').select('Electronics');
    cy.get('.product-card').should('have.length.greaterThan', 5);
  });
});

A Critical Warning: The power of before() comes with a significant responsibility. Since it runs only once, any state it creates is shared across all subsequent tests in the describe() block. If your tests modify this shared state (e.g., delete a product from the seeded catalog), it will affect all following tests, leading to cascading failures that are difficult to debug. This anti-pattern violates the principle of test isolation. Therefore, use before() primarily for setting up read-only state. For any state that will be mutated, stick with beforeEach() to ensure a fresh start for every test. This common pitfall is a frequent topic in Cypress GitHub issue discussions and community forums.

The `after()` Hook: The Final Teardown

Complementing the before() hook, the after() hook executes its code block once after all tests within the describe() block have finished. It serves as the final cleanup mechanism for the entire test suite, ideal for reversing the actions performed in the before() hook.

This hook is less common than afterEach() because individual test cleanup is often preferred for maintaining isolation. However, after() is indispensable in specific scenarios where a global teardown is necessary or more efficient. Its primary function is to restore the environment to its original state before the test suite was run.

Typical use cases for the after() hook are:

  • Global Database Cleanup: Dropping tables or wiping all data that was seeded in the before() hook.
  • Shutting Down Services: Stopping a mock server or other background process that was initiated in before().
  • Generating a Final Report: Compiling results from the entire suite and sending them to a test management tool.

Let's see before() and after() working in tandem:

describe('Integration with External Service', () => {
  before(() => {
    // Start a mock server for the external service
    cy.task('startMockServer');
  });

  after(() => {
    // Stop the mock server after all tests are done
    cy.task('stopMockServer');
  });

  it('fetches data correctly from the service', () => {
    cy.request('http://localhost:9000/api/data').its('status').should('eq', 200);
  });

  it('posts data correctly to the service', () => {
    cy.request('POST', 'http://localhost:9000/api/data', { item: 'new' })
      .its('status').should('eq', 201);
  });
});

In this example, the mock server is started once before any test runs and is reliably shut down once all tests are complete, regardless of their individual outcomes. This pattern is clean and efficient. One important consideration, as outlined in the official Cypress hook documentation, is that after() hooks will always run, even if some tests in the suite failed. This makes them reliable for critical cleanup tasks. However, relying on them for complex logic can be risky, as the state of the application might be unpredictable after a test failure.

Advanced Cypress Hooks Patterns and Best Practices

Mastering the basic Cypress hooks is the first step. To truly elevate your testing strategy, you need to understand their behavior in more complex scenarios and adopt advanced patterns.

Nested Hooks and Execution Order

Cypress supports nesting describe() blocks, and hooks within these blocks follow a clear, hierarchical execution order. Hooks in an outer block will wrap around the hooks and tests in an inner block.

Consider this structure:

describe('Outer Suite', () => {
  before(() => console.log('Outer before'));
  after(() => console.log('Outer after'));
  beforeEach(() => console.log('Outer beforeEach'));
  afterEach(() => console.log('Outer afterEach'));

  describe('Inner Suite', () => {
    beforeEach(() => console.log('Inner beforeEach'));
    afterEach(() => console.log('Inner afterEach'));

    it('is test 1', () => console.log('TEST 1'));
    it('is test 2', () => console.log('TEST 2'));
  });
});

The console output would be:

  1. Outer before
  2. Outer beforeEach
  3. Inner beforeEach
  4. TEST 1
  5. Inner afterEach
  6. Outer afterEach
  7. Outer beforeEach
  8. Inner beforeEach
  9. TEST 2
  10. Inner afterEach
  11. Outer afterEach
  12. Outer after

Understanding this flow is crucial for orchestrating complex setup and teardown logic across different parts of your application.

Sharing State with this

Sometimes you need to share data from a hook (like a fetched user ID) with your it() blocks. A common way to do this is by using the this context. However, this only works if you use function() declarations, not arrow functions () => {}, because arrow functions have a lexical this.

describe('User Context', function() {
  beforeEach(function() {
    // Use a regular function to access 'this'
    cy.fixture('user.json').then((user) => {
      this.user = user;
    });
  });

  it('can access user data from the hook', function() {
    // Access the user data via this.user
    cy.visit(`/users/${this.user.id}`);
    cy.get('h1').should('contain', this.user.name);
  });
});

This pattern, while powerful, should be used with caution as it can make tests less explicit. A more modern and often preferred approach is to use aliases (.as()) or custom commands. For a deeper understanding of this in JavaScript, the MDN web docs are an excellent resource.

Cypress hooks are not merely a convenience; they are a foundational element of a well-structured, scalable, and reliable test automation strategy. By thoughtfully applying beforeEach for isolated setup, afterEach for consistent cleanup, before for efficient one-time preparations, and after for final teardowns, you can drastically improve the quality and performance of your test suite. Moving from simply writing tests to architecting them with hooks is a key differentiator for any serious quality engineering practice.

What today's top teams are saying about Momentic:

"Momentic makes it 3x faster for our team to write and maintain end to end tests."

- Alex, CTO, GPTZero

"Works for us in prod, super great UX, and incredible velocity and delivery."

- Aditya, CTO, Best Parents

"…it was done running in 14 min, without me needing to do a thing during that time."

- Mike, Eng Manager, Runway

Increase velocity with reliable AI testing.

Run stable, dev-owned tests on every push. No QA bottlenecks.

Ship it

FAQs

Momentic tests are much more reliable than Playwright or Cypress tests because they are not affected by changes in the DOM.

Our customers often build their first tests within five minutes. It's very easy to build tests using the low-code editor. You can also record your actions and turn them into a fully working automated test.

Not even a little bit. As long as you can clearly describe what you want to test, Momentic can get it done.

Yes. You can use Momentic's CLI to run tests anywhere. We support any CI provider that can run Node.js.

Mobile and desktop support is on our roadmap, but we don't have a specific release date yet.

We currently support Chromium and Chrome browsers for tests. Safari and Firefox support is on our roadmap, but we don't have a specific release date yet.

Β© 2025 Momentic, Inc.
All rights reserved.