From Mocks To The Real Deal


By replacing the real storage implementation with a mocked equivalent, we have managed to get a single test to a passing state. We'd already covered that this was the simplest happy path as in reality if we had no games in our list, we could simply return whatever was "posted in" as the only value to return in the response.

Things get more complex when we have existing data in the list, and want to add something new.

Here's our existing test:

  it(`should keep track of all games added to the list'`, async () => {
    const game1 = { name: "Half Life 3" };
    const response1 = await request(server)
      .post("/codereviewvideos")
      .send(game1);

    expect(response1.status).toEqual(201);
    expect(response1.type).toEqual("application/json");
    expect(response1.body).toEqual({
      games: [
        game1.name,
      ]
    });

    const game2 = { name: "FSX 2020" };
    const response2 = await request(server)
      .post("/codereviewvideos")
      .send(game2);

    expect(response2.status).toEqual(201);
    expect(response2.type).toEqual("application/json");
    expect(response2.body).toEqual({
      games: [
        game1.name,
        game2.name,
      ]
    });
  });

This forces us to implement the ability to add new games to the list.

Let's cover how to do this in a test:

  it.only(`should keep track of all games added to the list'`, async () => {

    // this is all new
    const list_of_games: string[] = [];

    const mockGet = jest.fn((list: string) => Promise.resolve(list_of_games));
    const mockAdd = jest.fn((list: string, game: string) => {
      list_of_games.push(game);
      return Promise.resolve(list_of_games.length > 0)
    });

    storage.redisStorage = jest.fn(() => {
      return {
        get: mockGet,
        add: mockAdd,
        remove: (list: string) => Promise.resolve(false),
      }});

    const game1 = { name: "Half Life 3" };
    const response1 = await request(server)
      .post("/codereviewvideos")
      .send(game1);

    expect(response1.status).toEqual(201);
    expect(response1.type).toEqual("application/json");
    expect(response1.body).toEqual({
      games: list_of_games.slice(0,1)
    });

    const game2 = { name: "FSX 2020" };
    const response2 = await request(server)
      .post("/codereviewvideos")
      .send(game2);

    expect(response2.status).toEqual(201);
    expect(response2.type).toEqual("application/json");
    expect(response2.body).toEqual({
      games: list_of_games
    });

    // this is new also
    expect(storage.add).toHaveBeenCalledTimes(2);
    expect(storage.get).toHaveBeenCalledTimes(2);
  });

Essentially the same as before. We define the mock functions that have exactly the same signature that is found on the interface.

In the case of our mock we just use an in-memory data store - an array - to track the values added to the list over the course of our tests. Good enough.

We add some assertions around what the list should be after adding a single game, and what the list should be after adding multiple games. And we double check that the storage functions were called the expected number of times.

We could do more here, but really, this is good enough to validate our simple endpoint.

Making this endpoint work (and satisfy the test) involves expanding on what we have already done:

import {Context} from "koa";
import {AddGameRequest} from '../request/AddGameRequest';
import { validate } from "class-validator";
import { redisStorage as storage } from '../storage/redis';
import Router from "koa-router";

const router = new Router();

router.post(`/codereviewvideos`, async (ctx: Context) => {
  try {
    const validatorOptions = {};

    const game = new AddGameRequest();
    game.name = ctx.request.body.name || '';

    const errors = await validate(game, validatorOptions);

    if (errors.length > 0) {
      ctx.status = 400;
      ctx.body = {
        status: 'error',
        data: errors
      };

      return ctx;
    }

    const list = 'game_list';

    storage.add(list, game.name);

    ctx.status = 201;
    ctx.body = {
      games: await storage.get(list)
    };
  } catch (err) {
    console.error(err);
  }
});

export default router;

The list name is extracted to a separate variable as it now used twice. Aside from this, business as usual.

At this point, all four of our tests should be passing:

 PASS  __tests__/routes/codereviewvideos.ts
  routes/codereviewvideos
    ✓ should allow adding a game to the list - World of Warships (29ms)
    ✓ should allow adding a game to the list - Battlefield (8ms)
    ✓ should keep track of all games added to the list' (14ms)
    ✓ should return a validation failure if the game data is incorrect (5ms)

There is a bug here. The same game name can be added to the list multiple times.

As a challenge to you: add one or more tests to cover this possibility, and create an implementation to solve that problem. Post your solution in the comments below.

Episodes