Unit Tests — NestJS + Jest

~30 daq · NestJS · Dokumentatsiya va Testing

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.compare to'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