Skip to content

Part 4.2 — Writing Unit Tests for Services, DTOs & Pipes

Site Console Site Console
4 min read Updated Dec 9, 2025 Backend Development 0 comments

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.ts

You 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

Leave a comment

Sign in to leave a comment.

Comments