javascript

Unit Testing in NestJS for Node Using Suites (Formerly Automock)

Antonello Zanini

Antonello Zanini on

Unit Testing in NestJS for Node Using Suites (Formerly Automock)

For years, Automock was a popular framework for defining mocks and stubs in backend test environments. As technology has evolved, new methods and techniques for streamlining the simulation of dependencies in testing have emerged. That's why Automock has been succeeded by Suites, a more modern and robust library.

In this article, we'll explore the transition from Automock to Suites, understand what Suites offers, and see it in action in NestJS through a complete example.

Let's dive into how to use Suites for simplified unit test mocks and stubs!

From Automock to Suites for Node

Automock was long considered a go-to tool for making unit testing easier by creating mocks and stubs within dependency injection frameworks. As development practices evolved, the need for more advanced and flexible testing solutions grew. This is why, on 13 July 2024, the team behind Automock decided to replace it with Suites.

Suites is the rebranded and enhanced successor to Automock. Built on Automock's solid foundation, Suites offers more features and advanced capabilities to meet modern software testing needs.

What is Suites?

Suites is a flexible and opinionated testing meta-framework designed to enhance the testing experience in backend systems.

A meta-framework operates as a "framework of frameworks." Unlike traditional frameworks that offer specific functionalities, a meta-framework like Suites works at a higher level. It works, orchestrates, and enhances various underlying frameworks, libraries, and tools without directly providing the functionality itself.

Specifically, Suites integrates with a few dependency injection frameworks and testing libraries, including NestJS, Jest, and Sinon. The end goal of this testing tool is to simplify the process of creating reliable tests, thus contributing to the development of high-quality software.

At the time of writing, Suites has 428 stars on GitHub and over 37,000 weekly npm downloads. It is gaining strong traction among the community.

Suites for Node: Main Features and Capabilities

Now that you understand what Suites is and how it represents the future of Automock, let's explore what it has to offer!

Multiple Dependency Injection and Mocking Adapters

Suites supports the following DI frameworks via dedicated adapter packages:

DI frameworkAdapter name
NestJS@suites/di.nestjs
Inversify@suites/di.inversify

Note: The TSyringe adapter is currently under development.

Similarly, Suites integrates with the following mocking libraries:

Mocking libraryAdapter name
Jest@suites/doubles.jest
Sinon@suites/doubles.sinon
Vitest@suites/doubles.vitest

Note: Support for Bun and Deno is coming soon.

Support for Both Solitary and Sociable Unit Tests

In testing, a "unit" refers to the smallest testable part of an application. Depending on the software design, that can be a function, method, procedure, module, object, or class. In Suites, a unit can be tested either in isolation or in combination with its dependencies.

These two approaches to unit testing in Suites are:

  • Solitary unit testing: Also known as isolated unit testing, these tests focus on testing a single unit of work entirely separate from its external dependencies. They use test doubles (i.e., mocks and stubs) to simulate the behavior of the unit's dependencies. The goal is to verify the functionality and reliability of individual units, ensuring they perform as expected under controlled conditions. This approach is supported by the TestBed.solitary() method.
  • Sociable unit testing: Also known as integrated unit testing, these tests focus on testing a unit of work in conjunction with its real dependencies. While they still mock the dependencies of the unit's dependencies, they ensure that the interactions between a unit and its immediate collaborators are tested in a controlled environment. This provides a broader scope of validation compared to solitary unit tests and is supported by the TestBed.sociable() method.

Zero-Setup Mocking

Suites provides a comprehensive API for generating mock objects, eliminating the need for manual setup and reducing boilerplate code.

The TestBed.solitary() and TestBed.sociable() methods in Suites offer advanced mocking capabilities through these two methods:

  • mock().final(): To set the final behavior of a mock, meaning that the mock cannot be changed later for further stubbing or assertions. It defines how the mock should behave without returning a reference for future adjustments.
typescript
const { unit } = await TestBed.solitary(UsersService) .mock(UsersRepository) // stub the getFirst() method of UsersRepository .final({ getFirst: async () => ({ id: 1, name: "John Doe" }) }) .compile();
  • mock().impl(): To define mock behavior using a callback function. It returns a reference to the mocked unit, allowing further stubbing.
typescript
const { unit, unitRef } = await TestBed.solitary(UsersService) .mock(UsersRepository) // define how to stub the getFirst() method of UsersRepository .impl(stubFn => ({ getFirst: stubFn().mockResolvedValue({ id: 1, name: 'John Doe' }) .compile();

