E2E Tests — NestJS + Jest

~30 daq · NestJS · Dokumentatsiya va Testing

Unit Test va E2E Test — farqi nima?

Oldingi darsda Unit Test yozdingiz. Har bir funksiyani alohida, mock bilan test qildingiz. Tez, izolyatsiyalangan.

E2E (End-to-End) — boshqacha yondashuv:

  Unit Test                         E2E Test
  ──────────────────────────────────────────────────────────────
  Bitta funksiya                    Butun so'rov yo'li
  Mock bilan                        Haqiqiy baza bilan

  "service.create() to'g'ri         "POST /users ga yuborilsa
  ishlayaptimi?"                     baza da yozuv paydo bo'ladimi?"

  Haqiqiy baza shart emas           Haqiqiy baza shart

  Mock → tez (ms)                   Haqiqiy baza → sekinroq (s)

  Kod ichki logikasini              Butun tizim birgalikda
  tekshiradi                        to'g'ri ishlashini tekshiradi
  E2E test so'rov yo'li:

  Test → HTTP so'rov → Controller → Guard → Pipe
      → Service → Repository → PostgreSQL
      ← JSON javob ← Controller ← Service ← Repository ←
  Test ← natijani tekshiradi

Hamma qatlamlar haqiqiy — mock yo'q. Shuning uchun E2E test "integratsion" ham deyiladi.


Muammodan boshlaylik

Unit testlar o'tdi. Hammasi yashil. Lekin production ga chiqdi — ishlayotgan emas.

  ❌ Nima bo'lishi mumkin:

  Unit test:  UsersService.create() ✓
              UserRepository.save() ✓  (mock)
              EmailService.send()   ✓  (mock)

  Production: POST /users → 500 Internal Server Error

  Sabab:
  - ValidationPipe o'rnatilmagan → DTO tekshirilmaydi
  - Guard noto'g'ri sozlangan → hamma so'rov bloklanyapti
  - Database migration o'tmagan → jadval yo'q
  - ConfigModule noto'g'ri → env o'zgaruvchi topilmaydi

Unit testlar bu xatolarni topa olmaydi — chunki hamma narsa mock. E2E test haqiqiy serverni ishga tushiradi, haqiqiy so'rov yuboradi — shuning uchun bu xatolarni topadi.


O'rnatish

npm install --save-dev supertest @types/supertest

supertest — HTTP so'rovlarni test ichida yuborish uchun. Brauzer yoki Postman shart emas.

NestJS da test/ papkasida app.e2e-spec.ts fayli allaqachon bor:

  test/
  ├── app.e2e-spec.ts     ← NestJS yaratgan default test
  └── jest-e2e.json       ← E2E uchun Jest konfiguratsiyasi
// test/jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts
quot;, "transform": { "^.+\\.(t|j)s
quot;: "ts-jest" } }
# E2E testlarni ishga tushirish
npm run test:e2e

Test bazasi — muhim qadam

E2E test haqiqiy bazaga yozadi. Production yoki development bazasiga yozmaslik uchun alohida test bazasi kerak:

# .env.test
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=12345
DB_NAME=myapp_test     ← alohida baza nomi
NODE_ENV=test
JWT_SECRET=test_secret_key
# PostgreSQL da test bazasini yarating
createdb myapp_test

# Yoki psql da
psql -U postgres -c "CREATE DATABASE myapp_test;"

Test ishga tushishidan oldin baza tozalanib, qayta sozlanishi kerak:

// test/setup.ts
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';

dotenv.config({ path: '.env.test' });

export async function cleanDatabase(dataSource: DataSource) {
  const entities = dataSource.entityMetadatas;

  // Barcha jadvallarni tozalash (teskari tartibda — FK constraint uchun)
  for (const entity of entities.reverse()) {
    const repo = dataSource.getRepository(entity.name);
    await repo.query(`TRUNCATE TABLE "${entity.tableName}" RESTART IDENTITY CASCADE`);
    // RESTART IDENTITY — id lar 1 dan boshlansin
    // CASCADE — bog'liq jadvallar ham tozalansin
  }
}

Birinchi E2E test fayli

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { DataSource } from 'typeorm';
import { cleanDatabase } from './setup';

describe('Users E2E', () => {
  let app: INestApplication;
  let dataSource: DataSource;
  let authToken: string;   // Keyin kerak bo'ladi

  // ─── Bir marta — barcha testlar oldidan ──────────
  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
      // Haqiqiy AppModule — mock yo'q
    }).compile();

    app = moduleFixture.createNestApplication();

    // Production da qanday sozlangan bo'lsa — xuddi shunday
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,
      transform: true,
    }));

    await app.init();

    dataSource = moduleFixture.get(DataSource);
  });

  // ─── Har test oldidan — baza tozalanadi ──────────
  beforeEach(async () => {
    await cleanDatabase(dataSource);
  });

  // ─── Barcha testlar keyin — server yopiladi ──────
  afterAll(async () => {
    await app.close();
  });

