Integration Test — database bilan testlash

~20 daq · Express · Testing

Muammodan boshlaylik

Unit testlar o'tdi — funksiyalar to'g'ri ishlaydi. Lekin real loyihada bir muammo chiqdi: foydalanuvchi ro'yxatdan o'tyapti, DB ga yozilmayapti. Unit test hech narsani ko'rsatmadi — chunki DB mock qilingan edi.

Integration test — real DB bilan ishlaydigan test. "Kod va DB birga to'g'ri ishlaydimi?"


Integration test nima?

Unit test: bitta funksiya, DB yo'q. Integration test: bir nechta qism birga — DB ham ishtirok etadi.

Unit test:                Integration test:
─────────────             ─────────────────
funksiya                  HTTP so'rov
   ↓                         ↓
mock DB                   real DB (test)

💡 Integration test sekinroq (DB bilan ishlaydi), lekin unit testda ko'rinmaydigan muammolarni topadi. Netflix, Stripe, GitHub — hammasi ikkalasini ishlatadi.


Test uchun alohida DB

Production DB ni testda ishlatmang — ma'lumotlar buziladi.

Eng oddiy yondashuv — SQLite (xotirada):

npm install --save-dev better-sqlite3

Yoki PostgreSQL bilan test uchun alohida DB:

DATABASE_URL=postgres://localhost/myapp_test

.env.test faylda:

DATABASE_URL=postgres://localhost/myapp_test
NODE_ENV=test

package.json:

{
  "scripts": {
    "test": "NODE_ENV=test jest"
  }
}

Sequelize bilan misol

// db.js
import { Sequelize } from 'sequelize';

const isTest = process.env.NODE_ENV === 'test';

export const sequelize = new Sequelize(
  isTest
    ? 'sqlite::memory:'          // ← test — xotirada
    : process.env.DATABASE_URL,  // ← production — real DB
  { logging: false }
);
// models/User.js
import { DataTypes } from 'sequelize';
import { sequelize } from '../db.js';

export const User = sequelize.define('User', {
  ism:   { type: DataTypes.STRING, allowNull: false },
  email: { type: DataTypes.STRING, unique: true },
  parol: { type: DataTypes.STRING },
});

Integration test yozish

// tests/user.integration.test.js
import { sequelize } from '../db.js';
import { User } from '../models/User.js';
import { foydalanuvchiYarat } from '../services/userService.js';

// Har testdan oldin — jadval yaratish va tozalash
beforeEach(async () => {
  await sequelize.sync({ force: true }); // ← jadvalni qayta yaratish
});

// Barcha testlar tugagach — ulanishni yopish
afterAll(async () => {
  await sequelize.close();
});

describe('foydalanuvchiYarat', () => {
  test('DB ga yozadi', async () => {
    await foydalanuvchiYarat({
      ism: 'Ali',
      email: 'ali@test.com',
      parol: 'Parol123',
    });

    const user = await User.findOne({ where: { email: 'ali@test.com' } });
    expect(user).not.toBeNull();
    expect(user.ism).toBe('Ali');
  });

  test('parolni hash qiladi', async () => {
    await foydalanuvchiYarat({
      ism: 'Ali',
      email: 'ali@test.com',
      parol: 'Parol123',
    });

    const user = await User.findOne({ where: { email: 'ali@test.com' } });
    expect(user.parol).not.toBe('Parol123'); // ← ochiq parol saqlanmagan
  });

  test('bir email ikki marta — xato', async () => {
    await foydalanuvchiYarat({ ism: 'Ali', email: 'ali@test.com', parol: 'Parol123' });

    await expect(
      foydalanuvchiYarat({ ism: 'Vali', email: 'ali@test.com', parol: 'Parol456' })
    ).rejects.toThrow('Bu email band');
  });
});

Supertest + DB

HTTP endpointdan DB gacha to'liq zanjir:

