Appearance
Chapter 8 - Unit Testing
Ikhtisar Chapter
Chapter ini memperkenalkan unit testing dengan Jest di NestJS. Fokusnya adalah menguji TasksService secara terisolasi dengan mock repository, termasuk skenario sukses dan error.
Peta Cepat
FokusJest, TestingModule, mock repository, async tests
ManfaatRefactor lebih aman dan bug lebih cepat diketahui
Hasil AkhirUnit test untuk method service, termasuk NotFoundException
Gambaran Besar
Testing adalah safety net. Ketika aplikasi bertambah besar, perubahan kecil dapat memecahkan behavior lama tanpa terlihat. Unit test membantu memeriksa satu unit secara terisolasi, misalnya service tanpa database sungguhan.
NestJS menggunakan Jest secara default. Untuk testing service yang bergantung pada repository, repository asli diganti mock. Dengan begitu, test hanya menilai logika service, bukan koneksi database.
Struktur Unit Test
text
describe('TasksService')
|
|-- beforeEach: buat TestingModule baru
|-- mock repository: getTasks, findOne, delete, save
|-- module.get(): ambil service dan repository mock
|
|-- it('success case')
|-- it('error case')Struktur Direktori
Chapter ini menambahkan satu file spec untuk setiap unit yang diuji. File spec ditempatkan di sebelah file yang diuji — bukan di folder test/ terpisah:
src/
└── tasks/
├── tasks.service.ts
└── tasks.service.spec.ts ← BARU: unit test untuk TasksService
test/
└── app.e2e-spec.ts ← sudah ada: end-to-end test (bukan fokus chapter ini)Konvensi Penamaan Jest
Jest secara default menemukan test file dengan pola **/*.spec.ts atau **/*.test.ts. Artinya selama nama file berakhir dengan .spec.ts, Jest akan menemukannya secara otomatis tanpa konfigurasi tambahan.
File yang diuji → File test-nya
─────────────────────────────────────
tasks.service.ts → tasks.service.spec.ts
auth.service.ts → auth.service.spec.ts
tasks.controller.ts → tasks.controller.spec.tsRingkasan Lecture
1. Unit Testing Crash Course: Basics
Jest adalah framework testing default pada proyek NestJS. File test memakai suffix .spec.ts dan ditempatkan di sebelah file yang diuji. Struktur dasar sebuah test file:
typescript
// src/tasks/tasks.service.spec.ts
describe('TasksService', () => {
// describe mengelompokkan semua test yang berkaitan
// argumen pertama: nama group, argumen kedua: callback
it('should return true when the sky is blue', () => {
// it (alias test) mendefinisikan satu skenario
const result = true
expect(result).toBe(true) // assertion — apa yang kita harapkan
})
it('should fail when result is wrong', () => {
expect(1 + 1).toEqual(2) // toEqual untuk perbandingan nilai
})
})Jalankan semua test satu kali:
bash
yarn testJalankan dalam mode watch (re-run otomatis saat ada perubahan):
bash
yarn test --watchJalankan hanya file tertentu:
bash
yarn test tasks.service.spec.ts2. Unit Testing Crash Course: First Tests
Fungsi yang pure (tidak punya side effect, output hanya bergantung input) paling mudah dites. Sebelum masuk ke NestJS, konsep dasar assertion perlu dipahami:
typescript
// Contoh testing fungsi pure sederhana
function add(a: number, b: number): number {
return a + b
}
describe('add', () => {
it('should return sum of two numbers', () => {
expect(add(2, 3)).toEqual(5) // OK
expect(add(0, 0)).toEqual(0) // edge case: nol
expect(add(-1, 1)).toEqual(0) // edge case: negatif
})
it('should NOT return wrong value', () => {
expect(add(2, 2)).not.toEqual(5) // negasi dengan .not
})
})Matcher umum Jest:
| Matcher | Kegunaan |
|---|---|
.toBe(val) | Perbandingan dengan === (nilai primitif) |
.toEqual(val) | Perbandingan deep untuk object/array |
.toBeTruthy() | Nilai truthy (tidak falsy) |
.toBeNull() | Nilai null |
.toContain(item) | Array mengandung item tertentu |
.toThrow() | Fungsi melempar error |
3. Fixing Import Paths
NestJS CLI menghasilkan alias path @/ untuk src/. Import seperti ini bisa bermasalah di Jest karena Jest tidak membaca konfigurasi alias TypeScript secara default:
typescript
// Bisa bermasalah di Jest
import { TasksService } from '@/tasks/tasks.service'
// Lebih aman: relative path
import { TasksService } from '../tasks/tasks.service'Jika ingin mempertahankan alias, perlu mengkonfigurasi moduleNameMapper di jest.config.js atau package.json:
json
// package.json — bagian jest
{
"jest": {
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/src/$1"
}
}
}4. Testing TasksService — Part 1
Kunci unit testing di NestJS adalah Test.createTestingModule() — mirip @Module sungguhan, tetapi hanya untuk keperluan testing. Repository asli diganti dengan factory mock:
typescript
// src/tasks/tasks.service.spec.ts
import { Test } from '@nestjs/testing'
import { TasksService } from './tasks.service'
import { TasksRepository } from './tasks.repository'
// Factory yang menghasilkan object dengan semua method repository sebagai mock
const mockTasksRepository = () => ({
getTasks: jest.fn(),
findOne: jest.fn(),
createTask: jest.fn(),
delete: jest.fn(),
save: jest.fn(),
})
describe('TasksService', () => {
let tasksService: TasksService
let tasksRepository: ReturnType<typeof mockTasksRepository>
beforeEach(async () => {
// Buat module baru sebelum setiap test
// beforeEach memastikan setiap test mulai dari state bersih
const module = await Test.createTestingModule({
providers: [
TasksService,
{
provide: TasksRepository, // token yang diganti
useFactory: mockTasksRepository, // factory yang menghasilkan mock
},
],
}).compile()
// Ambil instance dari module yang baru dikompilasi
tasksService = module.get<TasksService>(TasksService)
tasksRepository = module.get<TasksRepository>(TasksRepository)
})
// test cases akan ditambahkan di sini
})Setiap kali beforeEach dijalankan, module baru dibuat dan mock function direset — tidak ada state dari test sebelumnya yang bocor.
5. Testing TasksService — Part 2
Test getTaskById() mencakup dua skenario: task ditemukan dan task tidak ditemukan. Karena method ini async, test harus async dan menggunakan await:
typescript
// Skenario 1: happy path — task ditemukan
describe('getTaskById', () => {
it('returns the task if found', async () => {
const mockTask = {
id: 'uuid-1',
title: 'Test task',
description: 'Test desc',
status: TaskStatus.OPEN,
}
// atur agar findOne mengembalikan mockTask
tasksRepository.findOne.mockResolvedValue(mockTask)
const result = await tasksService.getTaskById('uuid-1', mockUser)
expect(result).toEqual(mockTask) // harus mengembalikan task yang sama
})
// Skenario 2: error path — task tidak ditemukan
it('throws NotFoundException if task not found', async () => {
// atur agar findOne mengembalikan null (tidak ditemukan)
tasksRepository.findOne.mockResolvedValue(null)
await expect(
tasksService.getTaskById('nonexistent-id', mockUser)
).rejects.toThrow(NotFoundException)
// .rejects.toThrow — cara mengecek bahwa Promise di-reject dengan exception tertentu
})
})mockResolvedValue(value) mengatur apa yang dikembalikan mock function sebagai Promise yang resolved. Untuk mensimulasikan error database, gunakan mockRejectedValue(new Error('DB error')).
Tes error path sama pentingnya dengan happy path. Bahkan, bug produksi paling berbahaya sering tersembunyi di edge case dan skenario gagal yang tidak pernah dites.
Konsep Kunci
| Konsep | Fungsi | Contoh |
|---|---|---|
describe | Mengelompokkan test | describe('TasksService', () => {}) |
it | Satu skenario test | it('should return task', async () => {}) |
expect | Assertion | expect(result).toEqual(task) |
jest.fn() | Membuat mock function | findOne: jest.fn() |
mockResolvedValue | Mengatur hasil promise mock | repository.findOne.mockResolvedValue(task) |
| TestingModule | Module NestJS untuk test | Test.createTestingModule() |
beforeEach | Setup baru sebelum setiap test | Mencegah state bocor antar test |
rejects.toThrow | Mengecek async exception | await expect(promise).rejects.toThrow(...) |
Pola Test Service
typescript
describe('TasksService', () => {
let service: TasksService
let repository: TasksRepository
const mockTasksRepository = () => ({
getTasks: jest.fn(),
findOne: jest.fn(),
})
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
TasksService,
{
provide: TasksRepository,
useFactory: mockTasksRepository,
},
],
}).compile()
service = module.get<TasksService>(TasksService)
repository = module.get<TasksRepository>(TasksRepository)
})
})typescript
it('throws NotFoundException when task is not found', async () => {
repository.findOne.mockResolvedValue(null)
await expect(service.getTaskById('invalid-id', mockUser))
.rejects
.toThrow(NotFoundException)
})Jebakan Umum
- Menguji terlalu banyak layer sekaligus lalu menyebutnya unit test.
- Memakai repository asli sehingga test tergantung database.
- Lupa
awaitsaat mengetes promise. - Hanya mengetes happy path dan lupa error path.
- Membuat nama test terlalu umum, misalnya
should work, sehingga sulit dipahami saat gagal. - Tidak membuat setup baru pada
beforeEach, sehingga state dari test sebelumnya bocor.
Pertanyaan Reflektif
- Apa perbedaan unit test dan integration test pada service NestJS?
- Mengapa repository perlu di-mock saat menguji service?
- Apa manfaat
beforeEach()dalam test yang memakai dependency injection? - Bagaimana cara mengetes method async yang melempar exception?
- Mengapa error path sama pentingnya dengan happy path?