Last Updated: February 2026
Testing Framework: Jest
Coverage Target: 80%+
✅ Test what the code does:
it("should throw error when user not found", async () => {
await expect(service.findById("invalid-id")).rejects.toThrow(NotFoundException);
});❌ Don't test how it does it:
it("should call repository.findById", async () => {
const spy = jest.spyOn(repository, "findById");
await service.findById("id");
expect(spy).toHaveBeenCalled(); // Testing implementation!
});| Layer | Minimum Coverage | Priority |
|---|---|---|
| Services | 90%+ | 🔴 Critical |
| Repositories | 70%+ | 🟡 High |
| Guards | 95%+ | 🔴 Critical |
| Controllers | 80%+ | 🟢 Medium |
| DTOs | 100% | 🔴 Critical |
| Utils | 80%+ | 🟢 Medium |
Overall Target: 80%+
Tests live next to the code:
src/services/
├── user.service.ts
└── user.service.spec.ts ← Same directory
| Code File | Test File |
|---|---|
user.service.ts |
user.service.spec.ts |
user.repository.ts |
user.repository.spec.ts |
import { Test, TestingModule } from "@nestjs/testing";
import { ServiceUnderTest } from "./service-under-test";
import { DependencyOne } from "./dependency-one";
describe("ServiceUnderTest", () => {
let service: ServiceUnderTest;
let dependency: jest.Mocked<DependencyOne>;
beforeEach(async () => {
const mockDependency = {
method: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [ServiceUnderTest, { provide: DependencyOne, useValue: mockDependency }],
}).compile();
service = module.get<ServiceUnderTest>(ServiceUnderTest);
dependency = module.get(DependencyOne);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("methodName", () => {
it("should return expected result", async () => {
// Arrange
dependency.method.mockResolvedValue("data");
// Act
const result = await service.methodName();
// Assert
expect(result).toBe("expected");
});
it("should handle errors", async () => {
// Arrange
dependency.method.mockRejectedValue(new Error("DB error"));
// Act & Assert
await expect(service.methodName()).rejects.toThrow(InternalServerErrorException);
});
});
});const mockRepository = {
findById: jest.fn(),
create: jest.fn(),
updateById: jest.fn(),
deleteById: jest.fn(),
list: jest.fn(),
};
// In test
mockRepository.findById.mockResolvedValue({
_id: "id",
name: "Test",
});const mockModel = {
findById: jest.fn().mockReturnThis(),
findOne: jest.fn().mockReturnThis(),
find: jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
lean: jest.fn().mockResolvedValue({}),
exec: jest.fn(),
};const mockLogger = {
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};What to test:
- ✅ Business logic correctness
- ✅ Error handling
- ✅ Edge cases
- ✅ State changes
Example:
describe("createItem", () => {
it("should create item with valid data", async () => {
mockRepository.create.mockResolvedValue(mockItem);
const result = await service.createItem(validDto);
expect(result).toEqual(mockItem);
});
it("should throw BadRequestException for invalid data", async () => {
await expect(service.createItem(invalidDto)).rejects.toThrow(BadRequestException);
});
});What to test:
- ✅ CRUD operations
- ✅ Query logic
- ✅ Population/aggregation
Example:
describe("findByEmail", () => {
it("should return user when email exists", async () => {
modelMock.findOne.mockResolvedValue(mockUser);
const user = await repository.findByEmail("test@example.com");
expect(user).toEqual(mockUser);
expect(modelMock.findOne).toHaveBeenCalledWith({
email: "test@example.com",
});
});
});What to test:
- ✅ Allow authorized requests
- ✅ Deny unauthorized requests
- ✅ Token validation
- ✅ Role checks
Example:
describe("canActivate", () => {
it("should allow authenticated users", async () => {
const context = createMockContext(validToken);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it("should deny missing token", async () => {
const context = createMockContext(null);
await expect(guard.canActivate(context)).rejects.toThrow(UnauthorizedException);
});
});What to test:
- ✅ Route handlers call correct service methods
- ✅ Response formatting
- ✅ Error propagation
Example:
describe("getItems", () => {
it("should return list of items", async () => {
mockService.list.mockResolvedValue([mockItem]);
const result = await controller.getItems();
expect(result).toEqual([mockItem]);
expect(mockService.list).toHaveBeenCalled();
});
});# Run all tests
npm test
# Watch mode
npm run test:watch
# Coverage report
npm run test:coverage
# Run specific test file
npm test -- user.service.spec.ts// ❌ BAD
it("should call bcrypt.hash", () => {
const spy = jest.spyOn(bcrypt, "hash");
service.method();
expect(spy).toHaveBeenCalled();
});
// ✅ GOOD
it("should hash password", async () => {
const result = await service.hashPassword("password");
expect(result).not.toBe("password");
expect(result.length).toBeGreaterThan(20);
});// ✅ Always clean up
afterEach(() => {
jest.clearAllMocks();
});// ❌ Missing await
it("test", () => {
expect(service.asyncMethod()).resolves.toBe("value");
});
// ✅ Proper async handling
it("test", async () => {
await expect(service.asyncMethod()).resolves.toBe("value");
});- All tests passing
- Coverage >= 80%
- No skipped tests (it.skip)
- No focused tests (it.only)
- Mocks cleaned up in afterEach
- Async operations properly awaited
- Error cases tested
This package provides shared test utilities in test/test-utils.ts to reduce code duplication and make testing easier.
import {
MockRepository,
MockSender,
MockTemplateEngine,
MockEventEmitter,
MockFailingSender,
} from "../test/test-utils";
// In-memory notification repository
const repository = new MockRepository();
await repository.create(notification);
// Mock notification sender (always succeeds)
const sender = new MockSender(NotificationChannel.EMAIL);
await sender.send(recipient, content);
// Mock sender that simulates failures
const failingSender = new MockFailingSender();
failingSender.setShouldFail(true);
// Mock template engine
const templateEngine = new MockTemplateEngine();
await templateEngine.render("welcome", { name: "John" });
// Mock event emitter
const eventEmitter = new MockEventEmitter();
eventEmitter.on("notification.sent", handler);import {
createNotificationServiceWithDeps,
createFailingNotificationServiceWithDeps,
createModuleTestOptions,
} from "../test/test-utils";
// Create service with all mocked dependencies
const { service, repository, sender, idGenerator, dateTimeProvider } =
createNotificationServiceWithDeps();
// Create service with failing sender for error testing
const { service: failingService, repository: failingRepo } =
createFailingNotificationServiceWithDeps();
// Create module configuration for NestJS testing
const options = createModuleTestOptions({
senders: [new MockSender()],
repository: new MockRepository(),
});import { defaultNotificationDto, createMockNotification } from "../test/test-utils";
// Standard notification DTO for tests
const notification = await service.send(defaultNotificationDto);
// Create mock notification with custom overrides
const mockNotification = createMockNotification({
status: NotificationStatus.SENT,
priority: NotificationPriority.HIGH,
});import { createNotificationServiceWithDeps, defaultNotificationDto } from "../test/test-utils";
describe("MyFeature", () => {
let service: NotificationService;
let repository: MockRepository;
beforeEach(() => {
const ctx = createNotificationServiceWithDeps();
service = ctx.service;
repository = ctx.repository;
});
it("should create notification", async () => {
const notification = await service.create(defaultNotificationDto);
expect(notification.id).toBeDefined();
expect(notification.status).toBe(NotificationStatus.QUEUED);
});
it("should send notification", async () => {
const result = await service.send(defaultNotificationDto);
expect(result.success).toBe(true);
// Repository is shared, can verify persistence
const notifications = await repository.find({});
expect(notifications).toHaveLength(1);
});
});- ✅ Reduced duplication: Centralized mock implementations
- ✅ Consistent behavior: All tests use the same mocks
- ✅ Easy setup: Factory functions handle complex initialization
- ✅ Type safety: Full TypeScript support
- ✅ Maintainable: Changes to mocks update all tests automatically
The package maintains comprehensive test coverage:
- Total Tests: 133+
- Test Suites: 10
- Code Duplication: 2.66% (well below 3% threshold)
- Coverage Target: 80%+ (achieved)
- ✅ Core domain tests (notification.service.test.ts)
- ✅ DTO validation tests (dtos.test.ts)
- ✅ Error handling tests (errors.test.ts)
- ✅ Provider tests (providers.test.ts)
- ✅ Controller tests (notification.controller.test.ts, webhook.controller.test.ts)
- ✅ Module tests (module.test.ts)
- ✅ Decorator tests (decorators.test.ts)
- ✅ Integration tests (integration.test.ts)
- ✅ Smoke tests (smoke.test.ts)
# Run all tests
npm test
# Run with coverage
npm run test:cov
# Watch mode for development
npm run test:watch
# Run specific test file
npm test -- notification.service.test.ts
# Run tests matching pattern
npm test -- --testNamePattern="should send notification"When adding new tests:
- Use shared utilities from
test/test-utils.tsto avoid duplication - Follow naming conventions:
[feature].test.tsor[feature].spec.ts - Test behavior, not implementation details
- Include error cases and edge conditions
- Keep tests independent - no shared state between tests
- Use descriptive names:
it('should [expected behavior] when [condition]') - Clean up mocks in
afterEach()hooks
Before committing:
npm run lint # Check code style
npm run typecheck # Check TypeScript types
npm test # Run all tests
npm run test:cov # Verify coverageAll checks must pass before merging to main branch.