In particular, unit represents the instance of the class under test. Instead, unitRef is the mocked dependency created by the TestBed. In other terms, unitRef is a stubbed instance of a mocked dependency that you can further customize using methods provided by your configured mocking library.

Typed Mocks in TypeScript

Suites comes with native TypeScript integration, enabling you to retain the same types as real objects while mocking. Specifically, the Mocked type from the @suites/unit package automatically types the mocked instances of classes.

Here's Mocked in action:

typescript
import { TestBed, Mocked } from "@suites/unit"; import { UserService } from "./user.service"; import { UserRepository } from "./user.repository"; describe("Users Service Unit Spec", () => { let usersService: UsersService; // the object to mock let usersRepository: Mocked<UsersRepository>; beforeAll(async () => { const { unit, unitRef } = await TestBed.solitary(UsersService).compile(); usersService = unit; // the mocked dependency will have the right type usersRepository = unitRef.get(UsersRepository); }); it("should return the user name and call repository", async () => { // the mocked dependency comes with mocking functions // from your mocking library usersRepository.getUserById.mockResolvedValue({ id: 14, name: "Maria Williams", }); const result = await usersService.getUserName(14); expect(usersRepository.getUserById).toHaveBeenCalledWith(14); expect(result).toBe("Maria Williams"); }); });

Thanks to Mocked, you don't have to define a custom type for the usersRepository mocked dependency.

Optimized Performance

Suites bypasses the traditional dependency injection container by directly leveraging the dependency injection (DI) framework's reflection and metadata capabilities to set up an isolated test environment. Instead of loading the entire DI container, Suites creates a lightweight, virtual container that mirrors the mechanism.

This streamlined approach simplifies setup, reduces overhead, and speeds up test execution. All that while preserving the benefits of dependency injection principles.

Backward Compatibility With Automock

Suites represents an evolution from Automock, which means the transition requires minimal effort. The main change involves moving the TestBed factory, which is now part of @suites/unit rather than being split between @suites/jest or @suites/sinon.

The key API changes to be aware of when migrating from Automock to Suites are:

  • TestBed.compile() is now async.
  • TestBed.create() has been renamed to TestBed.solitary().
  • The new sociable() method has been added to the TestBed API.
  • TestBed is now imported from @suites/unit regardless of the installed adapters.
  • The new Mocked type from @suites/unit offers deep partial mock capabilities.
  • The mock.using() method has been replaced by the mock.impl() and mock.final() methods.

Explore the documentation for a complete Automock to Suites migration guide.

Using Suites for Unit Testing in NestJS

Now you'll learn how to use Suites for mocking within a Jest unit test in NestJS.

For a quicker setup or to have the code readily available, clone the GitHub repository supporting this article:

Shell
git clone https://github.com/Tonel/nestjs-suites-demo

Navigate to the project folder in your terminal and install the project dependencies:

Shell
cd nestjs-suites-demo npm install --legacy-peer-deps

Note that the --legacy-peer-deps option is required to avoid the "unable to resolve dependency tree" you may get while installing Suites and its dependencies.

Creating a NestJS Project

Before getting started, make sure you have Node.js and the NestJS CLI installed on your machine. Then, initialize a new NestJS project using the new command:

Shell
nest new nestjs-suites-demo

Great! The nestjs-suites-demo directory now contains a new NestJS project with a Jest integration. Load it into your favorite JavaScript IDE.

You're ready to define the CRUD APIs for handling products in an in-memory database.

First, add a new module to the NestJS project with the generate module command:

Shell
nest generate module products

This will create a products.module.ts file in the products folder inside /src:

Products file in folder

You're ready to create a new entity representing your product data. Create an entities directory inside products and add the following product.entity.ts file:

typescript
export interface Product { id: string; name: string; description: string; price: number; }

This defines a Product entity with id, name, description, and price fields.

Now, create a service for your Product entity. In the products folder, add a products.service.ts file as follows:

typescript
import { Injectable } from "@nestjs/common"; import { Product } from "./entities/product.entity"; @Injectable() export class ProductsService { // the in-memory database where to store data private products: Product[] = []; create(product: Omit<Product, "id">): Product { const newProduct: Product = { // generate a random UUID for the new product id: crypto.randomUUID(), ...product, }; this.products.push(newProduct); return newProduct; } findAll(): Product[] { return this.products; } findById(id: string): Product | undefined { return this.products.find((product) => product.id === id); } update( id: string, updateProduct: Partial<Omit<Product, "id">> ): Product | undefined { const product = this.findById(id); if (product) { Object.assign(product, updateProduct); return product; } return undefined; } delete(id: string): boolean { const index = this.products.findIndex((product) => product.id === id); if (index !== -1) { this.products.splice(index, 1); return true; } return false; } }

The above class defines the business logic for implementing CRUD operations to manage products. In this example, the in-memory database is simply a JavaScript array.

Next, add the following products.controller.ts file to the products folder:

typescript
import { Controller, Get, Post, Put, Delete, Param, Body, HttpCode, HttpStatus, NotFoundException, } from "@nestjs/common"; import { ProductsService } from "./products.service"; import { Product } from "./entities/product.entity"; @Controller("products") export class ProductsController { constructor(private productsService: ProductsService) {} @Post() @HttpCode(HttpStatus.CREATED) async create( @Body() createProductDto: Omit<Product, "id"> ): Promise<Product> { return this.productsService.create(createProductDto); } @Get() async getAll(): Promise<Product[]> { return this.productsService.findAll(); } @Get(":id") async getOne(@Param("id") id: string): Promise<Product> { const product = this.productsService.findById(id); if (!product) { throw new NotFoundException(`Product with ID ${id} not found`); } return product; } @Put(":id") async update( @Param("id") id: string, @Body() updateProductDto: Partial<Omit<Product, "id">> ): Promise<Product> { const updatedProduct = this.productsService.update(id, updateProductDto); if (!updatedProduct) { throw new NotFoundException(`Product with ID ${id} not found`); } return updatedProduct; } @Delete(":id") @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param("id") id: string): Promise<void> { const deleted = this.productsService.delete(id); if (!deleted) { throw new NotFoundException(`Product with ID ${id} not found`); } } }

The ProductsController class provides the implementation for the route methods below:

  • create(): To create a new product. Mapped to the POST /products endpoint.
  • getOne(): To get a single product. Mapped to the GET /products/:id endpoint.
  • getAll(): To get all products. Mapped to the GET /products endpoint.
  • update(): To update a single product. Mapped to the PUT /products/:id endpoint.
  • delete(): To delete a single product. Mapped to the DELETE /products/:id endpoint.

Each of the above routes calls the corresponding CRUD method in the ProductsService class.

Register the ProductsController and ProductsService files in products.module.ts with the @Module annotation:

typescript
import { Module } from "@nestjs/common"; import { ProductsController } from "./products.controller"; import { ProductsService } from "./products.service"; @Module({ controllers: [ProductsController], providers: [ProductsService], }) export class ProductsModule {}

Here we go! Your NestJS backend now exposes CRUD endpoints to create, read, update, and delete products.

Run your NestJS application locally with:

Shell
npm run start:dev

Your backend server will start on port 3000.

Add some products by calling the POST /products endpoint:

Shell
curl -X POST http://localhost:3000/products \ -H "Content-Type: application/json" \ -d '{ "name": "UltraWidget", "description": "A versatile gadget for everyday tasks.", "price": 199.99 }'

Then, retrieve them with a GET request to /products:

Shell
curl -X GET http://localhost:3000/products

The result will be something like:

JSON
[ { "name": "UltraWidget", "description": "A versatile gadget for everyday tasks.", "price": 199.99, "id": "ce34a968-1a73-4b82-99fc-9af57768c22d" }, { "name": "SmartLight 3000", "description": "An energy-efficient smart light bulb with customizable colors.", "price": 29.99, "id": "080c044f-3dfc-43e0-90d9-9666086ff944" }, { "name": "EcoBreeze Air Purifier", "description": "A compact air purifier that removes 99.9% of airborne particles.", "price": 149.95, "id": "a9188527-f139-482d-bd01-ff501c168c23" } ]

Amazing! You now have a NestJS application to unit test with Suites.

Setting Up Suites

Install the Suites core package by adding the @suites/unit npm package to your project's development dependencies:

Shell
npm install --save-dev @suites/unit

To fully integrate Suites, you also need to install the framework and mocking library adapters for Jest and NestJS:

Shell
npm install --save-dev @suites/doubles.jest @suites/di.nestjs

Suites will automatically detect the installed adapters and configure itself accordingly.

If you're an npm user, you may get an "unable to resolve dependency tree" error due to the reflect-metadata peer package. As a quick fix, launch the installation command with the --legacy-peer-deps option:

Shell
npm install --save-dev @suites/doubles.jest @suites/di.nestjs --legacy-peer-deps

For more details, check out the dedicated guide on how to properly address that error.

As a final step, ensure that your tsconfig.json file has the emitDecoratorMetadata and experimentalDecorators options set to true:

JSON
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true } }

