Writing Clean and Maintainable Code in NestJS
Part 11 of 12
Writing Clean and Maintainable Tests in NestJS
Testing is crucial for maintaining reliable and bug-free applications. However, poorly written tests can be just as harmful as no tests at all. In this guide, we’ll explore:
- Avoiding flaky tests
- Structuring unit and integration tests properly
- Using mocks and spies effectively
1. Avoiding Flaky Tests
A flaky test is a test that sometimes passes and sometimes fails without code changes.
This leads to false confidence and wasted debugging time.
❌ Common Causes of Flaky Tests
- Tests dependent on external APIs or databases
- Race conditions due to improper async handling
- Shared global state between tests
- Randomized data without predictable assertions
✅ How to Avoid Flaky Tests
- Use dependency injection & mocks to avoid external API failures
- Ensure database transactions are properly rolled back in tests
- Use deterministic test data instead of random values
- Avoid testing implementation details – focus on behavior
2. Structuring Unit and Integration Tests Properly
In NestJS, we commonly write:
- Unit tests – Test isolated components (services, utilities)
- Integration tests – Test multiple components working together
✅ Unit Test Example (UserService)
📌 user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
describe('UserService', () => {
let userService: UserService;
let userRepository: Partial<UserRepository>;
beforeEach(async () => {
userRepository = {
findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'John Doe' }),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{ provide: UserRepository, useValue: userRepository },
],
}).compile();
userService = module.get<UserService>(UserService);
});
it('should return a user by ID', async () => {
const user = await userService.getUserById('1');
expect(user).toEqual({ id: '1', name: 'John Doe' });
expect(userRepository.findUserById).toHaveBeenCalledWith('1');
});
});
✅ Mocks dependencies using Jest
✅ Uses a clean beforeEach
setup
✅ Tests only the behavior, not implementation details
✅ Integration Test Example (UserController)
📌 user.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';
describe('UserController', () => {
let userController: UserController;
let userService: Partial<UserService>;
beforeEach(async () => {
userService = {
getUserById: jest.fn().mockResolvedValue({ id: '1', name: 'John Doe' }),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [{ provide: UserService, useValue: userService }],
}).compile();
userController = module.get<UserController>(UserController);
});
it('should return user data', async () => {
const user = await userController.getUser('1');
expect(user).toEqual({ id: '1', name: 'John Doe' });
expect(userService.getUserById).toHaveBeenCalledWith('1');
});
});
✅ Mocks UserService
instead of using the real service
✅ Keeps dependencies isolated
✅ Ensures controller works correctly without needing an actual database
3. Using Mocks and Spies Effectively
When to Use Mocks?
Mocks help replace real dependencies with controlled implementations for testing.
✅ Use Mocks for:
- Databases (
UserRepository
) – Prevents real DB calls - External APIs (
HttpService
) – Avoids network failures - Services (
UserService
) – Keeps tests isolated
✅ Mocking External API Calls
📌 http.service.spec.ts
import { HttpService } from '@nestjs/axios';
import { of } from 'rxjs';
describe('External API Call', () => {
let httpService: HttpService;
beforeEach(() => {
httpService = new HttpService();
jest
.spyOn(httpService, 'get')
.mockReturnValue(of({ data: { result: 'success' } }));
});
it('should call external API and return success', async () => {
const result = await httpService
.get('https://api.example.com/data')
.toPromise();
expect(result.data.result).toBe('success');
});
});
✅ Mocks external API requests with Jest spies
✅ Avoids network dependency in tests
When to Use Spies?
A spy wraps a real function and tracks calls to it.
const spy = jest.spyOn(userService, 'getUserById');
✅ Spies are useful for tracking function calls without overriding behavior
4. Summary: Best Practices for Clean Testing
✅ Avoid flaky tests by using mocks and handling async properly
✅ Structure unit and integration tests clearly
✅ Use Jest mocks and spies to isolate dependencies
✅ Test behavior, not implementation details