Exploring ts-mockito: An Alternative Mocking Library for Node.js Unit Testing
Tags: unittest, node, testing
Published: 2024-02-08 00:35:02

## 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](https://jestjs.io/) and [Sinon](https://sinonjs.org/) are powerful tools, but may not seamlessly integrate with classes, TypeScript, or Dependency Injection. Curious about alternatives? Enter **[ts-mockito](https://github.com/NagRock/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](https://site.mockito.org/) 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 📦](https://github.com/NagRock/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](https://github.com/kakawi/post_nodejs-mocking))
```typescript
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;
};
```


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

#### Jest
```typescript
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();
  });
});
```

#### ts-mockito
```typescript
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();
  });
});
```

### 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](https://github.com/johanblumenberg), 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](https://github.com/TypeStrong/ts-mockito) as forked alternative to **ts-mockito**