Testing POST Endpoints
The POST testing pattern
POST tests verify: the resource is created, the response is 201 with the created resource, the Location header points to the new resource, and the data is persisted in the database.
Testing successful creation
describe("POST /v2/books", () => {
beforeEach(() => {
testDb.exec("DELETE FROM books; DELETE FROM authors;");
testDb.prepare("INSERT INTO authors (id, name) VALUES (?, ?)").run("a1", "Butler");
});
test("creates a book and returns 201", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
}),
);
const book = await response.json();
expect(response.status).toBe(201);
expect(book.title).toBe("Kindred");
expect(book.id).toBeDefined();
});
test("returns Location header", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
}),
);
const book = await response.json();
expect(response.headers.get("location")).toBe(`/v2/books/${book.id}`);
});
test("book is persisted in the database", async () => {
await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
}),
);
const books = testDb.prepare("SELECT * FROM books").all();
expect(books).toHaveLength(1);
expect((books[0] as any).title).toBe("Kindred");
});
test("created book appears in GET /v2/books", async () => {
await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
}),
);
const listResponse = await app.fetch(new Request("http://localhost/v2/books"));
const data = await listResponse.json();
expect(data).toHaveLength(1);
expect(data[0].title).toBe("Kindred");
});
}); The last test is a true integration test: create via POST, verify via GET. This catches bugs where the creation succeeds but the query does not return the new resource (wrong query, missing JOIN, etc.).
Testing with optional fields
test("creates book with description", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
description: "A novel about time travel",
}),
);
const book = await response.json();
expect(response.status).toBe(201);
expect(book.description).toBe("A novel about time travel");
});
test("creates book without optional description", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "science-fiction",
}),
);
const book = await response.json();
expect(response.status).toBe(201);
expect(book.description).toBeNull();
}); Testing validation failures
test("returns 400 for missing title", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
authorId: "a1",
genre: "science-fiction",
}),
);
expect(response.status).toBe(400);
});
test("returns 400 for invalid genre", async () => {
const response = await app.fetch(
jsonRequest("POST", "/v2/books", {
title: "Kindred",
authorId: "a1",
genre: "romance",
}),
);
expect(response.status).toBe(400);
const error = await response.json();
expect(error.error).toBeDefined();
}); [!NOTE] Testing validation failures here complements the schema unit tests from the previous section. Schema tests check that the schema rejects invalid input. Integration tests check that the route handler returns the correct HTTP status code and error format when the schema rejects input.
Exercises
Exercise 1: Test POST creation: 201 status, correct body, Location header, persisted in database.
Exercise 2: Test POST with and without optional fields. Verify both succeed with correct data.
Exercise 3: Test POST with invalid input. Verify 400 status code and error response.
Why test that the created resource appears in GET after POST?