circuit

How to Unit Test Express Middleware




If you use Test Driven Development for your Node.js project uses Express library, you need to write unit tests among other things. As I experienced, testing code parts which use third party libraries are usually difficult for less experienced developers. The key aspect of effective unit testing is define boundaries of the business logic to be tested.

Test the business logic, and nothing else

When I write unit tests including any structural items of the Express library, it produces me I have to avoid testing the library itself. It's not my job. Rather than only my business logic needs to be tested. As for the middleware, it's the logic inside the middleware function. The input parameters are coming from the Express library, so you need to mock or stub them.

Mock or stub only the needed parts coming from Express, nothing more

An Express middleware is a special function which has three parameters: a request, a response and a next function. These are coming from the library flow, and you just need to wire the middleware to the proper level (application, router, etc.).

Stay typed (use official Express types)

The most common problem with typescript is that a lack of knowledge of the type system that forces the programmer to try to bypass the own type system instead of taking advantage of its potential. Don't do that! Express provides you its types for the requests (Request), responses (Response) and even for the next function (NextFunction).

The only problem with Request and Response types when tests come into the picture is that they have so many required attributes which means you would have to define all of them to pass the type check. That wouldn't be effective at all. That's why you should learn the Utility types from TypeScript. These are great for customizing existing types for your needs.

In this case I would use just some parts of the Request and Response types, that's why the Partial utility type helps me this time. Partial with the generic type parameter means ‘some subset of the type comes as type parameter'.

Simple example — authorization header guard middleware

Let's see a simple example. A middleware checks if the request contains authorization header or not. If it is, you just let the flow runs through the middleware but if the header is missing send an error response immediately.

import { NextFunction, Request, Response } from "express";

const authorizationMiddleware = (
  request: Request,
  response: Response,
  next: NextFunction
): void => {
  if (!request.headers || !request.headers["authorization"]) {
    response.statusCode = 403;
    response.json({
      error: "Missing JWT token from the 'Authorization' header",
    });
  } else {
    next();
  }
};

export default authorizationMiddleware;

Here is the unit tests with Express Request, Response and Next function mocks.

import { NextFunction, Request, Response } from "express";
import authorizationMiddleware from "./authorization";

describe("Authorization middleware", () => {
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;
  let nextFunction: NextFunction = jest.fn();

  beforeEach(() => {
    mockRequest = {};
    mockResponse = {
      json: jest.fn(),
    };
  });

  test("without headers", async () => {
    const expectedResponse = {
      error: "Missing JWT token from the 'Authorization' header",
    };
    authorizationMiddleware(
      mockRequest as Request,
      mockResponse as Response,
      nextFunction
    );

    expect(mockResponse.json).toBeCalledWith(expectedResponse);
  });

  test('without "authorization" header', async () => {
    const expectedResponse = {
      error: "Missing JWT token from the 'Authorization' header",
    };
    mockRequest = {
      headers: {},
    };
    authorizationMiddleware(
      mockRequest as Request,
      mockResponse as Response,
      nextFunction
    );

    expect(mockResponse.json).toBeCalledWith(expectedResponse);
  });

  test('with "authorization" header', async () => {
    mockRequest = {
      headers: {
        authorization: "Bearer abc",
      },
    };
    authorizationMiddleware(
      mockRequest as Request,
      mockResponse as Response,
      nextFunction
    );

    expect(nextFunction).toBeCalledTimes(1);
  });
});

Keep the DRY principle (Don't Repeat Yourself)

Don't write test cases over each other. For instance: You want to test your middleware with a string input property in your request body. In this scenario don't write multiple different test cases with the same string type if these don't differ from your business logic point of view. At first sight it doesn't seem to be a serious problem but believe me, you can generate high amount of code smell that nobody wants to have in their projects. Just keep this advice in mind.

Practice, practice a lot

Writing effective automated tests is one of the hardest software development challenge you can choose. You can easily feel you don't have any concept of how to do your tests in the proper way. That's normal. I think the only way to improve testing skills is practice. Write as much as unit (or integrated, or whatever) tests as you can, make mistakes and learn from them.




Continue Learning