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.

Episodes