Testing Edge Cases
Beyond the happy path
Happy path tests verify that valid input produces correct output. Edge case tests verify that unusual, extreme, or malicious input is handled gracefully — not with crashes, data corruption, or security vulnerabilities.
Empty and whitespace inputs
describe("edge cases: empty input", () => {
test("rejects empty string title", async () => {
const response = await app.fetch(
post("/v2/books", {
title: "",
authorId: "a1",
genre: "fiction",
}),
);
expect(response.status).toBe(400);
});
test("rejects whitespace-only title", async () => {
const response = await app.fetch(
post("/v2/books", {
title: " ",
authorId: "a1",
genre: "fiction",
}),
);
expect(response.status).toBe(400);
});
test("handles empty query parameter gracefully", async () => {
const response = await app.fetch(get("/v2/books?genre="));
// Should either ignore the empty genre or return empty results
expect([200, 400]).toContain(response.status);
});
}); [!NOTE] The Zod course’s
.trim().min(1)pattern catches whitespace-only strings. These tests verify the schema works correctly in the route context.
Special characters
describe("edge cases: special characters", () => {
test("handles unicode in title", async () => {
const author = createAuthor();
const response = await app.fetch(
post("/v2/books", {
title: "日本語の本",
authorId: author.id,
genre: "fiction",
}),
);
expect(response.status).toBe(201);
const book = await response.json();
expect(book.title).toBe("日本語の本");
});
test("handles emoji in description", async () => {
const author = createAuthor();
const response = await app.fetch(
post("/v2/books", {
title: "Good Book",
authorId: author.id,
genre: "fiction",
description: "This book is great! 📚🌟",
}),
);
expect(response.status).toBe(201);
});
test("query param with special characters", async () => {
const response = await app.fetch(get("/v2/books?genre=science-fiction"));
expect(response.status).toBe(200);
});
}); Large payloads
describe("edge cases: large payloads", () => {
test("rejects title exceeding max length", async () => {
const response = await app.fetch(
post("/v2/books", {
title: "A".repeat(10000),
authorId: "a1",
genre: "fiction",
}),
);
expect(response.status).toBe(400);
});
test("rejects description exceeding max length", async () => {
const response = await app.fetch(
post("/v2/books", {
title: "Book",
authorId: "a1",
genre: "fiction",
description: "X".repeat(100000),
}),
);
expect(response.status).toBe(400);
});
}); Boundary values
describe("edge cases: boundary values", () => {
test("accepts rating of exactly 1 (minimum)", async () => {
const book = createBook();
const response = await app.fetch(
post(`/v2/books/${book.id}/reviews`, {
rating: 1,
userId: "u1",
}),
);
expect(response.status).toBe(201);
});
test("accepts rating of exactly 5 (maximum)", async () => {
const book = createBook();
const response = await app.fetch(
post(`/v2/books/${book.id}/reviews`, {
rating: 5,
userId: "u1",
}),
);
expect(response.status).toBe(201);
});
test("rejects rating of 0 (below minimum)", async () => {
const book = createBook();
const response = await app.fetch(
post(`/v2/books/${book.id}/reviews`, {
rating: 0,
userId: "u1",
}),
);
expect(response.status).toBe(400);
});
test("rejects rating of 6 (above maximum)", async () => {
const book = createBook();
const response = await app.fetch(
post(`/v2/books/${book.id}/reviews`, {
rating: 6,
userId: "u1",
}),
);
expect(response.status).toBe(400);
});
test("page 1 is valid (minimum)", async () => {
const response = await app.fetch(get("/v2/books?page=1"));
expect(response.status).toBe(200);
});
test("page 0 is invalid", async () => {
const response = await app.fetch(get("/v2/books?page=0"));
expect(response.status).toBe(400);
});
}); Boundary value testing checks the edges of valid ranges: the minimum valid value, the maximum valid value, one below minimum, and one above maximum.
Type confusion
describe("edge cases: type confusion", () => {
test("rejects number as title", async () => {
const response = await app.fetch(
post("/v2/books", {
title: 42,
authorId: "a1",
genre: "fiction",
}),
);
expect(response.status).toBe(400);
});
test("rejects string as rating", async () => {
const book = createBook();
const response = await app.fetch(
post(`/v2/books/${book.id}/reviews`, {
rating: "five",
userId: "u1",
}),
);
expect(response.status).toBe(400);
});
test("rejects null body", async () => {
const response = await app.fetch(
new Request("http://localhost/v2/books", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "null",
}),
);
expect(response.status).toBe(400);
});
}); Exercises
Exercise 1: Write boundary value tests for all numeric fields (ratings, page, limit). Test min, max, below min, above max.
Exercise 2: Write special character tests. Create a book with unicode, emoji, and HTML characters in the title.
Exercise 3: Write type confusion tests. Send numbers where strings are expected, strings where numbers are expected, null, and undefined.
Why test boundary values (min, max, min-1, max+1)?