Node.js و Express
اختبار واجهات Express APIs
اختبار واجهات Express APIs
اختبار نقاط نهاية API الخاصة بـ Express أمر ضروري لضمان عمل تطبيقك بشكل صحيح. في هذا الدرس، سنستخدم Supertest، وهي مكتبة مصممة خصيصًا لاختبار واجهات HTTP APIs مع Express.
إعداد Supertest
يعمل Supertest بسلاسة مع Jest ويسمح لك بإجراء طلبات HTTP إلى تطبيق Express الخاص بك:
npm install --save-dev supertest
أولاً، دعنا نهيكل تطبيق Express لجعله قابلاً للاختبار. افصل إنشاء التطبيق عن بدء الخادم:
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
module.exports = app;
// server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
نصيحة: فصل إنشاء التطبيق عن بدء الخادم يسمح لك باختبار تطبيقك دون بدء خادم فعلي على منفذ.
اختبار نقاط نهاية GET
لنقم بإنشاء API بسيط واختباره:
// app.js (تابع)
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// __tests__/users.test.js
const request = require('supertest');
const app = require('../app');
describe('GET /api/users', () => {
test('يجب أن يعيد جميع المستخدمين', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name', 'Alice');
});
});
describe('GET /api/users/:id', () => {
test('يجب أن يعيد مستخدم محدد', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toMatchObject({
id: 1,
name: 'Alice',
email: 'alice@example.com'
});
});
test('يجب أن يعيد 404 لمستخدم غير موجود', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body).toHaveProperty('error', 'User not found');
});
});
اختبار نقاط نهاية POST
الآن دعنا نختبر إنشاء الموارد:
// app.js (تابع)
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// التحقق
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
if (!email.includes('@')) {
return res.status(400).json({ error: 'Invalid email format' });
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// __tests__/users.test.js (تابع)
describe('POST /api/users', () => {
test('يجب أن ينشئ مستخدم جديد', async () => {
const newUser = {
name: 'Charlie',
email: 'charlie@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
test('يجب أن يرفض بيانات مستخدم غير صالحة', async () => {
const invalidUser = { name: 'Charlie' }; // البريد الإلكتروني مفقود
await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
});
test('يجب أن يرفض تنسيق بريد إلكتروني غير صالح', async () => {
const invalidUser = {
name: 'Charlie',
email: 'not-an-email'
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
expect(response.body.error).toContain('Invalid email');
});
});
اختبار نقاط نهاية PUT و DELETE
// app.js (تابع)
app.put('/api/users/:id', (req, res) => {
const { name, email } = req.body;
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
if (name) users[userIndex].name = name;
if (email) users[userIndex].email = email;
res.json(users[userIndex]);
});
app.delete('/api/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
// __tests__/users.test.js (تابع)
describe('PUT /api/users/:id', () => {
test('يجب أن يحدث مستخدم', async () => {
const updates = { name: 'Alice Updated' };
const response = await request(app)
.put('/api/users/1')
.send(updates)
.expect(200);
expect(response.body.name).toBe('Alice Updated');
});
test('يجب أن يعيد 404 لمستخدم غير موجود', async () => {
await request(app)
.put('/api/users/999')
.send({ name: 'Test' })
.expect(404);
});
});
describe('DELETE /api/users/:id', () => {
test('يجب أن يحذف مستخدم', async () => {
await request(app)
.delete('/api/users/1')
.expect(204);
});
test('يجب أن يعيد 404 لمستخدم غير موجود', async () => {
await request(app)
.delete('/api/users/999')
.expect(404);
});
});
اختبار الوسيطة (Middleware)
الوسيطة جزء حاسم من تطبيقات Express. دعنا نختبر وسيطة المصادقة:
// middleware/auth.js
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
if (token !== 'Bearer valid-token') {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = { id: 1, name: 'Authenticated User' };
next();
}
module.exports = authMiddleware;
// app.js (استخدام الوسيطة)
const authMiddleware = require('./middleware/auth');
app.get('/api/profile', authMiddleware, (req, res) => {
res.json(req.user);
});
// __tests__/auth.test.js
const request = require('supertest');
const app = require('../app');
describe('وسيطة المصادقة', () => {
test('يجب أن يرفض الطلبات بدون رمز', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
test('يجب أن يرفض الطلبات برمز غير صالح', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
test('يجب أن يسمح بالطلبات برمز صالح', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer valid-token')
.expect(200);
expect(response.body).toHaveProperty('name', 'Authenticated User');
});
});
تحذير: لا تختبر أبدًا برموز مصادقة أو بيانات اعتماد حقيقية. استخدم دائمًا بيانات وهمية في الاختبارات.
اختبار قاعدة البيانات مع تركيبات الاختبار
عند الاختبار مع قاعدة بيانات، استخدم تركيبات الاختبار والإعداد/التنظيف المناسب:
// __tests__/database.test.js
const db = require('../database');
const request = require('supertest');
const app = require('../app');
describe('User API مع قاعدة البيانات', () => {
beforeAll(async () => {
await db.connect();
});
beforeEach(async () => {
// مسح قاعدة البيانات
await db.query('DELETE FROM users');
// ملء بيانات الاختبار
await db.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
['Test User', 'test@example.com']
);
});
afterAll(async () => {
await db.disconnect();
});
test('يجب أن يجلب المستخدمين من قاعدة البيانات', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body).toHaveLength(1);
expect(response.body[0].email).toBe('test@example.com');
});
test('يجب أن ينشئ مستخدم في قاعدة البيانات', async () => {
const newUser = {
name: 'New User',
email: 'new@example.com'
};
await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
const users = await db.query('SELECT * FROM users');
expect(users).toHaveLength(2);
});
});
ملاحظة: استخدم قاعدة بيانات اختبار منفصلة أو قاعدة بيانات في الذاكرة (مثل SQLite :memory:) للاختبار لتجنب التأثير على بيانات الإنتاج.
الاختبار مع متغيرات البيئة
// __tests__/setup.js
process.env.NODE_ENV = 'test';
process.env.DB_NAME = 'test_database';
process.env.JWT_SECRET = 'test-secret';
// jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['/__tests__/setup.js'],
coveragePathIgnorePatterns: ['/node_modules/']
};
تمرين تطبيقي:
أنشئ blog API مع نقاط النهاية التالية:
- GET /api/posts - قائمة جميع المنشورات
- GET /api/posts/:id - الحصول على منشور واحد
- POST /api/posts - إنشاء منشور (يتطلب مصادقة)
- PUT /api/posts/:id - تحديث منشور (يتطلب مصادقة وملكية)
- DELETE /api/posts/:id - حذف منشور (يتطلب مصادقة وملكية)
اكتب اختبارات شاملة لجميع نقاط النهاية، بما في ذلك المصادقة والترخيص والتحقق وحالات الخطأ. استهدف تغطية كود لا تقل عن 90٪.
الخلاصة
في هذا الدرس، تعلمت:
- إعداد Supertest لاختبار API
- اختبار نقاط نهاية GET و POST و PUT و DELETE
- اختبار الوسيطة والمصادقة
- اختبار قاعدة البيانات مع التركيبات
- الإعداد والتنظيف لبيئات الاختبار
- أفضل الممارسات لاختبار API