Skip to main content

Testing Strategy

Cal.com uses a comprehensive testing strategy:
  1. Unit Tests - Vitest for business logic and utilities
  2. Integration Tests - Testing API endpoints and services
  3. E2E Tests - Playwright for end-to-end user flows
  4. Type Checking - TypeScript compiler for type safety

Unit Testing with Vitest

Running Unit Tests

# Run all unit tests
TZ=UTC yarn test

# Run tests in watch mode
yarn tdd

# Run tests with UI
TZ=UTC yarn test:ui
Always set TZ=UTC to ensure consistent timezone behavior across environments.

Writing Unit Tests

Create test files with .test.ts or .spec.ts suffix:
// File: packages/lib/date/parseDate.test.ts
import { describe, it, expect } from "vitest";
import { parseDate } from "./parseDate";

describe("parseDate", () => {
  it("should parse ISO date string", () => {
    const result = parseDate("2024-03-04T10:00:00Z");
    expect(result).toBeInstanceOf(Date);
  });

  it("should throw error for invalid date", () => {
    expect(() => parseDate("invalid")).toThrow();
  });
});

Mocking Prisma

Use Prismock for Prisma mocking:
import { PrismaClient } from "@prisma/client";
import { beforeEach } from "vitest";
import { mockDeep, mockReset } from "vitest-mock-extended";

const prismaMock = mockDeep<PrismaClient>();

beforeEach(() => {
  mockReset(prismaMock);
});

// Mock Prisma queries
prismaMock.booking.findUnique.mockResolvedValue({
  id: 1,
  title: "Test Booking",
  startTime: new Date(),
  // ...
});

Mocking Fetch

Use vitest-fetch-mock:
import { describe, it, expect, beforeAll } from "vitest";
import { enableFetchMocks } from "vitest-fetch-mock";

beforeAll(() => {
  enableFetchMocks();
});

it("should fetch data from API", async () => {
  fetchMock.mockResponseOnce(JSON.stringify({ data: "test" }));
  
  const response = await fetch("/api/test");
  const data = await response.json();
  
  expect(data).toEqual({ data: "test" });
});

E2E Testing with Playwright

Running E2E Tests

# Run all E2E tests
yarn test-e2e

# Run web app E2E tests
yarn e2e

# Run app-store E2E tests
yarn test-e2e:app-store

# Run embed E2E tests
yarn test-e2e:embed

# View test report
yarn playwright show-report test-results/reports/playwright-html-report

Installing Playwright Browsers

If you encounter browser errors:
npx playwright install
Error example:
Executable doesn't exist at /Users/alice/Library/Caches/ms-playwright/chromium-1048/chrome-mac/Chromium.app/Contents/MacOS/Chromium

Writing E2E Tests

Create test files with .e2e.ts suffix:
// File: apps/web/tests/bookings/create-booking.e2e.ts
import { expect, test } from "@playwright/test";

test.describe("Booking Creation", () => {
  test("should create a new booking", async ({ page }) => {
    // Navigate to booking page
    await page.goto("/free/30min");
    
    // Select time slot
    await page.click("[data-testid=time-slot]:first-child");
    
    // Fill booking form
    await page.fill("[name=name]", "John Doe");
    await page.fill("[name=email]", "[email protected]");
    
    // Submit booking
    await page.click("[data-testid=confirm-booking]");
    
    // Verify success message
    await expect(page.locator("text=Booking confirmed")).toBeVisible();
  });
});

E2E Test Configuration

Set environment variables in .env:
NEXT_PUBLIC_IS_E2E=1
E2E_TEST_MAILHOG_ENABLED=1
NEXTAUTH_URL=http://localhost:3000

E2E Test Utilities

Cal.com provides E2E test utilities in packages/testing/:
import { test } from "@calcom/test/e2e/lib/fixtures";
import { createBooking } from "@calcom/test/e2e/lib/helpers";

test("should reschedule booking", async ({ page, users }) => {
  const user = await users.create();
  const booking = await createBooking({ userId: user.id });
  
  // Test reschedule flow
});

Type Checking

Running Type Checks

# Type check all packages
yarn type-check

# Type check for CI (with force flag)
yarn type-check:ci --force
Always run yarn type-check:ci --force before concluding that CI failures are unrelated to your changes.

Fixing Type Errors

  1. Run yarn prisma generate if you see missing enum/type errors
  2. Fix type errors before test failures - they’re often the root cause
  3. Never use as any to bypass type errors

Integration Testing

Testing tRPC Endpoints

import { describe, it, expect } from "vitest";
import { createContextInner } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";

describe("Bookings Router", () => {
  it("should create a booking", async () => {
    const ctx = await createContextInner({ session: mockSession });
    const caller = appRouter.createCaller(ctx);
    
    const booking = await caller.viewer.bookings.create({
      eventTypeId: 1,
      startTime: "2024-03-04T10:00:00Z",
      timeZone: "UTC",
    });
    
    expect(booking).toBeDefined();
    expect(booking.id).toBeGreaterThan(0);
  });
});

Testing API Routes

