DEV Community

Cover image for Exploring ts-mockito: An Alternative Mocking Library for Node.js Unit Testing
Hleb Bandarenka
Hleb Bandarenka

Posted on

Exploring ts-mockito: An Alternative Mocking Library for Node.js Unit Testing

Who should read it?

If you've transitioned from the Java/C# realm to NodeJS and find comfort in the reliability of traditional OOP practices, but struggle with writing unit tests and mocking your classes, this article is for you. Jest and Sinon are powerful tools, but may not seamlessly integrate with classes, TypeScript, or Dependency Injection. Curious about alternatives? Enter ts-mockito. πŸŽ‰πŸ₯³

Prerequisites

  • OOP language (Java/C#)
  • Unit tests
  • Jest/Sinon

Let's rock πŸš€πŸŽΈ

So, like me, you probably prefer a good night's sleep over deploying hotfixes in the wee hours of the morning. And to ensure that luxury, you understand the importance of having robust test coverage.

Naturally, you want to cover all possible scenarios, from the happy paths to the not-so-happy ones, in your code. And you want to do it effortlessly.

However, the reality is that popular libraries like Jest and Sinon aren't exactly tailored for mock classes and TypeScript interfaces. Mocking with them can be a tedious and time-consuming process.

So, I started searching for something like Mockito from the Java world.
What did I need?

  • I wanted to easily mock all methods of classes and interfaces.
  • It had to work well with TypeScript, so my IDE could help me with auto-complete.
  • I also wanted a straightforward verification function for asserts.

And then I discovered - ts-mockito πŸ“¦. The name says it all - it's a mix of TypeScript and Mockito.
Sounds exciting, doesn't it? Let's give it a try and see what it can do:

Application 🧩

Imagine we have an application where an employee wants to send a message to their own department.

Following the MVC pattern, we'll have:

  • 2 repositories (one for employees and one for departments)
  • 2 services (one for departments and one for notifications)

I will show just the main DepartmentService that we will test.
(Github)

export class DepartmentService {
  constructor(
    private readonly employeeRepository: EmployeeRepository,
    private readonly departmentRepository: DepartmentRepository,
    private readonly serviceA: NotificationService
  ) {}

  public async sendMessage(input: InputData) {
    try {
      const employee = await this.employeeRepository.getEmployee(input.employeeId);
      const department = await this.departmentRepository.getDepartment(
        employee.departmentId
      );
      await this.serviceA.sendMessage(department, employee, input.message);
    } catch (e) {
      if (e instanceof Error) {
        await this.serviceA.sendAlert(input.employeeId, e.message);
      }
    }
  }
}

export type InputData = {
  employeeId: number;
  message: string;
};
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at the same unit tests written using Jest and ts-mockito.

Jest

describe("Department Service", () => {
  let employeeRepository: jest.Mocked<EmployeeRepository>;
  let departmentRepository: jest.Mocked<DepartmentRepository>;
  let notificationService: jest.Mocked<NotificationService>;
  let departmentService: DepartmentService;

  beforeEach(() => {
    employeeRepository = {
      getEmployee: jest.fn(),
      saveEmployee: jest.fn(),
      updateEmployee: jest.fn(),
      deleteEmployee: jest.fn(),
    } as jest.Mocked<EmployeeRepository>;
    departmentRepository = {
      getDepartment: jest.fn(),
      saveDepartment: jest.fn(),
      updateDepartment: jest.fn(),
      deleteDepartment: jest.fn(),
    } as jest.Mocked<DepartmentRepository>;
    notificationService = {
      sendMessage: jest.fn(),
      sendAlert: jest.fn(),
    } as jest.Mocked<NotificationService>;
    departmentService = new DepartmentService(
      employeeRepository,
      departmentRepository,
      notificationService
    );
  });

  it("should send a message when employee and department exist", async () => {
    // given
    const departmentId = 94323;
    const employeeId = 2342;
    const employee: Employee = {
      id: employeeId,
      name: "John Doe",
      departmentId,
    };
    const department: Department = { id: departmentId, name: "Department" };
    const input = { employeeId: employeeId, message: "Hello" };

    employeeRepository.getEmployee.mockResolvedValueOnce(employee);
    departmentRepository.getDepartment.mockResolvedValueOnce(department);

    // when
    await departmentService.sendMessage(input);

    // then
    expect(employeeRepository.getEmployee).toHaveBeenCalledWith(
      input.employeeId
    );
    expect(departmentRepository.getDepartment).toHaveBeenCalledWith(
      departmentId
    );
    expect(notificationService.sendMessage).toHaveBeenCalledWith(
      department,
      employee,
      input.message
    );
    expect(notificationService.sendAlert).not.toHaveBeenCalled();
  });

  it("should send an alert when an error occurs", async () => {
    // given
    const employeeId = 9834;
    const input = { employeeId: employeeId, message: "Hello" };
    const errorMessage = "Something went wrong";
    const error = new Error(errorMessage);

    employeeRepository.getEmployee.mockRejectedValueOnce(error);

    // when
    await departmentService.sendMessage(input);

    // then
    expect(notificationService.sendAlert).toHaveBeenCalledWith(
      employeeId,
      errorMessage
    );
    expect(notificationService.sendMessage).not.toHaveBeenCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode

ts-mockito

import { mock, instance, when, verify, _ } from "@johanblumenberg/ts-mockito";

describe("Department Service", () => {
  let employeeRepository: EmployeeRepository;
  let departmentRepository: DepartmentRepository;
  let notificationService: NotificationService;
  let departmentService: DepartmentService;

  beforeEach(() => {
    employeeRepository = mock(EmployeeRepository);
    departmentRepository = mock(DepartmentRepository);
    notificationService = mock(NotificationService);
    departmentService = new DepartmentService(
      instance(employeeRepository),
      instance(departmentRepository),
      instance(notificationService)
    );
  });

  it("should send a message when employee and department exist", async () => {
    // given
    const departmentId = 94323;
    const employeeId = 2342;
    const employee: Employee = {
      id: employeeId,
      name: "John Doe",
      departmentId,
    };
    const department: Department = { id: departmentId, name: "Department" };
    const input = { employeeId: employeeId, message: "Hello" };

    when(employeeRepository.getEmployee(employeeId)).thenResolve(employee);
    when(departmentRepository.getDepartment(departmentId)).thenResolve(
      department
    );

    // when
    await departmentService.sendMessage(input);

    // then
    verify(employeeRepository.getEmployee(input.employeeId)).called();
    verify(departmentRepository.getDepartment(departmentId)).called();
    verify(
      notificationService.sendMessage(department, employee, input.message)
    ).called();
    verify(notificationService.sendAlert(_, _)).never();
  });

  it("should send an alert when an error occurs", async () => {
    // given
    const employeeId = 9834;
    const input = { employeeId: employeeId, message: "Hello" };
    const errorMessage = "Something went wrong";
    const error = new Error(errorMessage);

    when(employeeRepository.getEmployee(employeeId)).thenThrow(error);

    // when
    await departmentService.sendMessage(input);

    // then
    verify(notificationService.sendAlert(employeeId, errorMessage)).once();
    verify(notificationService.sendMessage(_, _, _)).never();
  });
});
Enter fullscreen mode Exit fullscreen mode

Comparison results βš”οΈβš”οΈβš”οΈ

  • If we count the lines, we'll notice that ts-mockito is 20% shorter.
  • Also, I intentionally added more methods in the Repositories to demonstrate that Jest requires us to specify all methods manually and update the mocks every time a new method is added. Just image that you will have to update tests that are not related to a new functionality.
  • Additionally, autocomplete doesn't work for parameters in expect...toHaveBeenCalledWith with Jest.

Warning ⚠️

You may have noticed that instead of the official ts-mockito library, I imported its fork @johanblumenberg/ts-mockito. The reason for this is that ts-mockito hasn't received any new commits for the last 3 years. Johan Blumenberg, a developer who made numerous suggestions and pull requests for the library, decided to fork it and fix all the annoying bugs while the official version remains unupdated.

Conclusion

Despite some nuances, my team and I have decided to use forked ts-mockito for our unit tests. And I believe that this library will make your life as a developer much easier.

P.S. πŸ€”

You may consider to use @typestrong/ts-mockito as forked alternative to ts-mockito

Top comments (0)