Tech Notes
Unit Testing Demystified: Best Practices with Mocha, Sinon, and Proxyquire

Unit Testing Demystified: Best Practices with Mocha, Sinon, and Proxyquire

Keywords

unit test, stub, mock, spy, sinon, proxyquire, chai, expect, mocha, jest, js, ts

Table of Contents

Preface

This is a summary of my learning about unit testing in TypeScript. I will be using Mocha, Sinon and Proxyquire to demonstrate how to use them.

Recently, I've started a new job as a backend developer 🎉.

For an onboarding project, I was asked to write unit tests for a mall-like project that I had developed. And started to notice that I didn't know how to write proper unit tests.

I had written tests before using jest (opens in a new tab), but they were mostly integration tests.

After discussing with several colleagues, I came to realize that I didn't know the difference between stub, mock, and spy.

So I decided to learn about them and write a summary of my learning.

Hope this helps you as well!

Hierarchy of Tests

Hierarchy of Tests Hierarchy of tests (image source: https://katalon.com/resources-center/blog/unit-testing (opens in a new tab))


Acceptance Tests

Acceptance tests are the highest level of tests. They are also called end-to-end tests or functional tests. Usually done by QA team.

This type of tests are used to test the whole system, and they are usually written in a way that mimics the user's behavior.

For instance, if you want to test a login feature, you would write a test that goes to the login page, enter the username and password, and click the login button.

Acceptance tests are more easier to tell apart from other tests, so I won't be going into details in this article.


Integration Tests

Integration tests are the second highest level of tests. They are also called component tests.

This type of tests are used to test the interaction between different components of the system, and most importantly, system are tested as a whole, not in isolation.

For instance, if you want to test a login feature, you would write a test that calls the login function, and check if the user is logged in.

From past experience, in a backend project, integration tests are usually written in a way that makes calls to the API endpoints (in other words, a request) and checks if the response is correct. We could use tools like supertest (opens in a new tab) for Jest, or in Mocha, use chai-http (opens in a new tab) to accomplish HTTP request sending.


Pros

  • Tests the system as a whole, so it's easier to tell if the system is working or not.
  • Easier to write than unit tests. (No need to mock out dependencies)
  • Straightforward to write (at least for backend engineers).
  • For small projects, it's easier to write integration tests than unit tests because most logic are dealt with in /routes.

Cons

  • Hard to debug (couldn't tell which component is cauing the error).
  • Very Slow to run (Have to spin up most parts of the system (db, object bucket, api endpoint...)).
  • Mocking DB data is hard & tedious.

Unit Tests

Unit tests are the lowest level of tests. They are also called isolated tests.

This type of tests are used to test the smallest unit of the system, usually a function or a class.

For instance, using the same example, if you want to test a login feature, you would write multiple tests that the /login endpoint would call, and test each in different scenarios.

From past experience, in a backend project, unit tests are usually written in a way that directly calls the function (in other words, a function call) and checks if the response is correct.

For my case, idealy, I would test at least one success and one failed scenarios for all the functions in /models and /utils in isolation.

We could use tools like Sinon to mock out dependencies, and Proxyquire to mock out modules imported by target file.


Pros

  • Tests the smallest unit of the system, so it's a no-brainer to tell if the function is working or not.
  • Easy to debug (if you write good tests).
  • Fast to run (no need to spin up the system) (way faster!).

Cons

  • Hard to write (have to mock out dependencies).
  • If Mocked out dependencies are not correct, the test would pass even if the function is not working properly.
  • Misses the big picture.


Types of Test Doubles

Test Doubles Test Doubles (image source: https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da (opens in a new tab))

Introduction

Test doubles are objects that are used in unit tests to replace the real objects that the unit being tested depends on.

Normally in unit tests, we would want to test a function in isolation, but most functions require other functions to work properly, for example a function: getProductName(id: number) might require db instance for data fetching,

although for systems at larger scale, DI (dependency injection) might be a must for these functions (getProductName(db: DB, id: Number)),

but in the real world, for applications that possess only one or two databases, we often overlook Dependency Injection and import it straight from the top.

I'll briefly explain the differences between stub, spy, and mock, and how to use them in unit tests, for those looking for a more detailed explanation, please refer to Refernces.


For those interested in DI, I personally really recommend this video (opens in a new tab) which thoroughly explains DI and how to implement it in action.


1. Stub

Stubs are objects that provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

2. Spy

Spies are objects that record how a function is called, allowing us to verify that the function was called correctly.

Note that spies allow actual function calls to occur; they act as observers to record events during the function call span.

3. Mock

Mocks are objects that pre-programmed with expectations which form a specification of the calls they are expected to receive.

TL;DR

My personal understanding of the differences between stub, spy, and mock is that:

  • Stub is used to replace a function with a canned answer.
  • Spy is used to record how a function is called.
  • Mock is not just a substitute for a function; mocks come pre-programmed with expectations, specifying the calls they anticipate receiving. Mocks are primarily used to verify that a function is invoked correctly, providing a stricter form of validation.

Moreover, Mocking is a subset of Stubbing, and Stubbing is a subset of Spying.

Test DoublePurposeExample
StubProvide canned answers to calls made during the teststubProductService.getProductName(id: number).returns('Test Product')
SpyRecord how a function is calledspyOn(productService, 'getProductName')
MockPre-programmed with expectations which form a specification of the calls they are expected to receivemockProductService.expects('getProductName').once().withArgs(1)

Proxyquire

npm link (opens in a new tab)

Purpose

Proxyquire (opens in a new tab) is a library that allows you to stub out dependencies in your tests. It is useful when you want to test a module in isolation, but it requires other modules to function.

For my case, I wanted to test a module that required a database connection (specifically, a Knex (opens in a new tab) connection), but I didn't want to actually connect to the database in my tests.

And simply using Sinon (opens in a new tab) stubs wouldn't work because knex calls often require a lot of chaining function calls for example: db('table_name').select([]).where('...').orderBy('...')

Usage

tutorial.ts
import proxyquire from "proxyquire";
proxyquire("relative_path_to_your_module", {
  // path_to_external_module must be same to the path you imported in your module
  "path_to_external_module": { module_name: stub_object_you_created},
});

for instance, I wanted to test my model in my expressJS project called order under folder /src/models/order.ts that required knex connection from /src/db/mysql.ts, and my test file is stored under /__test__/models/order.ts.

The function I wanted to test in this example is called attachOrderItem(order: Order) that takes an order as a parameter and attaches order_items stored in the database to the order itself.

The original function looks like this:

models/order.ts
import {
  Order,
  PostOrderItemRequestBody,
  PaymentMethod,
  OrderStatus,
} from "../../api";
import { db } from "../db/mysql";
 
export const attachOrderItem = async (order: Order) => {
  try {
    const items = await db("order_item")
      .select([])
      .where({ orderId: order.id });
    order.items = items;
  } catch (err) {
    throw err;
  }
};

And the test file looks like this:

__test__/models/order.ts
import proxyquire from "proxyquire";
import sinon from "sinon";
import { expect } from "chai";
const expectedResult: any[] = [
  {
    id: 1,
    name: "test",
    price: 100,
    quantity: 1,
  },
];
const knexQuery = createKnexQueryStub(expectedResult);
const knexStub = sinon.stub().returns(knexQuery);
const orderModel = proxyquire("../../src/models/order", {
  "../db/mysql": { db: knexStub },
  // other dependencies that requires stubbing
  "../other/dependency": { dependencyModule: sinonStubObject },
});
 
describe("/models/order", () => {
  describe("attachOrderItem()", async () => {
    it("Success", async () => {
      const order = {
        id: 1,
        items: [],
      };
      await orderModel.attachOrderItem(order);
      expect(order.items).to.deep.equal(expectedResult);
    });
  });
});

Conclusion

In this article, I've briefly explained the differences between integration tests and unit tests, and the differences between stub, spy, and mock.

We also go through some simple usage example and purpose for library Proxyquire that allows you to stub out dependencies in your tests.

Hope this article gives you a better understanding of unit tests and how to write them, and try to use Proxyquire when Sinon couldn't do the job.

If not, at least learn how to distinguish them 🤣.

Reference