Suites requires these configurations to reflect class dependencies and operate properly with dependency injection frameworks.

Fantastic! It's time to define your first unit test in Suites.

Defining a Unit Test

When adding test files to a NestJS project, the best practice is to place them as close as possible to the files they're testing. This keeps your test files organized and easy to find.

Suppose you want to test the getAll() method of ProductsController in isolation. As a first step, create a products.controller.spec.ts file within the products folder.

After adding the test file, your products directory in the NestJS project will look like this:

Products directory

Now, time to define the testing logic.

You can use Suites to verify that the getAll() method works in isolation with a Jest unit test:

typescript
import { TestBed, Mocked } from "@suites/unit"; import { ProductsController } from "./products.controller"; import { ProductsService } from "./products.service"; import { Product } from "./entities/product.entity"; describe("Products Controller Unit Spec", () => { // declare the unit under test let productsController: ProductsController; // declare a mock dependency let productsService: Mocked<ProductsService>; beforeAll(async () => { // create an isolated test environment for the unit under test const { unit, unitRef } = await TestBed.solitary( ProductsController ).compile(); // assign the unit to test productsController = unit; // assign the dependency to mock from the unit reference productsService = unitRef.get(ProductsService); }); test("should return 3 products", async () => { // the products you expect to retrieve from the database const mockedProducts: Product[] = [ { name: "UltraWidget", description: "A versatile gadget for everyday tasks.", price: 199.99, id: "ce34a968-1a73-4b82-99fc-9af57768c22d", }, { name: "SmartLight 3000", description: "An energy-efficient smart light bulb with customizable colors.", price: 29.99, id: "080c044f-3dfc-43e0-90d9-9666086ff944", }, { name: "EcoBreeze Air Purifier", description: "A compact air purifier that removes 99.9% of airborne particles.", price: 149.95, id: "a9188527-f139-482d-bd01-ff501c168c23", }, ]; // mock the findAll() method so that it returns // the expected data productsService.findAll.mockReturnValue(mockedProducts); // call the function to test const products = await productsController.getAll(); // verify that the mocked service method // has been called as expected expect(productsService.findAll).toHaveBeenCalled(); // verify that it return the expected data expect(products).toEqual(mockedProducts); }); });

In the snippet above, TestBed creates an isolated test environment for solitary unit testing of ProductsController. This gives you the ability to test the controller independently of other parts of the application.

To do so, you must mock the ProductsService class used by ProductsController. That allows you to interact with a mock version of the actual service, ensuring that the test is focused on the controller logic rather than external dependencies.

In particular, you can use the mockReturnValue() method to mock what data the findAll() method from ProductsService should return. When calling the getAll() method of ProductsController, you can now verify that it works as expected with a few assertions.

You just wrote a Jest unit test with Suites integration in NestJS.

Running the Test

Run your Jest unit tests with this command:

Shell
npx jest

Or, equivalently, execute:

Shell
npm run test

The test npm script is automatically added to package.json by the nest new utility. Also, don't forget that the nest new command configures Jest in your project. So, you don't have to worry about installing and setting it up manually.

The result should be something similar to:

Shell
PASS src/app.controller.spec.ts (6.009 s) PASS src/products/products.controller.spec.ts (6.78 s) Test Suites: 2 passed, 2 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 7.568 s Ran all test suites.

Ignore the src/app.controller.spec.ts test file, as that's automatically generated by nest new when creating a new project. Instead, focus on the products.controller.spec.ts file, which contains the Jest unit test of interest.

Et voilà! Your unit tests with Suites mocking logic work like a charm.

Wrapping Up

In this blog post, we explored how Suites has taken the place of Automock by extending its capabilities with more features and better support for modern testing.

You now know:

  • Why Automock was replaced with Suites
  • What Suites is and how it enhances mocking
  • The features and characteristics that Suites offers for mocking and stubbing
  • How to use Suites in NestJS unit tests

Thanks for reading!

Wondering what you can do next?

Finished this article? Here are a few more things you can do:

  • Share this article on social media
Antonello Zanini

Antonello Zanini

Guest author Antonello is a software engineer, but prefers to call himself a Technology Bishop. Spreading knowledge through writing is his mission.

All articles by Antonello Zanini

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps