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 Pure Functions

What makes a function pure

A pure function takes input and returns output. No database, no HTTP, no side effects. Given the same input, it always returns the same output. These are the easiest functions to test.

The API Versioning course built transformers: formatBookV1 and formatBookV2. These are pure functions — a database row in, a formatted object out. Perfect for unit testing.

Testing a transformer

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

const sampleRow = {
  id: "book-1",
  title: "Kindred",
  genre: "science-fiction",
  description: "A novel about time travel and slavery",
  created_at: "2024-01-15T10:30:00",
  author_id: "author-3",
  author_name: "Octavia Butler",
  avg_rating: 4.5,
  review_count: 2,
};

describe("formatBookV2", () => {
  test("nests author as an object with id and name", () => {
    const result = formatBookV2(sampleRow);
    expect(result.author).toEqual({ id: "author-3", name: "Octavia Butler" });
  });

  test("nests ratings as an object with average and count", () => {
    const result = formatBookV2(sampleRow);
    expect(result.ratings).toEqual({ average: 4.5, count: 2 });
  });

  test("converts created_at to camelCase createdAt", () => {
    const result = formatBookV2(sampleRow);
    expect(result.createdAt).toBe("2024-01-15T10:30:00");
    expect((result as any).created_at).toBeUndefined();
  });

  test("handles null avg_rating", () => {
    const row = { ...sampleRow, avg_rating: null, review_count: 0 };
    const result = formatBookV2(row);
    expect(result.ratings).toEqual({ average: null, count: 0 });
  });

  test("handles null description", () => {
    const row = { ...sampleRow, description: null };
    const result = formatBookV2(row);
    expect(result.description).toBeNull();
  });
});

Anatomy of a test

test("nests author as an object with id and name", () => {
  // Arrange: set up the input
  const row = sampleRow;

  // Act: call the function
  const result = formatBookV2(row);

  // Assert: check the output
  expect(result.author).toEqual({ id: "author-3", name: "Octavia Butler" });
});

Arrange — Prepare the input. Act — Call the function. Assert — Check the result. This three-step pattern (Arrange-Act-Assert) applies to every test.

Common assertions

expect(value).toBe(42); // exact equality (===)
expect(value).toEqual({ a: 1, b: 2 }); // deep equality (objects, arrays)
expect(value).toBeDefined(); // not undefined
expect(value).toBeNull(); // is null
expect(value).toBeUndefined(); // is undefined
expect(value).toBeTruthy(); // truthy value
expect(value).toContain("hello"); // string contains or array includes
expect(value).toHaveLength(3); // array or string length
expect(fn).toThrow(); // function throws an error
expect(fn).toThrow("message"); // throws with specific message

toBe checks identity (===). toEqual checks deep equality — use it for objects and arrays.

Testing helper functions

// src/shared/helpers.ts
export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
}

export function paginate(total: number, page: number, limit: number) {
  return {
    total,
    page,
    limit,
    totalPages: Math.ceil(total / limit),
    hasNextPage: page * limit < total,
    hasPrevPage: page > 1,
  };
}
// tests/unit/helpers.test.ts
describe("slugify", () => {
  test("converts spaces to hyphens", () => {
    expect(slugify("Hello World")).toBe("hello-world");
  });

  test("removes special characters", () => {
    expect(slugify("Hello, World!")).toBe("hello-world");
  });

  test("handles multiple spaces", () => {
    expect(slugify("hello   world")).toBe("hello-world");
  });

  test("trims leading and trailing hyphens", () => {
    expect(slugify("--hello--")).toBe("hello");
  });
});

describe("paginate", () => {
  test("calculates totalPages", () => {
    expect(paginate(50, 1, 20).totalPages).toBe(3);
  });

  test("hasNextPage is true when more pages exist", () => {
    expect(paginate(50, 1, 20).hasNextPage).toBe(true);
  });

  test("hasNextPage is false on last page", () => {
    expect(paginate(50, 3, 20).hasNextPage).toBe(false);
  });

  test("hasPrevPage is false on first page", () => {
    expect(paginate(50, 1, 20).hasPrevPage).toBe(false);
  });

  test("hasPrevPage is true on second page", () => {
    expect(paginate(50, 2, 20).hasPrevPage).toBe(true);
  });
});

Exercises

Exercise 1: Write unit tests for formatBookV1. Test that it returns author_name (flat string), rating (single number), and snake_case fields.

Exercise 2: Write a slugify helper and test it with 5+ cases: spaces, special characters, multiple spaces, leading/trailing hyphens.

Exercise 3: Write a paginate helper and test edge cases: total=0, page=1 with limit=100 and only 5 items.

Why are pure functions the easiest to test?

← Project Setup Testing Zod Schemas →

© 2026 hectoday. All rights reserved.