Unit Test Like a Pro: Automock, My Open Source Answer to Mocking Frustration 🤓💡🎭
Tags: programming, opensource, typescript, backend
Published: 2023-12-12 16:16:38

Hello Dev Community 🙋‍♂️

I've been working with TypeScript for a while, and one thing that always seems to be a time-consuming task is manually creating mocks for unit tests when using dependency injection frameworks.

There is often a lack of consistent structure, which can make the experience even more frustrating. It can get repetitive and result in test suites that are difficult to handle and prone to becoming overly complex.

That's why I created Automock. This project is open-source and seeks to change the way we conduct unit testing in TypeScript DI environments. It automatically creates mock objects for classes, improves testing execution speed, and maintaines a consistent structure within DI frameworks such as NestJS and InversifyJS, as well as popular testing libraries like Jest and Sinon.

🔗 Here are some links to check out:
😎 GitHub: https://github.com/automock/automock
🐈 Automock's NestJS Official Recipe: https://docs.nestjs.com/recipes/automock
📦 NPM: https://www.npmjs.com/package/@automock/jest
📚 Docs Website: https://automock.dev

---

The rest of the examples in this post will be based on the following example, which is using NestJS arbitrarily but applies to all DI frameworks:

```typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserDAL {
  async getUsers(): Promise<User[]> { ... }
  async getSomethingElse(): Promise<unknown> { ... }
}

@Injectable()
export class UserApiService {
  async fetchUsers(): Promise<User[]> { ... }
  async fetchSomethingElse(): Promise<unknown> { ... }
}

@Injectable
export class UserService {
  constructor(private userDal: UserDAL, private userApi: UserApiService) {}

  async getAllUsers(): Promise<User[]> {
    const fromDb = await this.userDal.getUsers();
    const fromApi = await this.userApi.fetchUsers();

    return [...fromDb, ...fromApi];
  }
}
```

The `UserService` aggregates user data from both a database and an external api. To test the `UserService` in isolation, we need to mock the `UserDAL` and `UserApiService` classes.

## 🔄 Traditional Approach to Unit Testing

Creating manual mocks for class dependencies is a common part of the unit testing process. This manual method is time-consuming and tedious, especially when working with large classes, here is an example:

```typescript
const userDalMock: jest.Mocked<UserDAL> = {
  getUsers: jest.fn(),
  getSomethingElse: jest.fn()
};

const userApiSvcMock: jest.Mocked<UserApiService> = {
  fetchUsers: jest.fn(),
  fetchSomethingElse: jest.fn()
};
```

Looks familiar, right? 🥲

Regardless of the testing technique you use, these mocks are essential for the class under test. In most cases, you can avoid interacting with other classes in two ways:

* Inject these mocks as arguments into newly created instances of the classes.

* Leverage your framework's DI container to replace actual objects with their equivalent mock objects.

The primary goal of both approaches is to separate the unit under test (the class) and subject it to predictable and controlled behavior.

Here is the traditional approach using NestJS, which appears to be applicable to all dependency injection frameworks and libraries:

```typescript
describe('User Service Unit Test', () => {
  let userService: UserService; // Under Test

  // Mocks Declaration
  let userDal: jest.Mocked<UserDAL>;
  let apiService: jest.Mocked<ApiService>;

  let moduleRef: TestingModule;

  beforeAll(async () => {
    moduleRef = await Test.createTestingModule({})
      .overrideProvider(UserDAL)
      .useValue(userDalMock)
      .overrideProvider(ApiService)
      .useValue(userApiSvcMock)
      .compile();

    apiService = moduleRef.get(ApiService);
    userDal = moduleRef.get(UserDAL);
  });

  test('should retrieve users', async () => { ... });
});
```

As we change and refactor the `UserService`, it becomes increasingly reliant on additional classes and methods, which can complicate the process of updating these mocks. Furthermore, if there are any modifications to the other classes (`UserDAL` and `UserApiService`), it will be necessary to also make updates to the stubs.

<img src="https://media.giphy.com/media/3eS7kmMo9Ibzk0F7xg/giphy.gif" width="200" />

