Testing Strategy
Cal.com uses a comprehensive testing strategy:
- Unit Tests - Vitest for business logic and utilities
- Integration Tests - Testing API endpoints and services
- E2E Tests - Playwright for end-to-end user flows
- 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:
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
- Run
yarn prisma generate if you see missing enum/type errors
- Fix type errors before test failures - they’re often the root cause
- 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
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
- Lint - Check code formatting and style
- Type Check - Verify TypeScript types
- Unit Tests - Run Vitest tests
- E2E Tests - Run Playwright tests
- 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:
Issue: Prisma Client Not Generated
Solution: Regenerate Prisma types:
Issue: Playwright Browsers Not Installed
Solution: Install browsers:
Issue: Port Already in Use
Solution: Kill process on port 3000:
lsof -ti:3000 | xargs kill -9
Resources
Next Steps