POST /users — yaratish testi

  describe('POST /users', () => {

    const validDto = {
      name:     'Ali Valiyev',
      email:    'ali@test.com',
      password: 'Secret123!',
    };

    it('201 — yangi foydalanuvchi yaratadi', async () => {
      const response = await request(app.getHttpServer())
        .post('/users')
        .send(validDto)
        .expect(201);
        // .expect(201) — HTTP status kodi tekshiriladi

      // Javob tuzilmasi tekshiriladi
      expect(response.body).toMatchObject({
        id:    expect.any(Number),
        name:  validDto.name,
        email: validDto.email,
        role:  'user',
      });
      // toMatchObject — faqat ko'rsatilgan maydonlar tekshiriladi
      // boshqa maydonlar (createdAt, updatedAt) muhim emas

      // Parol JAVOBDA bo'lmasligi kerak
      expect(response.body.password).toBeUndefined();
    });

    it('400 — validatsiya xatosi, name yo\'q', async () => {
      const response = await request(app.getHttpServer())
        .post('/users')
        .send({ email: 'ali@test.com', password: 'Secret123!' })
        // name yo'q
        .expect(400);

      expect(response.body.message).toContain('name');
      // Xato xabarida "name" so'zi bo'lishi kerak
    });

    it('400 — noto\'g\'ri email format', async () => {
      await request(app.getHttpServer())
        .post('/users')
        .send({ ...validDto, email: 'notanemail' })
        .expect(400);
    });

    it('400 — parol 8 belgidan kam', async () => {
      await request(app.getHttpServer())
        .post('/users')
        .send({ ...validDto, password: '123' })
        .expect(400);
    });

    it('409 — email allaqachon band', async () => {
      // Birinchi yaratish
      await request(app.getHttpServer())
        .post('/users')
        .send(validDto)
        .expect(201);

      // Ikkinchi marta xuddi shu email
      await request(app.getHttpServer())
        .post('/users')
        .send(validDto)
        .expect(409);
    });

  });

GET /users — ro'yxat testi

  describe('GET /users', () => {

    // Testlar oldidan bir nechta foydalanuvchi yaratamiz
    beforeEach(async () => {
      await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Ali',  email: 'ali@test.com',  password: 'Secret123!' });
      await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Vali', email: 'vali@test.com', password: 'Secret123!' });
      await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Soli', email: 'soli@test.com', password: 'Secret123!' });

      // Login qilib token olamiz
      const loginRes = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' });
      authToken = loginRes.body.accessToken;
    });

    it('200 — token bilan foydalanuvchilar ro\'yxati', async () => {
      const response = await request(app.getHttpServer())
        .get('/users')
        .set('Authorization', `Bearer ${authToken}`)
        // set — header qo'shish
        .expect(200);

      expect(response.body.data).toHaveLength(3);
      expect(response.body.total).toBe(3);
      expect(response.body.page).toBe(1);
    });

    it('401 — token yo\'q', async () => {
      await request(app.getHttpServer())
        .get('/users')
        // Authorization header yo'q
        .expect(401);
    });

    it('200 — sahifalash ishlaydi', async () => {
      const response = await request(app.getHttpServer())
        .get('/users?page=1&limit=2')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.data).toHaveLength(2);
      expect(response.body.total).toBe(3);
      expect(response.body.lastPage).toBe(2);
    });

    it('200 — qidiruv ishlaydi', async () => {
      const response = await request(app.getHttpServer())
        .get('/users?search=ali')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      // "ali" dagi foydalanuvchilar: Ali va Vali
      expect(response.body.data).toHaveLength(2);
    });

  });

