NestJS Unit & Integration Tests
Testing is a crucial part of software development, ensuring that your code works as expected and preventing bugs from making their way into production. In this blog post, we will explore how to write unit and integration tests for a NestJS application, covering everything from the basics to more advanced techniques.
Checkout parts 1 & 2 of the NestJS series
Pros and Cons of Writing Tests
Pros
Improved Code Quality:
- Writing tests ensures that your code works as expected and helps catch bugs early in the development process.
Refactoring Confidence:
- Tests provide a safety net that allows you to refactor your code with confidence, knowing that any breaking changes will be caught by your tests.
Documentation:
- Tests serve as documentation for your code, showing how different parts of your application are expected to behave.
Reduced Bugs in Production:
- By catching issues early, tests reduce the likelihood of bugs making it into production, leading to more reliable software.
Encourages Best Practices:
- Writing tests encourages best practices in software development, such as modularity and separation of concerns.
Cons
Time-Consuming:
- Writing tests can be time-consuming, especially for large applications with many components.
Maintenance Overhead:
- Tests need to be maintained along with the code. Changes in the application may require updates to the tests.
Initial Learning Curve:
- There is an initial learning curve associated with writing effective tests, particularly for beginners.
Types of Tests
Unit Testing: Tests individual components or functions for correctness.
Integration Testing: Ensures that different components or systems work together as expected.
System Testing: Tests the complete and integrated software system to evaluate its compliance with the specified requirements.
End-to-End (E2E) Testing: Simulates real user scenarios to validate the entire application workflow.
Acceptance Testing: Validates that the software meets business requirements and is ready for deployment.
Performance Testing: Assesses the speed, responsiveness, and stability of the software under various conditions.
Security Testing: Identifies vulnerabilities and ensures the software is secure against attacks.
Prerequisites
Before we dive into testing, make sure you have the following set up:
Node.js and npm installed
A basic understanding of JavaScript/TypeScript
A NestJS application (You can create one using the NestJS CLI or go to NestJS 101)
Setting Up the NestJS Application
If you don't already have a NestJS application, you can create one using the NestJS CLI or go to NestJS 101 to get started with a simple API:
npm install -g @nestjs/cli
nest new nestjs-testing-app
cd nestjs-testing-app
Introduction to Testing in NestJS
NestJS uses Jest as the default testing framework. Jest is a powerful and flexible testing framework that provides a great developer experience with features like zero configuration, snapshot testing, and built-in mocking.
Alternatives of Jest you can configure:
Mocha - Highly configurable, supports various assertion libraries
Jasmine - Behavior-driven, simple, built-in assertions
Ava - Concurrent test running, minimalistic, async/await support
Testing Files Structure
By default, NestJS places test files alongside the source files with an .spec.ts
extension. This helps in keeping the tests close to the code they are testing.
Writing Unit Tests
Unit tests focus on testing individual units of code, such as functions or classes, in isolation from other parts of the application.
Example: Testing a Service
Let's start by writing a unit test for a simple service. Consider the following cats.service.ts
:
import { Injectable } from '@nestjs/common';
@Injectable() // Marks the class as a provider that can be injected into other components.
export class CatsService {
private readonly cats = []; // Private property to store an array of cats.
// Method to create a new cat and add it to the array.
create(cat) {
this.cats.push(cat); // Adds the new cat to the cats array.
}
// Method to retrieve all the cats.
findAll() {
return this.cats; // Returns the entire cats array.
}
}
To test this service, create a cats.service.spec.ts
file:
// Import necessary modules and classes from @nestjs/testing
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
// Describe the test suite for CatsService
describe('CatsService', () => {
let service: CatsService;
// Before each test, set up the testing module and get the CatsService instance
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
// Provide the CatsService for testing
providers: [CatsService],
}).compile();
// Get the CatsService instance from the testing module
service = module.get<CatsService>(CatsService);
});
// Test case to check if the service is defined
it('should be defined', () => {
// Expect the service to be defined
expect(service).toBeDefined();
});
// Test case to check if a cat can be created
it('should create a cat', () => {
// Define a sample cat object
const cat = { name: 'Tom' };
// Call the create method to add the cat to the service
service.create(cat);
// Expect the created cat to be in the list of all cats
expect(service.findAll()).toContain(cat);
});
// Test case to check if all cats can be returned
it('should return all cats', () => {
// Define two sample cat objects
const cat1 = { name: 'Tom' };
const cat2 = { name: 'Jerry' };
// Call the create method to add both cats to the service
service.create(cat1);
service.create(cat2);
// Expect the findAll method to return both cats in an array
expect(service.findAll()).toEqual([cat1, cat2]);
});
});
Running the Tests
You can run the tests using the following command:
npm run test
Writing Integration Tests
Integration tests focus on testing the interactions between different parts of the application. In NestJS, this often involves testing the controllers and their interactions with services.
Example: Testing a Controller
Consider the following cats.controller.ts
:
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service'; // Import the CatsService to be used in the controller
// Define the route path for this controller as 'cats'
@Controller('cats')
export class CatsController {
// Inject CatsService into the controller via dependency injection
constructor(private readonly catsService: CatsService) {}
// Define a POST route to create a new cat
@Post()
create(@Body() cat) {
// Call the create method of CatsService with the provided cat data
this.catsService.create(cat);
}
// Define a GET route to retrieve all cats
@Get()
findAll() {
// Call the findAll method of CatsService to get the list of all cats
return this.catsService.findAll();
}
}
To test this controller, create a cats.controller.spec.ts
file:
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let controller: CatsController;
let service: CatsService;
// This block is executed before each test in the describe block
beforeEach(async () => {
// Create a testing module with the CatsController and CatsService
const module: TestingModule = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
// Retrieve instances of CatsController and CatsService from the testing module
controller = module.get<CatsController>(CatsController);
service = module.get<CatsService>(CatsService);
});
// Test to check if the controller is defined
it('should be defined', () => {
expect(controller).toBeDefined();
});
// Test to check if the create method of the controller calls the create method of the service
it('should create a cat', () => {
const cat = { name: 'Tom' };
// Spy on the create method of the service and provide a mock implementation
jest.spyOn(service, 'create').mockImplementation(() => {});
// Call the create method of the controller
controller.create(cat);
// Check if the create method of the service was called with the correct argument
expect(service.create).toHaveBeenCalledWith(cat);
});
// Test to check if the findAll method of the controller returns all cats
it('should return all cats', () => {
const cats = [{ name: 'Tom' }, { name: 'Jerry' }];
// Spy on the findAll method of the service and provide a mock implementation
jest.spyOn(service, 'findAll').mockImplementation(() => cats);
// Check if the findAll method of the controller returns the correct data
expect(controller.findAll()).toEqual(cats);
})
})
Advanced Testing Techniques
Mocking Dependencies
In real-world applications, services often depend on other services or databases. To isolate the unit being tested, we can mock these dependencies.
Consider the following cats.service.ts
with a dependency on CatsRepository
:
import { Injectable } from '@nestjs/common';
import { CatsRepository } from './cats.repository';
// The @Injectable decorator marks the CatsService class as a provider that can be injected into other components or services.
@Injectable()
export class CatsService {
// Injecting the CatsRepository into the CatsService via the constructor
constructor(private readonly catsRepository: CatsRepository) {}
// Method to create a new cat entry in the repository
create(cat) {
// Calls the save method on the CatsRepository to persist the cat object
this.catsRepository.save(cat);
}
// Method to retrieve all cat entries from the repository
findAll() {
// Calls the findAll method on the CatsRepository to get all cat objects
return this.catsRepository.findAll();
}
}
To test this service, create a cats.service.spec.ts
file:
// Import necessary modules from NestJS for testing
import { Test, TestingModule } from '@nestjs/testing';
// Import the service and repository to be tested
import { CatsService } from './cats.service';
import { CatsRepository } from './cats.repository';
// Describe the test suite for the CatsService
describe('CatsService', () => {
let service: CatsService; // Declare a variable for the service
let repository: CatsRepository; // Declare a variable for the repository
// Before each test, set up the testing module and initialize the service and repository
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
// Provide the CatsService and a mock CatsRepository
providers: [
CatsService,
{
provide: CatsRepository,
useValue: {
save: jest.fn(), // Mock implementation for save method
findAll: jest.fn(), // Mock implementation for findAll method
},
},
],
}).compile();
// Get instances of the service and repository from the testing module
service = module.get<CatsService>(CatsService);
repository = module.get<CatsRepository>(CatsRepository);
});
// Test to ensure the service is defined
it('should be defined', () => {
expect(service).toBeDefined();
});
// Test the create method of the service
it('should create a cat', () => {
const cat = { name: 'Tom' }; // Define a sample cat object
service.create(cat); // Call the create method with the sample cat
expect(repository.save).toHaveBeenCalledWith(cat); // Check if the save method was called with the correct argument
});
// Test the findAll method of the service
it('should return all cats', () => {
const cats = [{ name: 'Tom' }, { name: 'Jerry' }]; // Define a sample list of cats
jest.spyOn(repository, 'findAll').mockImplementation(() => cats); // Mock the findAll method to return the sample list
expect(service.findAll()).toEqual(cats); // Check if the findAll method returns the correct list
});
});
E2E Testing
End-to-end (E2E) tests verify the complete flow of the application, from the user interface to the backend services. NestJS supports E2E testing with Jest.
Setting Up E2E Tests
NestJS provides a sample E2E test in the test
folder. You can modify it to suit your needs.
// Import necessary testing modules and utilities from NestJS
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest'; // Import supertest to make HTTP assertions
import { AppModule } from './../src/app.module'; // Import the main application module
// Describe the E2E test suite for AppController
describe('AppController (e2e)', () => {
let app: INestApplication; // Define a variable to hold the NestJS application instance
// Before all tests, initialize the NestJS application
beforeAll(async () => {
// Create a testing module with the AppModule
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
// Create a NestJS application from the testing module
app = moduleFixture.createNestApplication();
await app.init(); // Initialize the application
});
// Define a test case for the root route
it('/ (GET)', () => {
return request(app.getHttpServer()) // Use supertest to make a request to the app's HTTP server
.get('/') // Make a GET request to the root route
.expect(200) // Expect the HTTP status code to be 200
.expect('Hello World!'); // Expect the response body to be 'Hello World!'
});
// After all tests, close the NestJS application
afterAll(async () => {
await app.close(); // Close the application
});
});
Running E2E Tests
You can run E2E tests using the following command:
npm run test:e2e
Considerations When Writing NestJS Tests
Isolation: Ensure that unit tests isolate the component being tested. Mock dependencies to avoid testing multiple units at once.
Coverage: Aim for high test coverage but focus on testing critical parts of your application first.
Readability: Write tests that are easy to read and understand. Use descriptive test names and clear assertions.
Maintainability: Keep tests maintainable by refactoring them alongside your code. Avoid brittle tests that break with minor changes in the code.
Speed: Keep unit tests fast by avoiding external dependencies like databases or network calls. Integration and E2E tests can be slower but should still be optimized for performance.
Testing is an essential part of software development, ensuring that your code is reliable and maintainable. By following these practices and continuously writing tests, you can build robust NestJS applications that stand the test of time.
Stay tuned for more tutorials and deep dives into the world of NestJS!
Subscribe to my newsletter
Read articles from Nicanor Talks Web directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nicanor Talks Web
Nicanor Talks Web
Hi there! I'm Nicanor, and I'm a Software Engineer. I'm passionate about solving humanitarian problems through tech, and I love sharing my knowledge and insights with others.