// tests/auth.integration.test.js
import request from 'supertest';
import app from '../app.js';
import { sequelize } from '../db.js';
import { User } from '../models/User.js';
import bcrypt from 'bcrypt';

beforeEach(async () => {
  await sequelize.sync({ force: true });
});

afterAll(async () => {
  await sequelize.close();
});

describe('POST /auth/register', () => {
  test('foydalanuvchi yaratadi', async () => {
    const res = await request(app)
      .post('/auth/register')
      .send({ ism: 'Ali', email: 'ali@test.com', parol: 'Parol123' });

    expect(res.status).toBe(201);

    // DB da saqlanganini tekshirish
    const user = await User.findOne({ where: { email: 'ali@test.com' } });
    expect(user).not.toBeNull();
  });

  test('ikki marta ro\'yxatdan o\'tish — 409', async () => {
    await request(app)
      .post('/auth/register')
      .send({ ism: 'Ali', email: 'ali@test.com', parol: 'Parol123' });

    const res = await request(app)
      .post('/auth/register')
      .send({ ism: 'Ali', email: 'ali@test.com', parol: 'Parol123' });

    expect(res.status).toBe(409);
  });
});

describe('POST /auth/login', () => {
  beforeEach(async () => {
    // Test foydalanuvchisi
    await User.create({
      ism: 'Ali',
      email: 'ali@test.com',
      parol: await bcrypt.hash('Parol123', 10),
    });
  });

  test('to\'g\'ri ma\'lumotlar — token', async () => {
    const res = await request(app)
      .post('/auth/login')
      .send({ email: 'ali@test.com', parol: 'Parol123' });

    expect(res.status).toBe(200);
    expect(res.body.token).toBeDefined();
  });

  test('noto\'g\'ri parol — 401', async () => {
    const res = await request(app)
      .post('/auth/login')
      .send({ email: 'ali@test.com', parol: 'xato' });

    expect(res.status).toBe(401);
  });
});

beforeAll va beforeEach farqi

// beforeAll — bir marta, barcha testlardan oldin
beforeAll(async () => {
  await sequelize.sync({ force: true }); // ← bir marta yaratish
});

// beforeEach — har testdan oldin
beforeEach(async () => {
  await User.destroy({ where: {} }); // ← har safar tozalash
});

// afterAll — bir marta, barcha testlardan keyin
afterAll(async () => {
  await sequelize.close();
});

💡 force: true hamma jadvalni o'chirib qayta yaratadi. Tez, lekin katta loyihada sekin bo'lishi mumkin. O'sha holda truncate ishlatiladi — jadval tuzilmasi saqlanadi, faqat ma'lumotlar o'chadi.


Tez-tez uchraydigan xatolar

1. afterAll da DB ulanishini yopmaslik

Jest did not exit one second after the test run has completed.
// ✅
afterAll(async () => {
  await sequelize.close(); // ← yoping
});

2. Testlar orasida ma'lumot qolishi

// ❌ — birinchi test yaratgan ma'lumot ikkinchiga ta'sir qiladi
test('birinchi', async () => {
  await User.create({ email: 'test@test.com' });
});
test('ikkinchi', async () => {
  const count = await User.count();
  expect(count).toBe(0); // ← 1 bo'ladi!
});

// ✅ — beforeEach bilan tozalash
beforeEach(async () => {
  await User.destroy({ where: {} });
});

3. Production DB ga ulanib qolish

// ✅ — environment tekshirilsin
if (process.env.NODE_ENV === 'production') {
  throw new Error('Testni production da ishlatmang!');
}

Xulosa

  • Integration test — real DB bilan, qismlar birga ishlaydi
  • Test uchun alohida DB yoki SQLite xotirada — production ga tegmaydi
  • beforeEach — har testdan oldin ma'lumotlarni tozalang
  • afterAll — testlar tugagach DB ulanishini yoping
  • Unit test tez, integration test ishonchli — ikkalasi kerak

Keyingi dars: 54-dars: Mock — funksiya, modul, database mock qilish