import { describe, it, expect } from "vitest";
import { createMocks } from "node-mocks-http";
import handler from "@/pages/api/bookings";

describe("/api/bookings", () => {
  it("should return bookings for authenticated user", async () => {
    const { req, res } = createMocks({
      method: "GET",
      headers: {
        authorization: "Bearer test-token",
      },
    });
    
    await handler(req, res);
    
    expect(res._getStatusCode()).toBe(200);
    expect(res._getJSONData()).toHaveProperty("bookings");
  });
});

Database Testing

Using Test Database

Set up a separate test database:
# .env.test
DATABASE_URL="postgresql://user:pass@localhost:5432/calcom_test"

Database Seeding

Seed the database for testing:
# Seed with default data
yarn db-seed

# View seeded data
yarn db-studio

Reset Database Between Tests

import { beforeEach } from "vitest";
import { prisma } from "@calcom/prisma";

beforeEach(async () => {
  // Clean up database
  await prisma.booking.deleteMany();
  await prisma.user.deleteMany();
});

Testing Commands Reference

Unit Tests

TZ=UTC yarn test                 # Run all unit tests
yarn tdd                          # Watch mode
TZ=UTC yarn test:ui               # Run with UI

E2E Tests

yarn test-e2e                     # All E2E tests (with seeding)
yarn e2e                          # Web app E2E tests
yarn test-e2e:app-store           # App store E2E tests
yarn test-e2e:embed               # Embed E2E tests
yarn test-e2e:embed-react         # Embed React E2E tests
npx playwright install            # Install browsers

Type Checking

yarn type-check                   # Type check all packages
yarn type-check:ci --force        # Type check for CI

Linting & Formatting

yarn lint                         # Run linter
yarn lint:fix                     # Fix linting issues
yarn biome check --write .        # Format and lint with Biome

Test Coverage

Generate test coverage reports:
TZ=UTC yarn test --coverage
View coverage report in coverage/index.html.

Continuous Integration

Cal.com uses GitHub Actions for CI/CD:

CI Workflow

  1. Lint - Check code formatting and style
  2. Type Check - Verify TypeScript types
  3. Unit Tests - Run Vitest tests
  4. E2E Tests - Run Playwright tests
  5. Build - Ensure production build succeeds

Local Pre-Push Checks

Run these commands before pushing:
# Type check
yarn type-check:ci --force

# Lint and format
yarn lint:fix

# Run unit tests
TZ=UTC yarn test

# Build
yarn build

Testing Best Practices

Test Organization

packages/
├── lib/
│   ├── date/
│   │   ├── parseDate.ts
│   │   └── parseDate.test.ts
│   └── validation/
│       ├── email.ts
│       └── email.test.ts
Place test files next to the code they test for better discoverability.

Test Naming

// Good - Descriptive test names
it("should create booking when user has available time slots", () => {
  // ...
});

it("should throw error when booking conflicts with existing booking", () => {
  // ...
});

// Bad - Vague test names
it("works", () => {
  // ...
});

it("test booking", () => {
  // ...
});

Arrange-Act-Assert Pattern

it("should cancel booking and send notification", async () => {
  // Arrange
  const booking = await createTestBooking();
  const emailSpy = vi.spyOn(emailService, "send");
  
  // Act
  await cancelBooking(booking.id);
  
  // Assert
  expect(booking.status).toBe("CANCELLED");
  expect(emailSpy).toHaveBeenCalledWith({
    to: booking.attendeeEmail,
    template: "booking_cancelled",
  });
});

Test Independence

Each test should be independent:
// Good - Independent tests
describe("Booking Service", () => {
  beforeEach(() => {
    // Reset state before each test
    resetDatabase();
  });
  
  it("test 1", () => { /* ... */ });
  it("test 2", () => { /* ... */ });
});

// Bad - Tests depend on each other
describe("Booking Service", () => {
  let bookingId;
  
  it("creates booking", () => {
    bookingId = createBooking(); // Sets shared state
  });
  
  it("cancels booking", () => {
    cancelBooking(bookingId); // Depends on previous test
  });
});

Debugging Tests

Playwright Debug Mode

PLAYWRIGHT_HEADLESS=false yarn e2e

Playwright Inspector

PLAYWRIGHT_DEBUG=1 yarn e2e

Vitest Debug Mode

import { it } from "vitest";

it.only("debug this test", () => {
  // Add debugger statement
  debugger;
  // ...
});
Run with Node inspector:
node --inspect-brk ./node_modules/.bin/vitest

Testing Checklist

Before committing:

Common Testing Issues

Issue: Timezone Inconsistencies

Solution: Always set TZ=UTC when running tests:
TZ=UTC yarn test

Issue: Prisma Client Not Generated

Solution: Regenerate Prisma types:
yarn prisma generate

Issue: Playwright Browsers Not Installed

Solution: Install browsers:
npx playwright install

Issue: Port Already in Use

Solution: Kill process on port 3000:
lsof -ti:3000 | xargs kill -9

Resources

Next Steps

Build docs developers (and LLMs) love