Bringing It All Together - Part 2


We're part way through the implementation of our POST endpoint, and previously we said we had the following commented code as a structure to work through:

import {Context} from "koa";
import Router from "koa-router";

const router = new Router();

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

    // DONE: validate the incoming request

    // DONE:  - return early if invalid

    // save the new game to storage

    // get all the games we know about

    ctx.status = 201;
    ctx.body = {
      games: [
        // ... a list of games
      ]
    };
  } catch (err) {
    console.error(err);
  }
});

export default router;

We can be confident we have a valid piece of data if we haven't already returned an error to the API consumer by this point.

What we need to do next is to use our redisStorage object to persist off the submitted data to our Redis instance, and then get the full list of games back to return as part of our response.

The caveat is that we want this whole process to work without Redis when in the test environment. In other words, we do not want to have to spin up a Redis instance just to run our unit tests.

This means we must mock our storage implementation during testing.

Mocking with TypeScript and Jest

Writing tests can sometimes reveal issues with the existing design / architecture of your system.

This is a hurdle which must be overcome, and can sometimes feel so frustrating that testing is pushed aside. This is often the case in the real world, when project managers want features shipped to some ever shortening deadline, and corners get cut in order to deliver a "working" solution.

Typically in these cases, at some point later in time, this tech debt comes back to bite the project as a whole, and any short term gains made from skipping the testing phase are swallowed up (and then some) fixing the resulting weird bugs.

Unfortunately, TypeScript does make this process even more frustrating. At least, it does in my humble opinion. Always pros and cons.

One such immediate hurdle is the TypeScript compilation errors we will encounter during testing - or mocking specifically.

We need to make a change to our package.json file:

  ...
  "jest": {
    ...
    "globals": {
      "ts-jest": {
        "diagnostics": {
          "ignoreCodes": [2540]
        }
      }
    }
  },
  ...

TypeScript code 2540 throws up compilation error messages like:

error TS2540: Cannot assign to 'someProperty' because it is a constant or a read-only property.

You can see this problem in action in the video.

Mocking Storage

Our storage interface provides us with the following:

export interface IStorage {
  get: (list: string) => Promise<string[]>;
  add: (list: string, name: string) => Promise<boolean>;
  remove: (list: string, name: string) => Promise<boolean>;
}

We will need both add and get during this process.

To reduce noise, I'm going to focus on only one test to begin with. This will be our original test:

  const games = [
    "World of Warships",
    // "Battlefield",
  ];

  games.forEach((game: string) => {
    it.only(`should allow adding a game to the list - ${game}`, async () => {
      const response = await request(server)
        .post("/codereviewvideos")
        .send({ name: game });

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

Having the forEach sprinkled in is going to make this run twice. That's cool in so much as we test multiple scenarios. But it's also not great when exploring. I've commented out a single game, so this only runs once.

I'm also using it.only( to ensure only this test runs.

Jest really is quality.

First Attempt

Whilst we haven't yet used the redisStorage object in our POST route, we can take a shot at how this ought to work based on the comments above.

We can use Jest's Mock Implementations functionality to help us out here. There are potentially better ways to do this. I would prefer to go down a more typical dependency injection route, which really does make the art of testing dramatically easier.

But what we are about to cover is extremely common out there in the real world. At least, it has been on many JavaScript projects I've seen in UK corporate environments.

import * as storage from "../../src/storage/redis";

jest.mock("../../src/storage/redis");

// ...

it.only(`should allow adding a game to the list - ${game}`, async () => {

  // this is new
  const mockGet = jest.fn((list: string) => Promise.resolve([game]));

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

  const response = await request(server)
    .post("/codereviewvideos")
    .send({ name: game });

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

  // and so is this
  expect(mockGet).toHaveBeenCalled();
});

In short we can use Jest's mock implementations functionality to replace the real requirement / import, with a complete replacement that looks, feels, and acts just like the real thing.

TypeScript helps us here also be ensuring that our mock must behave the exact same way as the real implementation. The function parameters must be identical to those defined on the interface, and so must the return type.

As covered in the video, we must make some changes to redis.ts in order to mock the redisStorage implementation:

import { config } from "../config";
import * as Interfaces from '../types/interfaces';

const redis = require("redis");

const {promisify} = require('util');

export function redisStorage(): Interfaces.IStorage {

  const client = redis.createClient(config.redis);

  const rpush = promisify(client.rpush).bind(client);
  const lrem = promisify(client.lrem).bind(client);
  const lrange = promisify(client.lrange).bind(client);

  return {
    get: (list: string) => {
      return lrange(list, 0, -1)
        .then((val:string) => {
          console.log('i am here',val);
          return val
        })
        .catch((e: Error)  => []);
    },
    add: (list: string, name: string) => {
      return rpush(list, name)
        .then((val: number) => val > 0)
        .catch((e: Error)  => false);
    },
    remove: (list: string, name: string) => {
      return lrem(list, 0, name)
        .then((val: number) => val > 0)
        .catch((e: Error)  => false);
    }
  }
}

We also need to update src/routes/codereviewvideos.ts to use redisStorage:

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;
    }

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

export default router;

This isn't perfect, but it gets us to some passing tests:

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

Let's keep moving with this, and see how far we can get.

Episodes