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?