This continual maintenance requirement can lead to a testing environment that is hard and error-prone since devs need to make sure the mocks accurately reflect the current state of the codebase. This adds another layer of complexity to the development process.

## 💪 Automock's Approach

### Automatic Mock Generation
Automock's most notable feature is automated mock generation, which eliminates the repetitive task of manually creating and maintaining mock objects.

Here is a typical test suite using Automock:

```typescript
import { TestBed } from '@automock/jest';
import { UserService, UserDAL, UserApiService } from './services';

describe('Users Service Unit Test', () => {
  // 🧪 Unit under test
  let userService: UserService;

  // 🎭 Declare Mocks
  let mockUserApiService: jest.Mocked<UserApiService>;
  let mockUserDal: jest.Mocked<UserDAL>;

  beforeAll(() => {
    // 🚀 Automatically mock every dependency, and create on-the-fly virtual DI container for the mocks
    const { unit, unitRef } = TestBed.create(UserService).compile();

    // ⚙️ Assign the unit under test
    userService = unit;

    // 🔍 Retrieve mocks from the unit reference and assign
    mockUserApiService = unitRef.get(UserApiService);
    mockUserDal = unitRef.get(UserDAL);
  });

  it('should retrieve users from both database and api', async () => {
    // 📝 Arrange
    mockUserApiService.fetchUsers.mockResolvedValue([{ id: 1, name: 'Joe' }]);
    mockUserDal.getUsers.mockResolvedValue([{ id: 2, name: 'Jane' }]);

    // 🎬 Act
    const users = await userService.getAllUsers();

    // ✅ Assert
    expect(users).toHaveLength(2);

    expect(mockUserApiService.fetchUsers).toHaveBeenCalled();
    expect(mockUserDal.getUsers).toHaveBeenCalled();
  });
});
```

📚 [Full Step-by-Step Example](https://automock.dev/docs/getting-started/examples)

**This ensures consistent structure and syntax within test suites. This consistency is crucial and is maintained even as underlying classes or methods undergo changes or refactoring, providing a stable and reliable testing environment.**

### Speed
The introduction of a virtual DI container, using the `TestBed`, is another key aspect. It speeds up the testing execution by bypassing the conventional DI framework loading, leading to much faster test execution.

The isolation provided in testing ensures that each test runs independently with its own set of automatically provided mock implementations, creating a streamlined and interference-free environment.

🔗 Check out the full benchmark:
GitHub: https://github.com/automock/benchmark#visualizations

> `tsc` demonstrated a significant 28.9% improvement in speed for Automock (vs NestJS). Similarly, `tsc-isolated` and `SWC` also exhibited notable increases of 26.0% and 23.2% respectively. 

_* I haven't had the opportunity to test it with other dependency injection frameworks rather then NestJS yet._

<img src="https://media.giphy.com/media/3ornk4kGAQDeMaPBqo/giphy.gif" />

## 🧠🤓 The Reason Behind It All
I was motivated to create this project because I faced many common challenges when working with unit testing in different dependency injection frameworks. Regardless of the framework used, the process always proved to be complex and time-consuming. This problem was not limited to my personal experience; it highlighted a wider gap that exists throughout the industry.

I have a clear vision for Automock - to make the testing experience more efficient and improve the reliability and quality of code. This project represents a significant stride towards enhancing development practices within the TypeScript community, making them more efficient and effective.

## 👀 How Things Stand Right Now, and What Am I Looking for?

Until now, the project has gained around 60K monthly downloads, thanks to the recognition of NestJS, which added Automock to their recipes section.

Currently, the project is in its early-middle stage. It supports NestJS and InversifyJS. I am looking to expand its capabilities and integrate DI frameworks, and **I'm seeking contributions from the developer community, especially those proficient in TypeScript, dependency injection, and unit testing methodologies.**

## It's for You ❤️
Automock is more than just a tool; it is intended to be a community-driven solution aimed at aiding developers facing similar challenges in unit testing.

<img src="https://media.giphy.com/media/kesLaG9wgJCbmv1YfT/giphy.gif" />

I invite you to be part of this journey, let's shape the future of software quality and unit testing!

Happy testing! 🚀🧪🤓