Part 4.2 — Writing Unit Tests for Services, DTOs & Pipes
Your backend’s reliability depends heavily on its business logic, validation rules, and small pieces of reusable code like pipes.
Unit tests are where you verify these behaviors in isolation — without spinning up PostgreSQL or the full NestJS application.
In this post, you’ll write:
service unit tests (Prisma mocked)
DTO validation tests
custom pipe tests
These fast tests give you instant feedback while coding.
1. Unit Testing Philosophy in NestJS
Unit tests should:
not boot an HTTP server
not touch a real PostgreSQL database
not spin up the full DI container
Instead, they should:
mock Prisma
test logic inside your services
test DTO validation programmatically
test custom pipes with simple inputs
Think of these as pure logic tests.
2. Testing Services (Mocking Prisma)
Example: testing UsersService
src/users/users.service.tsYou mock Prisma using a simple object:
const prismaMock = {
user: {
create: jest.fn(),
findMany: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
};Service test structure:
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(() => {
service = new UsersService(prismaMock as any);
});
it('creates a user', async () => {
prismaMock.user.create.mockResolvedValue({
id: 1,
name: 'Alice',
email: 'alice@example.com',
});
const result = await service.create({
name: 'Alice',
email: 'alice@example.com',
});
expect(result.email).toBe('alice@example.com');
expect(prismaMock.user.create).toHaveBeenCalled();
});
});Why mock Prisma?
Much faster
No DB required
Ideal for testing service logic
Predictable behavior
This keeps your unit test environment lightweight.
3. Testing Nested Relations in Services
If your service fetches relations:
findOne(id: number) {
return this.prisma.user.findUnique({
where: { id },
include: { posts: true },
});
}Test it like:
it('finds a user with posts', async () => {
prismaMock.user.findUnique.mockResolvedValue({
id: 1,
name: 'Alice',
posts: [{ id: 10, title: 'Hello' }],
});
const result = await service.findOne(1);
expect(result.posts.length).toBe(1);
});You don’t care about database behavior — only service logic.
4. Testing DTOs Programmatically (class-validator)
DTO validation is crucial because controllers accept raw input.
Create a helper:
import { validate } from 'class-validator';
export async function validateDto(dto: any) {
const errors = await validate(dto);
return errors.map(e => Object.values(e.constraints)).flat();
}Example DTO test:
import { CreateUserDto } from './create-user.dto';
import { validateDto } from '../../test/utils/validate-dto';
it('rejects invalid email', async () => {
const dto = new CreateUserDto();
dto.name = 'Bob';
dto.email = 'invalid-email';
const errors = await validateDto(dto);
expect(errors).toContain('email must be an email');
});Programmatic DTO validation gives strong confidence before hitting HTTP layer tests.
5. Testing Optional & Required Fields
Given:
export class UpdateUserDto {
@IsOptional()
name?: string;
}Test:
it('accepts optional fields', async () => {
const dto = new UpdateUserDto();
const errors = await validateDto(dto);
expect(errors.length).toBe(0);
});For required:
it('rejects missing name', async () => {
const dto = new CreateUserDto();
dto.email = 'bob@example.com';
const errors = await validateDto(dto);
expect(errors).toContain('name should not be empty');
});6. Testing Custom Pipes
Example pipe from Part 3.4:
export class ParseIdPipe implements PipeTransform {
transform(value: string) {
const parsed = Number(value);
if (Number.isNaN(parsed)) {
throw new BadRequestException('ID must be a number');
}
return parsed;
}
}Test the pipe:
import { ParseIdPipe } from './parse-id.pipe';
import { BadRequestException } from '@nestjs/common';
describe('ParseIdPipe', () => {
const pipe = new ParseIdPipe();
it('parses a number', () => {
expect(pipe.transform('5')).toBe(5);
});
it('throws on invalid input', () => {
expect(() => pipe.transform('abc')).toThrow(BadRequestException);
});
});Pipes are tiny but critical — one mistake can break every endpoint.
7. Testing Error Scenarios in Services
Example:
it('throws when updating non-existing user', async () => {
prismaMock.user.update.mockRejectedValue(
new Error('Record not found'),
);
await expect(
service.update(999, { name: 'Test' }),
).rejects.toThrow();
});Tests should cover:
missing records
relation problems
Prisma errors you explicitly map in filters
edge-case scenarios (empty updates, invalid IDs, etc.)
8. Service Tests Should Not Test Controllers
Service tests should:
mock Prisma
test business logic only
Controller tests happen in Part 4.3 using Supertest.
Keep layers independent.
9. Snapshot Testing (Optional but Useful)
Snapshots help with complex error shapes:
expect(result).toMatchSnapshot();Great for DTO validation or error filters.
10. Summary
You now know how to:
mock Prisma for fast, isolated unit tests
write meaningful service tests
programmatically validate DTOs
test custom pipes
assert expected error behavior
With this foundation, you’re ready for integration testing.
Related
Part 4.1 — Setting Up Jest, Test Environment & Prisma Test DB
Before you write your first test, you need a stable foundation: Jest configuration, test modules, a sandboxed PostgreSQL database, and a clean Prisma workflow for reproducible tests.
Part 3.4 — Error Handling, Pipes, Filters & Global Interceptors
Production backends thrive on predictable errors, structured validation, and consistent responses. In this post, you’ll learn how to build those foundations using NestJS pipes, filters, and interceptors.
Part 3.3 — Prisma Queries, Relations & Best Practices
This post teaches you how to design efficient, relational Prisma queries while avoiding N+1 errors, modeling relations cleanly, and thinking like a PostgreSQL-first backend engineer.
Comments