Testing Services
⭐ Interview Importance: HIGH
⏱️ Revision Time: 7 min
Testing Services is the core of Unit Testing in NestJS. Since business logic resides almost entirely within services, this is where you will write the vast majority of your tests, ensuring the core algorithms and rules of your application are sound.
Overview
A Service is a class annotated with @Injectable(). It typically injects Repositories (to access the database) or other external Services (like an HTTP client).
To test a service, you must use the TestingModule to instantiate the service while providing mocked versions of all its injected dependencies.
Key Concepts
- Business Logic Focus: You are testing the
if/elsestatements, calculations, and data transformations inside the service methods. You are not testing if the database can successfully save a record. - Provider Overrides: Using
{ provide: Dependency, useValue: mockDependency }to satisfy the constructor requirements of the service being tested.
Code Examples
1. Testing a Service with Repository Dependencies
Assume we have an OrderService that calculates discounts and then saves the order using a TypeORM OrderRepository.
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { OrderService } from './order.service';
import { Order } from './order.entity';
describe('OrderService', () => {
let service: OrderService;
// Create a mock for the TypeORM Repository
const mockOrderRepository = {
save: jest.fn(),
findOne: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
OrderService, // The service we are actually testing
{
// When OrderService asks for the Order repository, give it the mock!
provide: getRepositoryToken(Order),
useValue: mockOrderRepository,
},
],
}).compile();
service = module.get<OrderService>(OrderService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createOrder', () => {
it('should apply a 10% discount if the user is VIP', async () => {
// Arrange
const mockSavedOrder = { id: 1, total: 90, isVip: true };
// Tell the mock repository what to return when save() is called
mockOrderRepository.save.mockResolvedValue(mockSavedOrder);
// Act: Call the service method
// Assume base price is 100, user is VIP
const result = await service.createOrder(100, true);
// Assert: Verify the business logic correctly calculated 90
expect(result.total).toBe(90);
// Assert: Verify the service passed the correct calculated data to the database
expect(mockOrderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({ total: 90 }) // We only care that the total was 90
);
});
it('should throw an error if the base price is negative', async () => {
// Act & Assert
await expect(service.createOrder(-50, false))
.rejects
.toThrow('Price cannot be negative');
// Verify that the database was NEVER called if the price was invalid
expect(mockOrderRepository.save).not.toHaveBeenCalled();
});
});
});
Best Practices
- Use
getRepositoryToken: When mocking TypeORM repositories, you must use thegetRepositoryToken(EntityClass)function as the injection token. You cannot use the string name of the entity, as NestJS internally generates a specific token string for repositories. - Don’t Mock Internal Methods: If
MethodAcallsMethodB(both inside the same Service), do not mockMethodBwhen testingMethodA. TestMethodAas a complete, cohesive unit of work. Only mock external classes injected via the constructor. - Use
expect.objectContaining(): When asserting that a mock was called with specific arguments, avoid asserting against massive, deeply nested objects if you only care about one specific field (like the calculated discount). Useexpect.objectContaining({ discount: 10 })to make tests less brittle when unrelated properties change.