Testing an API endpoint with Deno
October 8, 2023
Deno has become one of my favorite tools for web development. It’s refreshing to have all of the essential features readily available without needing to reach for third-party libraries. Recently, I’ve been working on an API-first version of Waveformr and landed on a way to test the endpoints that I’d like to share.
The endpoint
For the purposes of this article, I’m going to set up a basic HTTP GET
endpoint using Deno and Hono. But note, Deno and Hono here are implementation details. I’m only using them because they are easy to set up, but the tests won’t care what we’re using to implement the endpoint.
The endpoint implementation will look like this:
import { Hono } from "https://deno.land/x/hono/mod.ts";
const app = new Hono();
// The "database"
const books = new Map();
// Post a book with a name to create it
app.post("/book", async (c) => {
const body = await c.req.json<{ name: string }>();
const { name } = body;
const book = {
id: crypto.randomUUID(),
name,
};
books.set(book.id, book);
return c.json({ status: "ok", id: book.id });
});
// Look up a book by id
app.get("/book/:id", (c) => {
const book = books.get(c.req.param("id"));
if (book) {
return c.json({ status: "ok", book });
} else {
return c.json({ status: "error", message: "not found" }, 404);
}
});
export function run() {
return Deno.serve(app.fetch);
}
// Run this immediately if this is the entrypoint to the program
// Useful for testing because we can import this file and run it
// with a function call when we're ready
if (import.meta.main) {
run();
}
The test
The test code looks like this:
import { assert } from "https://deno.land/std@0.202.0/assert/assert.ts";
import { run } from "./main.ts";
import { assertEquals } from "https://deno.land/std@0.202.0/assert/mod.ts";
const PORT = 8000;
const url = (path: string) => new URL(`http://localhost:${PORT}/${path}`);
Deno.test("POST a new book", async (t) => {
// Pass an `AbortController` so that we can abort the server when we're done
const controller = new AbortController();
run({
port: PORT,
signal: controller.signal,
});
const resp = await fetch(url("book"), {
method: "POST",
body: JSON.stringify({
name: "The Great Gatsby",
}),
});
assertEquals(resp.status, 200);
const { id, status } = await resp.json();
assertEquals(status, "ok");
assert(typeof id === "string");
const newBookResp = await fetch(url(`book/${id}`));
assertEquals(newBookResp.status, 200);
const newBookJson = await newBookResp.json();
assertEquals(newBookJson.book.name, "The Great Gatsby");
controller.abort();
});
Running the tests will give an output that looks a bit like this:
Check file:///Users/nickball/code/deno-endpoint-testing/main_test.ts
running 1 test from ./main_test.ts
POST a new book ...
------- output -------
Listening on http://localhost:8000/
----- output end -----
POST a new book ... ok (8ms)
ok | 1 passed | 0 failed (26ms)
What I like about this method is that we’re hitting the endpoint like a real user would and using standard Web APIs. Deno has this all built in, so it feels very natural. One note here is that we’re passing an AbortController
into the server so that we can manually kill the server after we’re done testing it.
That’s it!