فهم اختبار التكامل
بينما تتحقق اختبارات الوحدة من أن المكونات الفردية تعمل بشكل صحيح بمعزل عن بعضها، تتحقق اختبارات التكامل من أن المكونات المتعددة تعمل معاً بشكل صحيح. يقع اختبار التكامل بين اختبار الوحدة والاختبار الشامل في هرم الاختبار. يختبر التفاعلات بين الوحدات أو الفئات أو الخدمات - مما يضمن أنه عند دمج المكونات، تتكامل بشكل صحيح وتنتج النتائج المتوقعة.
اختبارات التكامل أكثر تعقيداً من اختبارات الوحدة لأنها تتضمن مكونات متعددة وغالباً ما تتفاعل مع الأنظمة الخارجية مثل قواعد البيانات أو أنظمة الملفات أو واجهات برمجة التطبيقات. إنها أبطأ وأكثر هشاشة من اختبارات الوحدة، لكنها تكتشف أنواعاً مختلفة من الأخطاء - لا سيما المشكلات المتعلقة بالواجهات وتدفق البيانات وتكامل النظام. تتضمن مجموعة الاختبار المتوازنة اختبارات وحدة للمكونات الفردية واختبارات تكامل لتفاعلاتها.
خصائص اختبار التكامل:
- متعدد المكونات: يختبر عدة وحدات تعمل معاً
- تبعيات حقيقية: يستخدم قواعد بيانات حقيقية أو APIs أو أنظمة ملفات (أو مزيفات واقعية)
- أبطأ: يستغرق وقتاً أطول من اختبارات الوحدة بسبب عمليات الإدخال/الإخراج
- مستوى أعلى: يختبر سير العمل والميزات، وليس فقط الوظائف الفردية
- قيم: يكتشف أخطاء التكامل التي تفوتها اختبارات الوحدة
اختبارات التكامل مقابل اختبارات الوحدة
مثال اختبار وحدة
// اختبار وحدة - يختبر UserService بمعزل مع تبعيات محاكاة
describe('UserService', () => {
test('ينشئ مستخدماً مع كلمة مرور مُجزّأة', () => {
const mockHasher = {
hash: jest.fn(() => 'hashed_password')
};
const mockRepo = {
save: jest.fn(user => user)
};
const service = new UserService(mockHasher, mockRepo);
const user = service.createUser('john@example.com', 'password123');
expect(mockHasher.hash).toHaveBeenCalledWith('password123');
expect(user.passwordHash).toBe('hashed_password');
});
});
مثال اختبار تكامل
// اختبار تكامل - يختبر UserService مع hasher حقيقي وقاعدة بيانات
describe('UserService Integration', () => {
let db;
let service;
beforeEach(async () => {
db = await setupTestDatabase();
const hasher = new BcryptHasher();
const repo = new UserRepository(db);
service = new UserService(hasher, repo);
});
afterEach(async () => {
await db.close();
});
test('ينشئ مستخدماً ويستمر في قاعدة البيانات', async () => {
const user = await service.createUser('john@example.com', 'password123');
// تحقق من حفظ المستخدم في قاعدة البيانات
const savedUser = await db.query('SELECT * FROM users WHERE id = ?', [user.id]);
expect(savedUser.email).toBe('john@example.com');
expect(savedUser.passwordHash).toBeDefined();
expect(savedUser.passwordHash).not.toBe('password123'); // يجب تجزئة كلمة المرور
});
});
اختبار تكامل قاعدة البيانات
إعداد قاعدة بيانات الاختبار
تحتاج اختبارات التكامل إلى قاعدة بيانات حقيقية أو واقعية. الاستراتيجيات الشائعة تشمل:
// الاستراتيجية 1: قاعدة بيانات في الذاكرة (SQLite)
const setupTestDatabase = async () => {
const db = await sqlite.open({
filename: ':memory:',
driver: sqlite3.Database
});
// تشغيل الترحيلات
await db.migrate();
return db;
};
// الاستراتيجية 2: قاعدة بيانات اختبار منفصلة
const setupTestDatabase = async () => {
const db = await mysql.createConnection({
host: 'localhost',
user: 'test_user',
password: 'test_password',
database: 'test_db'
});
// مسح جميع الجداول قبل الاختبارات
await db.query('SET FOREIGN_KEY_CHECKS=0');
const tables = await db.query('SHOW TABLES');
for (const table of tables) {
await db.query(`TRUNCATE TABLE ${table.name}`);
}
await db.query('SET FOREIGN_KEY_CHECKS=1');
return db;
};
// الاستراتيجية 3: قاعدة بيانات حاوية Docker
const setupTestDatabase = async () => {
const container = await docker.createContainer({
Image: 'postgres:14',
Env: ['POSTGRES_PASSWORD=test'],
ExposedPorts: { '5432/tcp': {} }
});
await container.start();
// انتظر حتى تصبح قاعدة البيانات جاهزة...
// اتصل وأرجع الاتصال
};
أنماط اختبار قاعدة البيانات
describe('User Repository Integration', () => {
let db;
let repo;
beforeAll(async () => {
db = await setupTestDatabase();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
// مسح البيانات قبل كل اختبار
await db.query('DELETE FROM users');
repo = new UserRepository(db);
});
test('يحفظ ويسترجع المستخدم', async () => {
const user = { email: 'john@example.com', name: 'John' };
const savedUser = await repo.save(user);
expect(savedUser.id).toBeDefined();
const foundUser = await repo.findById(savedUser.id);
expect(foundUser).toMatchObject(user);
});
test('يحدث المستخدم الموجود', async () => {
const user = await repo.save({ email: 'john@example.com', name: 'John' });
user.name = 'John Doe';
await repo.save(user);
const updated = await repo.findById(user.id);
expect(updated.name).toBe('John Doe');
});
test('يحذف المستخدم', async () => {
const user = await repo.save({ email: 'john@example.com' });
await repo.delete(user.id);
const found = await repo.findById(user.id);
expect(found).toBeNull();
});
test('يجد المستخدمين بالبريد الإلكتروني', async () => {
await repo.save({ email: 'john@example.com', name: 'John' });
await repo.save({ email: 'jane@example.com', name: 'Jane' });
const users = await repo.findByEmail('john@example.com');
expect(users).toHaveLength(1);
expect(users[0].name).toBe('John');
});
});
أفضل ممارسات اختبار قاعدة البيانات:
- استخدم المعاملات والتراجع عندما يكون ذلك ممكناً للتنظيف السريع
- امسح أو ابذر البيانات قبل كل اختبار للعزل
- استخدم بيانات واقعية تطابق سيناريوهات الإنتاج
- اختبر القيود والفهارس والميزات الخاصة بقاعدة البيانات
- فكر في استخدام تركيبات قاعدة البيانات لسيناريوهات اختبار معقدة
اختبار تكامل API
اختبار نقاط نهاية HTTP
const request = require('supertest');
const app = require('./app'); // Express app
describe('User API', () => {
let server;
beforeAll(() => {
server = app.listen(0); // منفذ عشوائي
});
afterAll((done) => {
server.close(done);
});
test('GET /api/users يعيد جميع المستخدمين', async () => {
const response = await request(server)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toBeInstanceOf(Array);
});
test('POST /api/users ينشئ مستخدماً جديداً', async () => {
const newUser = {
email: 'john@example.com',
name: 'John Doe'
};
const response = await request(server)
.post('/api/users')
.send(newUser)
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toMatchObject(newUser);
expect(response.body.id).toBeDefined();
});
test('GET /api/users/:id يعيد مستخدماً محدداً', async () => {
// إنشاء مستخدم أولاً
const createResponse = await request(server)
.post('/api/users')
.send({ email: 'john@example.com', name: 'John' });
const userId = createResponse.body.id;
// جلب المستخدم
const response = await request(server)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body.id).toBe(userId);
expect(response.body.email).toBe('john@example.com');
});
test('PUT /api/users/:id يحدث المستخدم', async () => {
const user = await createTestUser();
const response = await request(server)
.put(`/api/users/${user.id}`)
.send({ name: 'John Updated' })
.expect(200);
expect(response.body.name).toBe('John Updated');
});
test('DELETE /api/users/:id يحذف المستخدم', async () => {
const user = await createTestUser();
await request(server)
.delete(`/api/users/${user.id}`)
.expect(204);
// تحقق من الحذف
await request(server)
.get(`/api/users/${user.id}`)
.expect(404);
});
test('يعيد 404 لمستخدم غير موجود', async () => {
await request(server)
.get('/api/users/99999')
.expect(404);
});
test('يعيد 400 لبيانات مستخدم غير صالحة', async () => {
const response = await request(server)
.post('/api/users')
.send({ email: 'invalid-email' }) // اسم مفقود، بريد إلكتروني غير صالح
.expect(400);
expect(response.body.errors).toBeDefined();
});
});
اختبار المصادقة والتفويض
describe('مسارات API المحمية', () => {
let authToken;
beforeEach(async () => {
// تسجيل الدخول والحصول على رمز المصادقة
const response = await request(server)
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'password' });
authToken = response.body.token;
});
test('يتطلب المصادقة', async () => {
await request(server)
.get('/api/admin/users')
.expect(401);
});
test('يسمح بالوصول مع رمز صالح', async () => {
await request(server)
.get('/api/admin/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
});
test('يرفض رمزاً غير صالح', async () => {
await request(server)
.get('/api/admin/users')
.set('Authorization', 'Bearer invalid_token')
.expect(401);
});
test('يفرض الوصول المستند إلى الدور', async () => {
// تسجيل الدخول كمستخدم عادي
const userResponse = await request(server)
.post('/api/auth/login')
.send({ email: 'user@example.com', password: 'password' });
// محاولة الوصول إلى مسار المسؤول
await request(server)
.get('/api/admin/users')
.set('Authorization', `Bearer ${userResponse.body.token}`)
.expect(403); // ممنوع
});
});
اختبار تكامل المكونات
اختبار مكونات React مع تبعيات حقيقية
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import { ApiProvider } from './ApiContext';
describe('UserProfile Integration', () => {
test('يجلب ويعرض بيانات المستخدم', async () => {
// إعداد مزود API حقيقي (غير محاكى)
render(
<ApiProvider baseUrl="http://localhost:3000">
<UserProfile userId={1} />
</ApiProvider>
);
// عرض حالة التحميل
expect(screen.getByText('Loading...')).toBeInTheDocument();
// انتظر تحميل البيانات
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
test('يحدث المستخدم عند إرسال النموذج', async () => {
const user = userEvent.setup();
render(
<ApiProvider baseUrl="http://localhost:3000">
<UserProfile userId={1} />
</ApiProvider>
);
// انتظر التحميل الأولي
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// تحرير الاسم
const nameInput = screen.getByLabelText('Name');
await user.clear(nameInput);
await user.type(nameInput, 'Jane Doe');
// إرسال النموذج
await user.click(screen.getByRole('button', { name: 'Save' }));
// تحقق من رسالة النجاح
await waitFor(() => {
expect(screen.getByText('Profile updated successfully')).toBeInTheDocument();
});
// تحقق من الاسم المحدث
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
test('يعرض خطأ عند فشل التحديث', async () => {
const user = userEvent.setup();
render(
<ApiProvider baseUrl="http://localhost:3000">
<UserProfile userId={1} />
</ApiProvider>
);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// إرسال بيانات غير صالحة
const emailInput = screen.getByLabelText('Email');
await user.clear(emailInput);
await user.type(emailInput, 'invalid-email');
await user.click(screen.getByRole('button', { name: 'Save' }));
// تحقق من رسالة الخطأ
await waitFor(() => {
expect(screen.getByText('Invalid email address')).toBeInTheDocument();
});
});
});
اختبار تكامل الخدمة الخارجية
اختبار تكامل API طرف ثالث
describe('Payment Service Integration', () => {
let paymentService;
beforeEach(() => {
// استخدم بيئة sandbox/test
paymentService = new StripePaymentService({
apiKey: process.env.STRIPE_TEST_KEY,
sandbox: true
});
});
test('يعالج الدفع بنجاح', async () => {
const payment = {
amount: 1000, // $10.00
currency: 'usd',
source: 'tok_visa', // رمز اختبار
description: 'Test payment'
};
const result = await paymentService.charge(payment);
expect(result.status).toBe('succeeded');
expect(result.amount).toBe(1000);
});
test('يتعامل مع بطاقة مرفوضة', async () => {
const payment = {
amount: 1000,
currency: 'usd',
source: 'tok_chargeDeclined', // رمز اختبار لبطاقة مرفوضة
description: 'Test payment'
};
await expect(paymentService.charge(payment))
.rejects
.toThrow('Card was declined');
});
test('يتعامل مع أموال غير كافية', async () => {
const payment = {
amount: 1000,
currency: 'usd',
source: 'tok_insufficientFunds',
description: 'Test payment'
};
await expect(paymentService.charge(payment))
.rejects
.toThrow('Insufficient funds');
});
});
اختبار الخدمات الخارجية الحقيقية:
استخدم دائماً بيئات sandbox/test عند اختبار الخدمات الخارجية. لا تستخدم أبداً مفاتيح API الإنتاجية أو تشحن بطاقات حقيقية في الاختبارات. فكر في استخدام اختبار العقد أو مح اكاة الخدمة لاختبارات أكثر موثوقية.
أفضل ممارسات اختبار التكامل
1. اختبر سيناريوهات واقعية
// جيد - يختبر سير عمل مستخدم واقعي
test('سير عمل تسجيل المستخدم الكامل', async () => {
// تسجيل المستخدم
const user = await userService.register({
email: 'john@example.com',
password: 'password123'
});
// تحقق من إرسال البريد الإلكتروني
expect(emailService.getSentEmails()).toContainEqual(
expect.objectContaining({
to: 'john@example.com',
subject: 'Verify your email'
})
);
// تحقق من المستخدم في قاعدة البيانات
const savedUser = await db.query('SELECT * FROM users WHERE id = ?', [user.id]);
expect(savedUser).toBeDefined();
// يجب ألا يكون المستخدم نشطاً بعد
expect(savedUser.isActive).toBe(false);
// تحقق من البريد الإلكتروني
await userService.verifyEmail(user.verificationToken);
// يجب أن يكون المستخدم نشطاً الآن
const verifiedUser = await db.query('SELECT * FROM users WHERE id = ?', [user.id]);
expect(verifiedUser.isActive).toBe(true);
});
2. عزل بيانات الاختبار
// استخدم بيانات فريدة لكل اختبار لتجنب التعارضات
test('ينشئ مستخدماً ببريد إلكتروني فريد', async () => {
const uniqueEmail = `test-${Date.now()}@example.com`;
const user = await userService.register({
email: uniqueEmail,
password: 'password123'
});
expect(user.email).toBe(uniqueEmail);
});
// أو استخدم تركيبات اختبار
const createTestUser = async (overrides = {}) => {
return userService.register({
email: `test-${Math.random()}@example.com`,
name: 'Test User',
password: 'password123',
...overrides
});
};
3. تنظيف بعد الاختبارات
describe('Order Service Integration', () => {
const createdOrders = [];
afterEach(async () => {
// تنظيف جميع الطلبات المنشأة
for (const order of createdOrders) {
await orderService.delete(order.id);
}
createdOrders.length = 0;
});
test('ينشئ طلباً', async () => {
const order = await orderService.create({
userId: 1,
items: [{ productId: 1, quantity: 2 }]
});
createdOrders.push(order);
expect(order.id).toBeDefined();
});
});
تمرين تطبيقي:
- أنشئ نظام مدونة مع نماذج Post وComment وUser
- اكتب اختبارات تكامل لإنشاء منشور مع قاعدة بيانات حقيقية
- اكتب اختبارات تكامل لنقاط نهاية API للتعليقات (إنشاء، قراءة، تحديث، حذف)
- اختبر سير العمل الكامل: تسجيل المستخدم → إنشاء منشور → إضافة تعليقات → حذف منشور (حذف متتالي للتعليقات)
- اكتب اختبارات تكامل للترقيم والتصفية للمنشورات
- تأكد من تنظيف الاختبارات للبيانات بشكل صحيح ويمكن تشغيلها بأي ترتيب
الخلاصة
اختبار التكامل يتحقق من أن المكونات المتعددة تعمل معاً بشكل صحيح. على عكس اختبارات الوحدة التي تعزل المكونات بالمحاكيات، تستخدم اختبارات التكامل تبعيات حقيقية - قواعد بيانات وAPIs وأنظمة ملفات - للتحقق من أن التكاملات تعمل كما هو متوقع. اختبارات التكامل ضرورية لاكتشاف الأخطاء في الواجهات وتدفق البيانات وتفاعلات النظام التي لا تستطيع اختبارات الوحدة اكتشافها.
بينما اختبارات التكامل أبطأ وأكثر تعقيداً من اختبارات الوحدة، فإنها توفر ثقة لا تقدر بثمن أن نظامك يعمل ككل. تتضمن استراتيجية الاختبار المتوازنة كليهما: اختبارات وحدة للحصول على ملاحظات سريعة على المكونات الفردية، واختبارات تكامل للثقة في تكامل النظام. ركز اختبارات التكامل على سير العمل الحرجة وتكاملات الخدمات الخارجية وعمليات قاعدة البيانات حيث من المحتمل أن تفشل تفاعلات المكونات.