GET /users/:id — bitta foydalanuvchi

  describe('GET /users/:id', () => {

    let createdUserId: number;

    beforeEach(async () => {
      const res = await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Ali', email: 'ali@test.com', password: 'Secret123!' });
      createdUserId = res.body.id;

      const loginRes = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' });
      authToken = loginRes.body.accessToken;
    });

    it('200 — mavjud foydalanuvchi qaytadi', async () => {
      const response = await request(app.getHttpServer())
        .get(`/users/${createdUserId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200);

      expect(response.body.id).toBe(createdUserId);
      expect(response.body.name).toBe('Ali');
    });

    it('404 — mavjud bo\'lmagan ID', async () => {
      await request(app.getHttpServer())
        .get('/users/99999')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
    });

    it('400 — ID son emas', async () => {
      await request(app.getHttpServer())
        .get('/users/abc')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(400);
      // ParseIntPipe → 400
    });

  });

PATCH /users/:id — yangilash

  describe('PATCH /users/:id', () => {

    let userId: number;

    beforeEach(async () => {
      const res = await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Ali', email: 'ali@test.com', password: 'Secret123!' });
      userId = res.body.id;

      const loginRes = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' });
      authToken = loginRes.body.accessToken;
    });

    it('200 — nom yangilanadi', async () => {
      const response = await request(app.getHttpServer())
        .patch(`/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Ali Valiyev' })
        .expect(200);

      expect(response.body.name).toBe('Ali Valiyev');
      // Email o'zgarmagan bo'lishi kerak
      expect(response.body.email).toBe('ali@test.com');
    });

    it('200 — faqat kelgan maydonlar yangilanadi', async () => {
      // Faqat name yuborildi
      const response = await request(app.getHttpServer())
        .patch(`/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Yangi Ism' })
        .expect(200);

      // Boshqa maydonlar o'zgarmagan
      expect(response.body.email).toBe('ali@test.com');
      expect(response.body.role).toBe('user');
    });

    it('404 — mavjud bo\'lmagan foydalanuvchi', async () => {
      await request(app.getHttpServer())
        .patch('/users/99999')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Test' })
        .expect(404);
    });

  });

DELETE /users/:id — o'chirish

  describe('DELETE /users/:id', () => {

    let userId: number;

    beforeEach(async () => {
      const res = await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Ali', email: 'ali@test.com', password: 'Secret123!' });
      userId = res.body.id;

      const loginRes = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' });
      authToken = loginRes.body.accessToken;
    });

    it('204 — muvaffaqiyatli o\'chirildi', async () => {
      await request(app.getHttpServer())
        .delete(`/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(204);
      // 204 — No Content, body bo'sh
    });

    it('o\'chirilgandan keyin GET 404 qaytaradi', async () => {
      // O'chiramiz
      await request(app.getHttpServer())
        .delete(`/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(204);

      // Keyin olishga urinib ko'ramiz
      await request(app.getHttpServer())
        .get(`/users/${userId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
      // Soft delete — baza dan o'chirilmagan, lekin ko'rinmaydi
    });

    it('404 — mavjud bo\'lmagan ID', async () => {
      await request(app.getHttpServer())
        .delete('/users/99999')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(404);
    });

  });

}); // describe('Users E2E') yopildi

Auth E2E testi

// test/auth.e2e-spec.ts
describe('Auth E2E', () => {
  // ... setup (yuqoridagidek)

  describe('POST /auth/login', () => {

    beforeEach(async () => {
      await request(app.getHttpServer())
        .post('/users')
        .send({ name: 'Ali', email: 'ali@test.com', password: 'Secret123!' });
    });

    it('200 — to\'g\'ri credentials bilan token oladi', async () => {
      const response = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' })
        .expect(200);

      expect(response.body).toMatchObject({
        accessToken:  expect.any(String),
        refreshToken: expect.any(String),
      });

      // Token format tekshirish (JWT — uchta qism nuqta bilan)
      const parts = response.body.accessToken.split('.');
      expect(parts).toHaveLength(3);
    });

    it('401 — noto\'g\'ri parol', async () => {
      await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'WrongPassword!' })
        .expect(401);
    });

    it('401 — mavjud bo\'lmagan email', async () => {
      await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'notexist@test.com', password: 'Secret123!' })
        .expect(401);
    });

    it('Olingan token bilan himoyalangan endpoint ishlaydi', async () => {
      // Token olamiz
      const loginRes = await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: 'ali@test.com', password: 'Secret123!' });

      const token = loginRes.body.accessToken;

      // Shu token bilan himoyalangan endpoint chaqiramiz
      await request(app.getHttpServer())
        .get('/users')
        .set('Authorization', `Bearer ${token}`)
        .expect(200);
      // Token ishladi
    });

  });

});

Test ishga tushirish buyruqlari

# E2E testlar — alohida config bilan
npm run test:e2e

# Faqat bitta fayl
npm run test:e2e -- users

# Verbose
npm run test:e2e -- --verbose

# Watch mode (E2E da kam ishlatiladi — sekin)
npm run test:e2e -- --watch
  Natija:

  Users E2E
    POST /users
      ✓ 201 — yangi foydalanuvchi yaratadi          (145ms)
      ✓ 400 — validatsiya xatosi, name yo'q          (23ms)
      ✓ 400 — noto'g'ri email format                 (18ms)
      ✓ 400 — parol 8 belgidan kam                   (17ms)
      ✓ 409 — email allaqachon band                  (89ms)
    GET /users
      ✓ 200 — token bilan foydalanuvchilar ro'yxati  (112ms)
      ✓ 401 — token yo'q                             (12ms)
      ✓ 200 — sahifalash ishlaydi                    (98ms)
      ✓ 200 — qidiruv ishlaydi                       (95ms)
    DELETE /users/:id
      ✓ 204 — muvaffaqiyatli o'chirildi              (134ms)
      ✓ o'chirilgandan keyin GET 404 qaytaradi       (201ms)

  Tests: 11 passed   Time: 8.3s

