Unit Tests — NestJS + Jest
Muammodan boshlaylik
Oldingi darsda Jest ning asoslarini o'rgandingiz. Endi real NestJS loyihasida qanday test yozilishini ko'ramiz.
Test turlari uchta:
Unit Test Integration Test E2E Test
──────────────────────────────────────────────────────────────
Bitta funksiya Bir nechta qism Butun ilova
yoki sinf birga ishlashi boshdan oxir
Tez — ms da O'rtacha — s da Sekin — s-daqiqa
Mock ishlatiladi Haqiqiy yoki mock Haqiqiy server
DB bo'lishi mumkin va baza
"Bu funksiya "Service va repository "POST /users ga
to'g'ri birga to'g'ri yuborilsa javob
ishlayaptimi?" ishlayaptimi?" to'g'rimi?"
Bu dars → 55-dars →
Unit test — eng kichik birlik. Bitta service, bitta funksiya, bitta metod. Atrofidagi hamma narsa mock qilinadi.
Nima uchun mock qilinadi?
UsersService.create() ichida nima bor?
──────────────────────────────────────
1. userRepository.findOne() ← baza so'rovi
2. bcrypt.hash() ← parol hash
3. userRepository.save() ← baza so'rovi
4. emailService.sendWelcome()← email yuborish
Agar bularni mock qilmasak:
- Baza bo'lishi kerak (test muhitida yo'q bo'lishi mumkin)
- Email server bo'lishi kerak
- Test sekin ishlaydi (baza so'rovi = 50-200ms)
- Test noaniq — baza holati har safar boshqacha
Mock qilsak:
- Baza shart emas
- Test < 5ms da ishlaydi
- Natija doim bir xil (deterministik)
- Faqat service ning o'z logikasini tekshiramiz
Test loyihasini tushunish
Biz UsersService ni test qilamiz. Uning tuzilishi:
// users/users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly emailService: EmailService,
) {}
async create(dto: CreateUserDto): Promise<User> { ... }
async findAll(page: number, limit: number): Promise<...> { ... }
async findOne(id: number): Promise<User> { ... }
async update(id: number, dto: UpdateUserDto): Promise<User> { ... }
async remove(id: number): Promise<void> { ... }
}
Service ikki narsaga bog'liq: userRepository va emailService. Ikkalasini mock qilamiz.
Test moduli yaratish
// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConflictException, NotFoundException } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { EmailService } from '../email/email.service';
import * as bcrypt from 'bcrypt';
// bcrypt ni mock qilamiz — haqiqiy hash shart emas, tez ishlash uchun
jest.mock('bcrypt', () => ({
hash: jest.fn().mockResolvedValue('hashed_password'),
compare: jest.fn().mockResolvedValue(true),
}));
describe('UsersService', () => {
let service: UsersService;
let userRepo: jest.Mocked<Repository<User>>;
let emailService: jest.Mocked<EmailService>;
// ─── Mock ob'ektlar ───────────────────────────────
const mockUserRepository = {
findOne: jest.fn(),
findAndCount: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
softRemove: jest.fn(),
};
const mockEmailService = {
sendWelcome: jest.fn(),
sendPasswordReset: jest.fn(),
};
// ─── Test moduli ──────────────────────────────────
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockUserRepository,
},
{
provide: EmailService,
useValue: mockEmailService,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
userRepo = module.get(getRepositoryToken(User));
emailService = module.get(EmailService);
});
afterEach(() => {
jest.clearAllMocks();
// Har test tugagandan keyin mock lar tozalansin
// Bir test boshqasiga ta'sir qilmasin
});
create() — to'liq test
// ─────────────────────────────────────────────────
describe('create', () => {
// Test uchun qayta ishlatiladigan ma'lumotlar
const createDto = {
name: 'Ali Valiyev',
email: 'ali@test.com',
password: 'Secret123!',
};
const savedUser: User = {
id: 1,
name: 'Ali Valiyev',
email: 'ali@test.com',
password: 'hashed_password',
role: 'user',
isActive: true,
createdAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
} as User;
it('yangi foydalanuvchi yaratib qaytaradi', async () => {
// ARRANGE
userRepo.findOne.mockResolvedValue(null);
// "Bu email yo'q" — null qaytaradi
userRepo.create.mockReturnValue(savedUser);
userRepo.save.mockResolvedValue(savedUser);
emailService.sendWelcome.mockResolvedValue(undefined);
// ACT
const natija = await service.create(createDto);
// ASSERT
expect(natija).toEqual(savedUser);
// findOne to'g'ri argument bilan chaqirilganmi?
expect(userRepo.findOne).toHaveBeenCalledWith({
where: { email: createDto.email },
});
// Parol hash qilinganmi?
expect(bcrypt.hash).toHaveBeenCalledWith(createDto.password, 10);
// save chaqirilganmi?
expect(userRepo.save).toHaveBeenCalledTimes(1);
// Xush kelibsiz email yuborilganmi?
expect(emailService.sendWelcome).toHaveBeenCalledWith(savedUser.email);
});
it('email allaqachon band bo\'lsa ConflictException otadi', async () => {
// ARRANGE — bu email baza da bor
userRepo.findOne.mockResolvedValue(savedUser);
// ACT + ASSERT
await expect(service.create(createDto))
.rejects
.toThrow(ConflictException);
// Xato otilganda save CHAQIRILMASLIGI kerak
expect(userRepo.save).not.toHaveBeenCalled();
// Email ham YUBORILMASLIGI kerak
expect(emailService.sendWelcome).not.toHaveBeenCalled();
});
it('parol hash qilinib saqlanadi', async () => {
userRepo.findOne.mockResolvedValue(null);
userRepo.create.mockReturnValue(savedUser);
userRepo.save.mockResolvedValue(savedUser);
await service.create(createDto);
// create ga ochiq parol EMAS, hash berilishi kerak
expect(userRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
password: 'hashed_password',
// expect.objectContaining — faqat shu maydonni tekshir, qolganlar muhim emas
}),
);
});
});
findAll() — sahifalash testi
describe('findAll', () => {
it('sahifalab foydalanuvchilar qaytaradi', async () => {
const users = [
{ id: 1, name: 'Ali' },
{ id: 2, name: 'Vali' },
] as User[];
userRepo.findAndCount.mockResolvedValue([users, 10]);
// [ma'lumotlar, umumiy son]
const natija = await service.findAll(1, 5);
expect(natija).toEqual({
data: users,
total: 10,
page: 1,
lastPage: 2,
// Math.ceil(10 / 5) = 2
});
expect(userRepo.findAndCount).toHaveBeenCalledWith({
where: { isActive: true },
skip: 0, // (1-1) * 5
take: 5,
order: { createdAt: 'DESC' },
});
});
it('2-sahifada to\'g\'ri offset hisoblanadi', async () => {
userRepo.findAndCount.mockResolvedValue([[], 10]);
await service.findAll(2, 5);
expect(userRepo.findAndCount).toHaveBeenCalledWith(
expect.objectContaining({
skip: 5, // (2-1) * 5 = 5
take: 5,
}),
);
});
it('ma\'lumot yo\'q bo\'lsa bo\'sh massiv qaytaradi', async () => {
userRepo.findAndCount.mockResolvedValue([[], 0]);
const natija = await service.findAll(1, 10);
expect(natija.data).toHaveLength(0);
expect(natija.total).toBe(0);
expect(natija.lastPage).toBe(0);
});
});
findOne() — topish va topilmaslik
describe('findOne', () => {
it('mavjud foydalanuvchini qaytaradi', async () => {
const user = { id: 1, name: 'Ali' } as User;
userRepo.findOne.mockResolvedValue(user);
const natija = await service.findOne(1);
expect(natija).toEqual(user);
expect(userRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
it('topilmasa NotFoundException otadi', async () => {
userRepo.findOne.mockResolvedValue(null);
await expect(service.findOne(999))
.rejects
.toThrow(NotFoundException);
await expect(service.findOne(999))
.rejects
.toThrow('999-foydalanuvchi topilmadi');
// Xato xabari ham to'g'rimi?
});
});
update() — yangilash
describe('update', () => {
const existingUser: User = {
id: 1, name: 'Ali', email: 'ali@test.com',
password: 'hashed', role: 'user', isActive: true,
} as User;
it('foydalanuvchi ma\'lumotlarini yangilaydi', async () => {
const updateDto = { name: 'Ali Valiyev' };
userRepo.findOne.mockResolvedValue(existingUser);
userRepo.save.mockResolvedValue({ ...existingUser, ...updateDto });
const natija = await service.update(1, updateDto);
expect(natija.name).toBe('Ali Valiyev');
// Eski maydonlar saqlanib qolganmi?
expect(natija.email).toBe('ali@test.com');
});
it('parol kelsa yangi hash qilinadi', async () => {
const updateDto = { password: 'YangiParol123!' };
userRepo.findOne.mockResolvedValue(existingUser);
userRepo.save.mockResolvedValue({
...existingUser,
password: 'yangi_hashed',
});
await service.update(1, updateDto);
expect(bcrypt.hash).toHaveBeenCalledWith('YangiParol123!', 10);
});
it('parol kelmasa hash chaqirilmaydi', async () => {
const updateDto = { name: 'Yangi Ism' };
userRepo.findOne.mockResolvedValue(existingUser);
userRepo.save.mockResolvedValue({ ...existingUser, ...updateDto });
await service.update(1, updateDto);
expect(bcrypt.hash).not.toHaveBeenCalled();
});
it('foydalanuvchi topilmasa NotFoundException otadi', async () => {
userRepo.findOne.mockResolvedValue(null);
await expect(service.update(999, { name: 'Test' }))
.rejects
.toThrow(NotFoundException);
expect(userRepo.save).not.toHaveBeenCalled();
});
});
remove() — o'chirish
describe('remove', () => {
it('foydalanuvchini soft delete qiladi', async () => {
const user = { id: 1, name: 'Ali' } as User;
userRepo.findOne.mockResolvedValue(user);
userRepo.softRemove.mockResolvedValue(undefined);
await service.remove(1);
expect(userRepo.softRemove).toHaveBeenCalledWith(user);
expect(userRepo.softRemove).toHaveBeenCalledTimes(1);
});
it('topilmasa NotFoundException otadi', async () => {
userRepo.findOne.mockResolvedValue(null);
await expect(service.remove(999))
.rejects
.toThrow(NotFoundException);
expect(userRepo.softRemove).not.toHaveBeenCalled();
});
});
}); // describe('UsersService') — yopildi
Controller ni test qilish
Controller testida ham service mock qilinadi:
// users/users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { NotFoundException } from '@nestjs/common';
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
const mockUsersService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get(UsersService);
});
afterEach(() => jest.clearAllMocks());
// ─── POST /users ──────────────────────────────────
describe('create', () => {
it('service.create ni chaqiradi va natija qaytaradi', async () => {
const dto = { name: 'Ali', email: 'ali@test.com', password: '12345678' };
const createdUser = { id: 1, ...dto };
service.create.mockResolvedValue(createdUser as any);
const natija = await controller.create(dto as any);
expect(natija).toEqual(createdUser);
expect(service.create).toHaveBeenCalledWith(dto);
// Controller DTO ni service ga uzatdimi?
});
});
// ─── GET /users/:id ───────────────────────────────
describe('findOne', () => {
it('service.findOne ni chaqiradi', async () => {
const user = { id: 1, name: 'Ali' };
service.findOne.mockResolvedValue(user as any);
const natija = await controller.findOne(1);
expect(natija).toEqual(user);
expect(service.findOne).toHaveBeenCalledWith(1);
});
it('service xato otsa — controller uni o\'tkazib yuboradi', async () => {
service.findOne.mockRejectedValue(new NotFoundException());
await expect(controller.findOne(999))
.rejects
.toThrow(NotFoundException);
// Controller xatoni ushlamasligi kerak — global exception filter qiladi
});
});
// ─── DELETE /users/:id ────────────────────────────
describe('remove', () => {
it('service.remove ni chaqiradi', async () => {
service.remove.mockResolvedValue(undefined);
await controller.remove(1);
expect(service.remove).toHaveBeenCalledWith(1);
});
});
});
Controller vs Service testlarining farqi:
Service testi Controller testi
──────────────────────────────────────────────────────
Repository mock qilinadi Service mock qilinadi
Biznes logika tekshiriladi HTTP qatlam tekshiriladi
"Parol hash qilinganmi?" "DTO service ga uzatildimi?"
"Xato to'g'ri otildimi?" "Xato o'tkazib yuborilganmi?"
Murakkab holatlar
Bir nechta chaqiruvni tekshirish
it('create da ketma-ketlik to\'g\'ri', async () => {
userRepo.findOne.mockResolvedValue(null);
userRepo.create.mockReturnValue({ id: 1 } as User);
userRepo.save.mockResolvedValue({ id: 1 } as User);
await service.create(createDto);
// Chaqiruvlar tartibi to'g'rimi?
const findOneCall = userRepo.findOne.mock.invocationCallOrder[0];
const saveCall = userRepo.save.mock.invocationCallOrder[0];
expect(findOneCall).toBeLessThan(saveCall);
// findOne dan KEYIN save chaqirilishi kerak
});
spy — haqiqiy funksiyani kuzatish
it('Object.assign to\'g\'ri chaqiriladi', async () => {
const assignSpy = jest.spyOn(Object, 'assign');
// spyOn — haqiqiy funksiyani o'zgartirmaydi, faqat kuzatadi
userRepo.findOne.mockResolvedValue({ id: 1, name: 'Ali' } as User);
userRepo.save.mockResolvedValue({ id: 1, name: 'Yangi' } as User);
await service.update(1, { name: 'Yangi' });
expect(assignSpy).toHaveBeenCalled();
assignSpy.mockRestore();
// Asl funksiyani qaytarish
});
mockImplementation — murakkab mock
// Birinchi chaqiruvda null, ikkinchisida user qaytarsin
userRepo.findOne
.mockResolvedValueOnce(null) // 1-chaqiruv
.mockResolvedValueOnce(savedUser); // 2-chaqiruv
// Har safar boshqacha qaytarish
userRepo.findOne.mockImplementation((options) => {
if (options.where.id === 1) return Promise.resolve(savedUser);
return Promise.resolve(null);
});
Testlarni ishga tushirish
# Barcha testlar
npm test
# Faqat bitta fayl
npm test users.service
# Watch mode — fayl o'zgarganda qayta ishlaydi
npm run test:watch
# Coverage bilan
npm run test:cov
# Verbose — har test alohida ko'rinsin
npm test -- --verbose
Verbose natija:
UsersService
create
✓ yangi foydalanuvchi yaratib qaytaradi (8ms)
✓ email band bo'lsa ConflictException otadi (3ms)
✓ parol hash qilinib saqlanadi (2ms)
findAll
✓ sahifalab foydalanuvchilar qaytaradi (4ms)
✓ 2-sahifada to'g'ri offset hisoblanadi (2ms)
✓ ma'lumot yo'q bo'lsa bo'sh massiv (1ms)
findOne
✓ mavjud foydalanuvchini qaytaradi (2ms)
✓ topilmasa NotFoundException otadi (2ms)
update
✓ foydalanuvchi ma'lumotlarini yangilaydi (3ms)
✓ parol kelsa yangi hash qilinadi (2ms)
✓ parol kelmasa hash chaqirilmaydi (1ms)
✓ foydalanuvchi topilmasa NotFoundException(2ms)
remove
✓ foydalanuvchini soft delete qiladi (2ms)
✓ topilmasa NotFoundException otadi (1ms)
Tests: 14 passed Time: 1.2s
Yaxshi test yozish qoidalari
1. Har bir test bitta narsani tekshirsin
// ❌ Yomon — bitta testda ko'p narsa
it('create ishlaydi', async () => {
// email tekshirish
// parol hash
// save
// email yuborish
// ... hammasi bitta testda
});
// ✅ Yaxshi — alohida testlar
it('yangi foydalanuvchi yaratadi', async () => { ... });
it('email band bo\'lsa xato otadi', async () => { ... });
it('parol hash qilinadi', async () => { ... });
it('xush kelibsiz email yuboriladi', async () => { ... });
2. Test nomi nima bo'lishi kerakligini aytsin
// ❌ Yomon
it('test 1', () => { ... });
it('ishlaydi', () => { ... });
// ✅ Yaxshi — nom o'qiganda nima bo'layotgani tushuniladi
it('email band bo\'lsa ConflictException 409 otadi', () => { ... });
it('topilmasa NotFoundException 404 otadi', () => { ... });
3. Muvaffaqiyat va xato holatlarini ikkalasini tekshiring
// Faqat muvaffaqiyat emas...
it('foydalanuvchi yaratadi', ...);
// ...xato ham tekshirilsin
it('email band bo\'lsa xato', ...);
it('validatsiya o\'tmasa xato', ...);
4. beforeEach da ma'lumotlarni yangilang
// ❌ Yomon — testlar bir-biriga ta'sir qilishi mumkin
let user = { id: 1, name: 'Ali' };
// ✅ Yaxshi — har test uchun yangi ob'ekt
beforeEach(() => {
user = { id: 1, name: 'Ali' };
});
Vazifalar
1-vazifa — Asosiy:
PostsService uchun unit test yozing. create, findOne, remove metodlarini test qiling. Kamida 8 ta test holati bo'lsin (muvaffaqiyat va xato).
2-vazifa — Chuqurroq:
AuthService.login() ni test qiling:
- Email topilmasa
UnauthorizedException - Parol noto'g'ri bo'lsa
UnauthorizedException - To'g'ri credentials bilan
{ accessToken }qaytaradi bcrypt.compareto'g'ri chaqirilganini tekshiring
3-vazifa — O'ylash: Controller testida service mock qilinadi. Service testida repository mock qilinadi. Bu "unit" test deyilishiga nima asas beradi? Agar mock qilmasdan haqiqiy baza bilan ishlasak — bu qanday test deyiladi?
Xulosa
Unit test nima: Bitta sinf yoki funksiyani alohida test qilish. Tashqi bog'liqliklar mock qilinadi.
Nima uchun mock: Baza, email, tashqi API shart emas. Test tez ishlaydi. Natija doim bir xil.
Test tuzilishi:
beforeEach → Test moduli yaratish, mock lar tayyorlash
afterEach → jest.clearAllMocks()
describe → Mantiqiy guruh (metod nomi bo'yicha)
it → Bitta holat (muvaffaqiyat yoki xato)
Tekshirilishi kerak bo'lgan narsalar:
- Natija to'g'rimi? (
expect(natija).toEqual(...)) - To'g'ri argument bilan chaqirildimi? (
toHaveBeenCalledWith) - Necha marta chaqirildi? (
toHaveBeenCalledTimes) - Chaqirilmadimi? (
not.toHaveBeenCalled) - Xato otildimi? (
rejects.toThrow)
AAA pattern: Arrange → Act → Assert — har testda shu tartib.
Keyingi dars
55-dars: E2E Tests — NestJS + Jest