E2E Tests — NestJS + Jest
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.tsquot;,
"transform": { "^.+\\.(t|j)squot;: "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
passwordmaydoni 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
beforeEachdacleanDatabase()— testlar mustaqil bo'lsinbeforeAllda server,afterAlldaapp.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