Building Out An Endpoint With TDD
We've laid all the groundwork. Now let's use our foundation and build out an endpoint with more functionality.
We're going to allow our API consumers to POST
in data. That data needs to be validated. If the validation process fails, we will return a nice, helpful message telling the consumer exactly how their request failed.
If the submitted data passes the validation process, we will save that data off to our persistent storage, which in our case will be Redis.
Let's start by defining the test / specification of exactly what a happy path POST
request looks like:
touch __tests__/routes/codereviewvideos.ts
And into that file:
import server from "../../src/server";
const request = require("supertest");
afterEach((done) => {
server.close();
done();
});
describe("routes/codereviewvideos", () => {
it("should allow adding a game to the list", async () => {
const response = await request(server)
.post("/codereviewvideos")
.send({ game: "World of Warships" });
expect(response.status).toEqual(201);
expect(response.type).toEqual("application/json");
expect(response.body).toEqual({
games: [
"World of Warships",
]
});
});
});
Running the tests, we should expect this to fail:
FAIL __tests__/routes/codereviewvideos.ts
routes/codereviewvideos
✕ should allow adding a game to the list (41ms)
● routes/codereviewvideos › should allow adding a game to the list
expect(received).toEqual(expected) // deep equality
Expected: 201
Received: 404
13 | .send({ game: "World of Warships" });
14 |
> 15 | expect(response.status).toEqual(201);
| ^
16 | expect(response.type).toEqual("application/json");
17 | expect(response.body.data).toEqual({
18 | games: [
at Object.<anonymous> (__tests__/routes/codereviewvideos.ts:15:29)
at step (__tests__/routes/codereviewvideos.ts:32:23)
at Object.next (__tests__/routes/codereviewvideos.ts:13:53)
at fulfilled (__tests__/routes/codereviewvideos.ts:4:58)
This is expected. After all, we do not yet have a route that handles this kind of request. Let's fix that.
touch src/routes/codereviewvideos.ts
Into which:
import {Context} from "koa";
import Router from "koa-router";
const router = new Router();
router.post(`/codereviewvideos`, async (ctx: Context) => {
try {
ctx.status = 201;
ctx.body = {
games: [
"World of Warships",
]
};
} catch (err) {
console.error(err);
}
});
export default router;
I'd recommend reading the API documentation for the router, as it is surprisingly versatile.
And also, don't forget to use
the new route information in your server's middleware stack:
// src/server.ts
import codeReviewVideosRoutes from "./routes/codereviewvideos";
// ...
app.use(codeReviewVideosRoutes.routes());
After which we should have a second passing test.
This is the bare bones needed to make the test pass. But of course, this is not a real implementation. We can prove this by sending in anything other than { game: "World of Warships" }
, e.g. { game: "Battlefield" }
, and we still get back a list containing the hardcoded "World of Warships" value.
And even if we fixed that issue, which we easily could:
import server from "../../src/server";
const request = require("supertest");
afterEach((done) => {
server.close();
done();
});
describe("routes/codereviewvideos", () => {
const games = [
"World of Warships",
"Battlefield",
];
games.forEach((game: string) => {
it(`should allow adding a game to the list - ${game}`, async () => {
const response = await request(server)
.post("/codereviewvideos")
.send({ game });
expect(response.status).toEqual(201);
expect(response.type).toEqual("application/json");
expect(response.body).toEqual({
games: [
game,
]
});
});
});
});
Which highlights the fail:
FAIL __tests__/routes/codereviewvideos.ts
routes/codereviewvideos
✓ should allow adding a game to the list - World of Warships (11ms)
✕ should allow adding a game to the list - Battlefield (4ms)
● routes/codereviewvideos › should allow adding a game to the list - Battlefield
expect(received).toEqual(expected) // deep equality
- Expected
+ Received
Object {
"games": Array [
- "Battlefield",
+ "World of Warships",
],
}
22 | expect(response.status).toEqual(201);
23 | expect(response.type).toEqual("application/json");
> 24 | expect(response.body).toEqual({
| ^
25 | games: [
26 | game,
27 | ]
at Object.<anonymous> (__tests__/routes/codereviewvideos.ts:24:29)
at step (__tests__/routes/codereviewvideos.ts:32:23)
at Object.next (__tests__/routes/codereviewvideos.ts:13:53)
at fulfilled (__tests__/routes/codereviewvideos.ts:4:58)
And we can fix, again, baloney:
import {Context} from "koa";
import Router from "koa-router";
const router = new Router();
router.post(`/codereviewvideos`, async (ctx: Context) => {
try {
ctx.status = 201;
ctx.body = {
games: [
ctx.request.body.game,
]
};
} catch (err) {
console.error(err);
}
});
export default router;
TDD is all about doing the least amount of possible work to get a pass. And in this way we are doing the very least amount of work to satisfy the tests. But we know this isn't quite right.
Let's reliably prove this system does not yet work the way we expect:
it(`should keep track of all games added to the list'`, async () => {
const data1 = { game: "Half Life 3" };
const response1 = await request(server)
.post("/codereviewvideos")
.send(data1);
expect(response1.status).toEqual(201);
expect(response1.type).toEqual("application/json");
expect(response1.body).toEqual({
games: [
data1.game,
]
});
const data2 = { game: "FSX 2020" };
const response2 = await request(server)
.post("/codereviewvideos")
.send(data2);
expect(response2.status).toEqual(201);
expect(response2.type).toEqual("application/json");
expect(response2.body).toEqual({
games: [
data1.game,
data2.game,
]
});
});
And now, we get the hardcore computerised truth:
routes/codereviewvideos
✓ should allow adding a game to the list - World of Warships (63ms)
✓ should allow adding a game to the list - Battlefield (18ms)
✕ should keep track of all games added to the list' (26ms)
● routes/codereviewvideos › should keep track of all games added to the list'
expect(received).toEqual(expected) // deep equality
- Expected
+ Received
Object {
"games": Array [
- "Half Life 3",
"FSX 2020",
],
}
51 | expect(response2.status).toEqual(201);
52 | expect(response2.type).toEqual("application/json");
> 53 | expect(response2.body).toEqual({
| ^
54 | games: [
55 | data1.game,
56 | data2.game,
at Object.<anonymous> (__tests__/routes/codereviewvideos.ts:53:28)
at step (__tests__/routes/codereviewvideos.ts:32:23)
at Object.next (__tests__/routes/codereviewvideos.ts:13:53)
at fulfilled (__tests__/routes/codereviewvideos.ts:4:58)
This is great. We know that our system isn't working. And we also have a way to prove that when it is working, it has fundamentally changed behaviour.
Let's continue solving this challenge in the next video.