hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Testing APIs with @hectoday/http

Why Test

  • What Testing Gives You
  • Types of Tests
  • Project Setup

Unit Testing

  • Testing Pure Functions
  • Testing Zod Schemas
  • Testing Business Logic

Integration Testing

  • Testing Route Handlers
  • Testing GET Endpoints
  • Testing POST Endpoints
  • Testing Error Responses
  • Testing Authentication

Test Helpers

  • Factories and Fixtures
  • Test Database Isolation
  • Request Helpers

Advanced Testing

  • Mocking External Services
  • Testing Background Jobs
  • Testing Edge Cases

Putting It All Together

  • Test Organization
  • Checklist and Capstone

Testing Zod Schemas

Why test schemas

Zod schemas define what your API accepts. If a schema is wrong — allows invalid data or rejects valid data — the bug affects every request. Testing schemas ensures the contract is correct.

Testing valid input

// tests/unit/schemas.test.ts
import { describe, test, expect } from "vitest";
import { CreateBookV2 } from "../../src/v2/schemas.js";

describe("CreateBookV2", () => {
  const validBook = {
    title: "Kindred",
    authorId: "550e8400-e29b-41d4-a716-446655440000",
    genre: "science-fiction",
  };

  test("accepts valid input", () => {
    const result = CreateBookV2.safeParse(validBook);
    expect(result.success).toBe(true);
  });

  test("accepts valid input with optional description", () => {
    const result = CreateBookV2.safeParse({ ...validBook, description: "A great book" });
    expect(result.success).toBe(true);
  });

  test("accepts valid input without optional fields", () => {
    const result = CreateBookV2.safeParse(validBook);
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.description).toBeUndefined();
    }
  });
});

[!NOTE] Always use safeParse in tests, not parse. safeParse returns a result object — your test checks result.success. parse throws, which makes the test harder to assert against and produces less readable test output.

Testing invalid input

describe("CreateBookV2 rejection", () => {
  test("rejects missing title", () => {
    const result = CreateBookV2.safeParse({
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
    });
    expect(result.success).toBe(false);
  });

  test("rejects empty title", () => {
    const result = CreateBookV2.safeParse({
      title: "",
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
    });
    expect(result.success).toBe(false);
  });

  test("rejects invalid authorId (not a UUID)", () => {
    const result = CreateBookV2.safeParse({
      title: "Kindred",
      authorId: "not-a-uuid",
      genre: "fiction",
    });
    expect(result.success).toBe(false);
  });

  test("rejects invalid genre (not in enum)", () => {
    const result = CreateBookV2.safeParse({
      title: "Kindred",
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "romance",
    });
    expect(result.success).toBe(false);
  });
});

Testing edge cases

describe("CreateBookV2 edge cases", () => {
  test("trims whitespace from title", () => {
    const result = CreateBookV2.safeParse({
      title: "  Kindred  ",
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.title).toBe("Kindred");
    }
  });

  test("rejects whitespace-only title", () => {
    const result = CreateBookV2.safeParse({
      title: "   ",
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
    });
    expect(result.success).toBe(false);
  });

  test("rejects title exceeding max length", () => {
    const result = CreateBookV2.safeParse({
      title: "A".repeat(201),
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
    });
    expect(result.success).toBe(false);
  });

  test("strips extra fields", () => {
    const result = CreateBookV2.safeParse({
      title: "Kindred",
      authorId: "550e8400-e29b-41d4-a716-446655440000",
      genre: "fiction",
      extraField: "should be stripped",
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect((result.data as any).extraField).toBeUndefined();
    }
  });
});

Testing query schemas with coercion

import { BookQueryV2 } from "../../src/v2/schemas.js";

describe("BookQueryV2", () => {
  test("coerces string page to number", () => {
    const result = BookQueryV2.safeParse({ page: "2" });
    expect(result.success).toBe(true);
    if (result.success) expect(result.data.page).toBe(2);
  });

  test("applies defaults for missing fields", () => {
    const result = BookQueryV2.safeParse({});
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.page).toBe(1);
      expect(result.data.limit).toBe(20);
      expect(result.data.sort).toBe("title");
    }
  });

  test("rejects page below 1", () => {
    const result = BookQueryV2.safeParse({ page: "0" });
    expect(result.success).toBe(false);
  });

  test("rejects limit above 100", () => {
    const result = BookQueryV2.safeParse({ limit: "200" });
    expect(result.success).toBe(false);
  });
});

Exercises

Exercise 1: Write tests for a ContactSchema: valid input, missing required fields, invalid email, whitespace-only name.

Exercise 2: Test a query schema with coercion. Verify strings are converted to numbers, defaults are applied.

Exercise 3: Test that extra fields are stripped by default. Test that .strict() rejects them.

Why use safeParse instead of parse in tests?

← Testing Pure Functions Testing Business Logic →

© 2026 hectoday. All rights reserved.