Unit test (ms) vs E2E test (ms-s) — E2E sekinroq, ammo haqiqiy tizimni tekshiradi.


Uch turdagi test birga

Real loyihada uchala tur birgalikda ishlatiladi:

  ┌─────────────────────────────────────────────────────┐
  │                   Test Piramidasi                   │
  │                                                     │
  │                     ▲  E2E                          │
  │                    ▲▲▲ Az, lekin muhim              │
  │                   ▲▲▲▲▲ (10-20 ta)                 │
  │                  ─────────                          │
  │                ▲▲▲▲▲▲▲▲▲ Integration               │
  │               ▲▲▲▲▲▲▲▲▲▲▲ O'rtacha                │
  │              ─────────────────                      │
  │          ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ Unit                 │
  │        ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ Ko'p               │
  │       ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ (100+ ta)        │
  └─────────────────────────────────────────────────────┘

  Unit   → tez, ko'p, logika tekshiradi
  E2E    → sekin, oz, tizimni tekshiradi
// package.json
{
  "scripts": {
    "test":       "jest",
    "test:watch": "jest --watch",
    "test:cov":   "jest --coverage",
    "test:e2e":   "jest --config ./test/jest-e2e.json"
  }
}

CI/CD da testlar

GitHub Actions da har commit da testlar avtomatik ishlaydi:

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB:       myapp_test
          POSTGRES_USER:     postgres
          POSTGRES_PASSWORD: password
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 20

      - run: npm ci

      - name: Unit testlar
        run: npm test

      - name: E2E testlar
        run: npm run test:e2e
        env:
          DB_HOST:     localhost
          DB_PORT:     5432
          DB_NAME:     myapp_test
          DB_USERNAME: postgres
          DB_PASSWORD: password
          JWT_SECRET:  test_secret
  Commit push qilindi
          │
          ▼
  GitHub Actions ishga tushdi
          │
          ├─► Unit testlar  → ✓ 47 passed
          │
          ├─► E2E testlar   → ✓ 23 passed
          │
          ▼
  Merge ruxsat berildi  ← barcha testlar o'tsa
  yoki bloklanadi       ← xato bo'lsa

Vazifalar

1-vazifa — Asosiy: POST /auth/register uchun E2E test yozing:

  • To'g'ri ma'lumotlar bilan 201 qaytaradi
  • Javobda password maydoni yo'q
  • Bir xil email ikki marta 409 qaytaradi
  • Noto'g'ri email format 400 qaytaradi

2-vazifa — Zanjirli test: "Ro'yxatdan o'tish → Login → Profil ko'rish → Chiqish" — to'liq oqimni bitta test da yozing. Har qadam oldingi qadam natijasidan foydalansin.

3-vazifa — O'ylash: E2E test har safar cleanDatabase() chaqiradi. Bu nima uchun muhim? Agar cleanDatabase() chaqirilmasa — testlar o'zaro qanday ta'sir qilishi mumkin?


Xulosa

E2E test nima: Haqiqiy server va baza bilan butun so'rov yo'lini tekshirish.

Unit testdan farqi:

Unit E2E
Baza Mock Haqiqiy (test DB)
Tezlik < 5ms 50-200ms
Nima tekshiriladi Logika Tizim birgalikda
Xatolar Kod xatosi Integratsiya xatosi

supertest — HTTP so'rovlarni test ichida yuborish. .post(), .get(), .set(), .send(), .expect().

Muhim qoidalar:

  • Alohida test bazasi — production yoki dev bazasiga yozmaslik
  • beforeEach da cleanDatabase() — testlar mustaqil bo'lsin
  • beforeAll da server, afterAll da app.close()
  • Muvaffaqiyat va xato holatlari ikkalasi tekshirilsin

Test piramidasi: Ko'p unit test, oz E2E test. Unit — tezkor, E2E — ishonchli.


Bo'lim yakunlandi

7-bo'lim: Dokumentatsiya va Testing to'liq:

  • 52-dars — Swagger: API dokumentatsiya va vizual sinash
  • 53-dars — Jest asoslari: matcher lar, mock, AAA pattern
  • 54-dars — Unit Tests: service va controller alohida test
  • 55-dars — E2E Tests: haqiqiy server va baza bilan

Keyingi bo'lim: 8-bo'lim — Deployment

56-dars: Docker — NestJS ni containerlashtirish