Testing

Comprehensive Testing Strategies with Cypress and Jest

6 MinutesRead
Anisa

A deep dive into testing methodologies combining unit tests with Jest and E2E tests with Cypress.

Testing is often overlooked in the early stages of development, but implementing a solid testing strategy from the beginning can save countless hours of debugging and prevent critical bugs from reaching production.

The Testing Pyramid

A well-structured testing strategy follows the testing pyramid:

  1. Unit Tests (70%) - Fast, isolated, numerous
  2. Integration Tests (20%) - Medium speed, test component interactions
  3. E2E Tests (10%) - Slow, expensive, but test real user scenarios

Unit Testing with Jest

Jest excels at unit and integration testing. Here’s how I structure my Jest tests:

Basic Setup

// jest.config.js
module.exports = {
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
  moduleNameMapping: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  collectCoverageFrom: [
    "src/**/*.{ts,tsx}",
    "!src/**/*.d.ts",
    "!src/test/**/*",
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Testing React Components

// UserProfile.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { UserProfile } from "./UserProfile";

describe("UserProfile", () => {
  const mockUser = {
    id: "1",
    name: "John Doe",
    email: "john@example.com",
  };

  it("displays user information correctly", () => {
    render(<UserProfile user={mockUser} />);

    expect(screen.getByText("John Doe")).toBeInTheDocument();
    expect(screen.getByText("john@example.com")).toBeInTheDocument();
  });

  it("calls onEdit when edit button is clicked", () => {
    const mockOnEdit = jest.fn();
    render(<UserProfile user={mockUser} onEdit={mockOnEdit} />);

    fireEvent.click(screen.getByRole("button", { name: /edit/i }));

    expect(mockOnEdit).toHaveBeenCalledWith(mockUser.id);
  });
});

Testing Custom Hooks

// useUserData.test.ts
import { renderHook, act } from "@testing-library/react";
import { useUserData } from "./useUserData";

describe("useUserData", () => {
  it("fetches and returns user data", async () => {
    const { result } = renderHook(() => useUserData("123"));

    expect(result.current.loading).toBe(true);

    await act(async () => {
      // Wait for the hook to complete
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.user).toBeDefined();
  });
});

E2E Testing with Cypress

Cypress is perfect for testing user workflows and ensuring your application works as expected from a user’s perspective.

Cypress Configuration

// cypress.config.ts
import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
  component: {
    devServer: {
      framework: "next",
      bundler: "webpack",
    },
  },
});

Writing E2E Tests

// cypress/e2e/user-registration.cy.ts
describe("User Registration Flow", () => {
  beforeEach(() => {
    cy.visit("/register");
  });

  it("successfully registers a new user", () => {
    // Fill out the registration form
    cy.get('[data-testid="name-input"]').type("John Doe");
    cy.get('[data-testid="email-input"]').type("john@example.com");
    cy.get('[data-testid="password-input"]').type("securePassword123");
    cy.get('[data-testid="confirm-password-input"]').type("securePassword123");

    // Submit the form
    cy.get('[data-testid="submit-button"]').click();

    // Verify successful registration
    cy.url().should("include", "/dashboard");
    cy.get('[data-testid="welcome-message"]').should(
      "contain",
      "Welcome, John Doe"
    );
  });

  it("shows validation errors for invalid input", () => {
    cy.get('[data-testid="submit-button"]').click();

    cy.get('[data-testid="name-error"]').should("be.visible");
    cy.get('[data-testid="email-error"]').should("be.visible");
    cy.get('[data-testid="password-error"]').should("be.visible");
  });
});

Custom Cypress Commands

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      createUser(userData: UserData): Chainable<void>;
    }
  }
}

Cypress.Commands.add("login", (email: string, password: string) => {
  cy.session([email, password], () => {
    cy.visit("/login");
    cy.get('[data-testid="email-input"]').type(email);
    cy.get('[data-testid="password-input"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should("include", "/dashboard");
  });
});

Testing API Endpoints

For API testing, I combine Jest with supertest:

// api/users.test.ts
import request from "supertest";
import { app } from "../app";

describe("POST /api/users", () => {
  it("creates a new user successfully", async () => {
    const userData = {
      name: "John Doe",
      email: "john@example.com",
    };

    const response = await request(app)
      .post("/api/users")
      .send(userData)
      .expect(201);

    expect(response.body).toMatchObject({
      id: expect.any(String),
      name: userData.name,
      email: userData.email,
      createdAt: expect.any(String),
    });
  });

  it("returns validation error for invalid email", async () => {
    const userData = {
      name: "John Doe",
      email: "invalid-email",
    };

    const response = await request(app)
      .post("/api/users")
      .send(userData)
      .expect(400);

    expect(response.body.error).toContain("Invalid email format");
  });
});

Best Practices

1. Use Data Test IDs

Always use data-testid attributes instead of relying on class names or text content.

2. Mock External Dependencies

Mock API calls, third-party services, and other external dependencies in your unit tests.

3. Test User Behavior, Not Implementation

Focus on testing what the user sees and does, not internal implementation details.

4. Maintain Test Data

Use factories or fixtures to create consistent test data:

// test/factories/userFactory.ts
export const createUser = (overrides = {}) => ({
  id: "1",
  name: "John Doe",
  email: "john@example.com",
  createdAt: new Date().toISOString(),
  ...overrides,
});

5. Parallel Test Execution

Configure your CI/CD pipeline to run tests in parallel for faster feedback.

Conclusion

A comprehensive testing strategy using Jest for unit/integration tests and Cypress for E2E tests provides confidence in your application’s reliability. Start with the most critical user paths and gradually expand your test coverage.

Remember: good tests are an investment in your application’s future maintainability and your team’s productivity.


What testing challenges have you faced in your projects? How do you balance test coverage with development speed?

Tags

#Testing #Cypress #Jest #E2E #Unit Testing