Node.js: الاختبار وتصحيح الأخطاء
آخر تحديث: 2026-08-26
كتب بوب واجهة برمجة تطبيقات RESTful لمنصة تجارة إلكترونية، وفي كل مرة يضيف فيها ميزة جديدة، كان شيء آخر يتعطل. كان الاختبار اليدوي باستخدام Postman يستغرق 30 دقيقة لكل عملية نشر، ومع ذلك كان لا يزال يفوت الحالات الحدية. فأدخل Jest لاختبارات الوحدة وSupertest لاختبارات تكامل واجهة برمجة التطبيقات — والآن يقوم npm test بتشغيل 50 اختبارًا في 3 ثوانٍ، وكل عملية نشر مدعومة بفحص CI آلي. كما أضاف winston للتسجيل المنظم، بحيث عندما يتسلل خطأ ما، تخبره السجلات بالضبط بما حدث من خطأ.
سوف تتعلم:
- اختبارات الوحدة باستخدام Jest (describe / it / expect / mock)
- اختبارات تكامل واجهة برمجة التطبيقات باستخدام Supertest (محاكاة طلبات HTTP)
- مصحح الأخطاء المدمج في Node.js (node inspect / Chrome DevTools)
- التسجيل المنظم باستخدام winston (transports / format / rotation)
- تغطية الاختبارات وسير عمل TDD
1. نظرة عامة على الاختبار
(1) لماذا نكتب الاختبارات
| بدون اختبارات | مع الاختبارات |
|---|---|
| تحقق يدوي، 30 دقيقة في كل مرة | آلي، 3 ثوانٍ |
| تغيير شيء واحد يكسر كل شيء | تغيير شيء واحد، تخبرك الاختبارات بما تعطل |
| قلق قبل النشر | إشارات خضراء، انشر بثقة |
| خوف من إعادة الهيكلة | إعادة الهيكلة مع شبكة أمان الاختبارات |
▶ مثال: (2) هرم الاختبارات
graph TD
A["Unit Tests<br/>Many / Fast / Cheap"] --> B["Integration Tests<br/>Moderate / Slower"]
B --> C["E2E Tests<br/>Few / Slow / Expensive"]
style A fill:#4CAF50,color:#fff
style B fill:#FF9800,color:#fff
style C fill:#F44336,color:#fff
- اختبارات الوحدة: تختبر الدوال/الوحدات الفردية، سريعة، وأكبر عددًا
- اختبارات التكامل: تختبر التعاون بين الوحدات (مثل API + قاعدة البيانات)، بسرعة متوسطة
- اختبارات E2E: تحاكي عمليات المستخدم، الأبطأ والأقل عددًا
(3) مقارنة أدوات الاختبار في Node.js
| الأداة | النوع | الميزات | الأنسب لـ |
|---|---|---|---|
| Jest | وحدة + تكامل | تكوين صفري، mock/تغطية مدمجة، اختبار اللقطة | الاستخدام العام |
| Mocha | وحدة + تكامل | مرن، إضافات غنية، يحتاج chai/sinon | الاحتياجات المخصصة |
| Vitest | وحدة | نظام Vite البيئي، ESM أصلي، سريع جدًا | مشاريع Vite |
| node:test | وحدة | مدمج في Node.js، بدون تبعيات | المشاريع البسيطة |
| Supertest | اختبار HTTP | محاكاة طلبات HTTP، اختبار مسارات Express | اختبارات تكامل API |
| Playwright | E2E | أتمتة المتصفح، دعم متعدد المتصفحات | E2E للواجهة الأمامية |
يستخدم هذا الدرس مزيج Jest + Supertest، وهو حل الاختبار الأكثر انتشارًا في مجتمع Node.js.
2. اختبارات الوحدة باستخدام Jest
▶ مثال: (1) التثبيت والإعداد
npm install --save-dev jest
أضف سكربت الاختبار إلى package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
يعمل Jest بدون تكوين افتراضيًا، حيث يكتشف تلقائيًا ملفات *.test.js أو *.spec.js.
▶ مثال: (2) الصيغة الأساسية: describe / it / expect
// math.js — الوحدة المطلوب اختبارها
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, subtract, multiply, divide };
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math utilities', () => {
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('subtract should return the difference', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiply should return the product', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divide should throw on zero divisor', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
تشغيل:
npm test
Output:
PASS ./math.test.js
Math utilities
✓ add should sum two numbers (2 ms)
✓ subtract should return the difference
✓ multiply should return the product
✓ divide should throw on zero divisor (1 ms)
Tests: 4 passed, 4 total
Time: 0.5s
(3) المطابِقات الشائعة
| المطابِق | المعنى | مثال |
|---|---|---|
toBe |
المساواة الصارمة (===) | expect(1 + 1).toBe(2) |
toEqual |
المساواة العميقة | expect(obj).toEqual({ a: 1 }) |
toBeTruthy / toBeFalsy |
فحص منطقي | expect(flag).toBeTruthy() |
toBeNull / toBeUndefined |
null / undefined | expect(val).toBeNull() |
toThrow |
يرمي استثناءً | expect(fn).toThrow() |
toContain |
يحتوي المصفوفة على | expect([1, 2, 3]).toContain(2) |
toMatch |
مطابقة تعبير نمطي | expect('hello').toMatch(/ell/) |
toHaveLength |
فحص الطول | expect('abc').toHaveLength(3) |
resolves / rejects |
نتيجة الوعد (Promise) | expect(promise).resolves.toBe(5) |
toHaveBeenCalled |
تم استدعاء الـ mock | expect(fn).toHaveBeenCalled() |
▶ مثال: (4) الاختبار غير المتزامن
// async-functions.js
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `User_${id}` });
}, 100);
});
}
async function getUserEmail(id) {
const user = await fetchUser(id);
return `${user.name.toLowerCase()}@example.com`;
}
module.exports = { fetchUser, getUserEmail };
// async-functions.test.js
const { fetchUser, getUserEmail } = require('./async-functions');
describe('Async function tests', () => {
test('fetchUser returns user object', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'User_1' });
});
test('getUserEmail returns formatted email', async () => {
const email = await getUserEmail(42);
expect(email).toBe('user_42@example.com');
});
test('fetchUser resolves within 200ms', async () => {
await expect(fetchUser(1)).resolves.toHaveProperty('id', 1);
});
});
(5) المحاكاة (Mocking)
تعزل الـ mocks التبعيات الخارجية (قواعد البيانات، طلبات HTTP، نظام الملفات) بحيث تركز الاختبارات على منطق الوحدة الحالية فقط.
// email-service.js
class EmailService {
send(to, subject, body) {
// استدعاء SMTP فعلي — بطيء جدًا لاختبارات الوحدة
console.log(`Sending email to ${to}: ${subject}`);
return true;
}
}
class UserService {
constructor(emailService) {
this.emailService = emailService;
}
register(name, email) {
// منطق الأعمال: التحقق + إرسال بريد الترحيب
if (!email.includes('@')) throw new Error('Invalid email');
this.emailService.send(email, 'Welcome', `Hello ${name}!`);
return { name, email, status: 'registered' };
}
}
module.exports = { EmailService, UserService };
// email-service.test.js
const { EmailService, UserService } = require('./email-service');
describe('UserService', () => {
let mockEmailService;
let userService;
beforeEach(() => {
mockEmailService = {
send: jest.fn().mockReturnValue(true),
};
userService = new UserService(mockEmailService);
});
test('register should call emailService.send', () => {
const result = userService.register('Alice', 'alice@example.com');
expect(result).toEqual({
name: 'Alice',
email: 'alice@example.com',
status: 'registered',
});
expect(mockEmailService.send).toHaveBeenCalledWith(
'alice@example.com',
'Welcome',
'Hello Alice!'
);
});
test('register should throw on invalid email', () => {
expect(() => userService.register('Bob', 'invalid')).toThrow('Invalid email');
expect(mockEmailService.send).not.toHaveBeenCalled();
});
test('register should send exactly one email', () => {
userService.register('Charlie', 'charlie@example.com');
expect(mockEmailService.send).toHaveBeenCalledTimes(1);
});
});
▶ مثال: التأكيدات الأساسية مع Jest
test('basic matchers', () => {
expect(2 + 2).toBe(4);
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect([1, 2, 3]).toContain(2);
expect('hello world').toMatch(/world/);
expect(() => { throw new Error('fail'); }).toThrow('fail');
});
يعرض مطابِقات Jest الأكثر استخدامًا: toBe للأنواع الأولية، وtoEqual للكائنات، وtoContain للمصفوفات، وtoMatch للسلاسل، وtoThrow لاختبار الأخطاء.
3. اختبارات تكامل واجهة برمجة التطبيقات باستخدام Supertest
▶ مثال: (1) التثبيت والمبدأ
npm install --save-dev supertest
يشغّل Supertest تطبيق Express في الذاكرة، ويرسل طلبات HTTP، ويتحقق من الاستجابات — دون الحاجة إلى الاستماع على منفذ حقيقي.
flowchart LR
A["Test File"] -->|"request(app)"| B["Express App<br/>(in-memory)"]
B -->|"response"| A
A -->|"assert"| C["Jest expect"]
▶ مثال: (2) اختبار واجهة برمجة تطبيقات Express
// app.js — تطبيق Express (تصدير بدون listen)
const express = require('express');
const app = express();
app.use(express.json());
let products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 499.99 },
];
app.get('/api/products', (req, res) => {
res.json(products);
});
app.get('/api/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ error: 'Product not found' });
res.json(product);
});
app.post('/api/products', (req, res) => {
const { name, price } = req.body;
if (!name || price == null) {
return res.status(400).json({ error: 'Name and price are required' });
}
const newProduct = { id: products.length + 1, name, price };
products.push(newProduct);
res.status(201).json(newProduct);
});
module.exports = app;
ملاحظة: يصدّر
app.jsكائنappفقط، ولا يستدعيapp.listen()داخل الوحدة. يتم وضعlistenفيserver.js، بحيث يستطيع Supertest اختبار كائنappمباشرة.
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('Products API', () => {
test('GET /api/products should return product list', async () => {
const response = await request(app).get('/api/products');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name', 'Laptop');
});
test('GET /api/products/:id should return a single product', async () => {
const response = await request(app).get('/api/products/1');
expect(response.status).toBe(200);
expect(response.body.id).toBe(1);
});
test('GET /api/products/:id should return 404 for missing product', async () => {
const response = await request(app).get('/api/products/999');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error', 'Product not found');
});
test('POST /api/products should create a new product', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Tablet', price: 299.99 });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', 'Tablet');
expect(response.body).toHaveProperty('id', 3);
});
test('POST /api/products should return 400 for missing fields', async () => {
const response = await request(app)
.post('/api/products')
.send({ name: 'Incomplete Product' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
▶ مثال: (3) اختبار واجهة برمجة تطبيقات محمية
// protected-routes.test.js
const request = require('supertest');
const app = require('./app');
describe('Protected API routes', () => {
let token;
beforeAll(async () => {
// تسجيل الدخول للحصول على رمز JWT
const response = await request(app)
.post('/api/auth/login')
.send({ username: 'alice', password: 'secret123' });
token = response.body.token;
});
test('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('username', 'alice');
});
test('should reject access without token', async () => {
const response = await request(app).get('/api/profile');
expect(response.status).toBe(401);
});
test('should reject access with invalid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid_token_here');
expect(response.status).toBe(401);
});
});
▶ مثال: اختبار فحص سلامة واجهة برمجة التطبيقات
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/health', (req, res) => res.json({ status: 'ok' }));
describe('GET /health', () => {
it('returns 200 with status ok', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
});
});
اختبار فحص سلامة بسيط لواجهة برمجة التطبيقات باستخدام Supertest — ينشئ تطبيق Express أساسيًا، ويرسل طلب GET، ويتحقق من نص استجابة JSON ورمز الحالة.
4. تصحيح الأخطاء في Node.js
(1) node inspect + Chrome DevTools
يحتوي Node.js على مصحح أخطاء مدمج — لا حاجة إلى أدوات إضافية.
node inspect app.js
ثم افتح chrome://inspect في Chrome وانقر فوق "Open dedicated DevTools for Node".
عمليات تصحيح الأخطاء الشائعة:
| الإجراء | اختصار DevTools | سطر الأوامر |
|---|---|---|
| متابعة | F8 | c |
| التخطي | F10 | n |
| الدخول | F11 | s |
| الخروج | Shift+F11 | o |
| تعيين نقطة توقف | النقر على رقم السطر | setBreakpoint() |
▶ مثال: (2) تعيين نقاط التوقف في الكود
// debug-demo.js
function calculateDiscount(price, memberLevel) {
debugger; // يتوقف التنفيذ هنا في المفتش
let discount = 0;
if (memberLevel === 'gold') discount = 0.2;
else if (memberLevel === 'silver') discount = 0.1;
const finalPrice = price * (1 - discount);
return finalPrice;
}
console.log(calculateDiscount(100, 'gold'));
console.log(calculateDiscount(100, 'silver'));
console.log(calculateDiscount(100, 'unknown'));
node inspect debug-demo.js
▶ مثال: (3) نصائح لتصحيح الأخطاء باستخدام console
// طرق تصحيح الأخطاء باستخدام console
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'admin' },
];
// عرض جدولي
console.table(users);
// قياس وقت التنفيذ
console.time('database-query');
// ... منطق الاستعلام ...
console.timeEnd('database-query');
// تتبع المكدس
console.trace('Where is this called from?');
// مخرجات مجمعة
console.group('User Details');
console.log('Name: Alice');
console.log('Role: admin');
console.groupEnd();
5. التسجيل المنظم باستخدام winston
(1) لماذا نستخدم winston بدلاً من console.log
| console.log | winston |
|---|---|
| لا توجد مستويات سجل | يدعم debug/info/warn/error |
| لا يوجد إخراج إلى ملف | يدعم Console + File + HTTP وغيرها من الـ transports |
| لا توجد تهيئة | يدعم JSON / الطابع الزمني / تنسيقات مخصصة |
| لا توجد دورة للسجلات | يقترن مع winston-daily-rotate-file للدورة التلقائية |
| لا يُنصح به للإنتاج | خيار بمستوى الإنتاج |
▶ مثال: (2) التثبيت والإعداد الأساسي
npm install winston
npm install winston-daily-rotate-file
// logger.js
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'my-app' },
transports: [
// سجلات الأخطاء — ملف منفصل
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
}),
// جميع السجلات — ملف مدمج
new DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// التطوير: السجل أيضًا إلى وحدة التحكم مع مخرجات ملونة
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
module.exports = logger;
▶ مثال: (3) الاستخدام في تطبيق Express
// app-with-logger.js
const express = require('express');
const logger = require('./logger');
const app = express();
app.use(express.json());
// برنامج وسيط لتسجيل الطلبات
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
});
app.get('/api/health', (req, res) => {
logger.debug('Health check requested');
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('User lookup', { userId });
if (isNaN(userId)) {
logger.warn('Invalid user ID format', { userId });
return res.status(400).json({ error: 'Invalid user ID' });
}
// محاكاة البحث عن مستخدم
const user = { id: parseInt(userId), name: 'Alice' };
logger.info('User found', { userId: user.id, name: user.name });
res.json(user);
});
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
مثال على إخراج ملف السجل:
{"level":"info","message":"HTTP request","service":"my-app","timestamp":"2026-07-13 10:30:00","method":"GET","url":"/api/users/1","status":200,"duration":"15ms"}
{"level":"warn","message":"Invalid user ID format","service":"my-app","timestamp":"2026-07-13 10:30:05","userId":"abc"}
{"level":"error","message":"Unhandled error","service":"my-app","timestamp":"2026-07-13 10:31:00","error":"Cannot read property 'name' of undefined","stack":"TypeError: Cannot read property..."}
▶ مثال: التسجيل المنظم باستخدام winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
logger.info('Server started', { port: 3000, env: 'development' });
logger.warn('Memory usage high', { used: '850MB' });
logger.error('Database connection failed', { db: 'main', retry: 3 });
يعرض التسجيل المنظم بتنسيق JSON باستخدام winston — مستويات سجل مختلفة (info وwarn وerror) وبيانات وصفية سياقية مرفقة بكل إدخال سجل.
6. تغطية الاختبارات وTDD
(1) تغطية الاختبارات
يتضمن Jest أداة Istanbul لإعداد تقارير التغطية:
npm run test:coverage
Output:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 90.5 | 85.71 | 100 | 90.5 |
math.js | 100 | 100 | 100 | 100 |
app.js | 88.24 | 83.33 | 100 | 88.24 | 45-48
----------|---------|----------|---------|---------|-------------------
شرح أبعاد التغطية:
| البعد | المعنى | الهدف المقترح |
|---|---|---|
| Statements | نسبة التعليمات المنفذة | ≥ 80% |
| Branches | نسبة الفروع الشرطية المنفذة | ≥ 75% |
| Functions | نسبة الدوال المستدعاة | ≥ 85% |
| Lines | نسبة أسطر الكود المنفذة | ≥ 80% |
إن السعي وراء تغطية 100% له عوائد متناقصة؛ فالتغطية بنسبة 80% تكتشف معظم الأخطاء.
▶ مثال: (2) سير عمل TDD (Red-Green-Refactor)
flowchart LR
A["🔴 Red<br/>Write failing test"] --> B["🟢 Green<br/>Write minimal code to pass"]
B --> C["🔵 Refactor<br/>Improve code while tests pass"]
C --> A
- Red: اكتب اختبارًا أولاً (سيفشل لأن الميزة غير موجودة بعد)
- Green: اكتب الحد الأدنى من الكود لنجاح الاختبار
- Refactor: حسّن الكود مع استمرار نجاح الاختبارات
▶ مثال: دوال أدوات السلاسل النصية باستخدام TDD
// string-utils.test.js — اكتب الاختبار أولاً (Red)
const { capitalize, truncate, slugify } = require('./string-utils');
describe('StringUtils', () => {
describe('capitalize', () => {
test('should capitalize first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('should handle empty string', () => {
expect(capitalize('')).toBe('');
});
test('should handle already capitalized', () => {
expect(capitalize('Hello')).toBe('Hello');
});
});
describe('truncate', () => {
test('should truncate long strings', () => {
expect(truncate('Hello World', 5)).toBe('Hello...');
});
test('should not truncate short strings', () => {
expect(truncate('Hi', 10)).toBe('Hi');
});
});
describe('slugify', () => {
test('should convert to URL slug', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('should handle special characters', () => {
expect(slugify('C# & .NET!')).toBe('c-and-net');
});
});
});
// string-utils.js — اكتب التنفيذ بعد الاختبار (Green)
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, maxLength) {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + '...';
}
function slugify(str) {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { capitalize, truncate, slugify };
7. مثال شامل: كتابة مجموعة اختبارات كاملة لواجهة برمجة تطبيقات لإدارة المهام
▶ مثال: مجموعة اختبارات واجهة برمجة تطبيقات المهام
الخطوة 1 — كود التطبيق
// tasks-app.js
const express = require('express');
const app = express();
app.use(express.json());
let tasks = [];
let nextId = 1;
function resetTasks() {
tasks = [];
nextId = 1;
}
app.get('/api/tasks', (req, res) => {
const { status, priority } = req.query;
let filtered = tasks;
if (status) filtered = filtered.filter(t => t.status === status);
if (priority) filtered = filtered.filter(t => t.priority === priority);
res.json(filtered);
});
app.get('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
res.json(task);
});
app.post('/api/tasks', (req, res) => {
const { title, priority = 'medium' } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const task = { id: nextId++, title, priority, status: 'pending' };
tasks.push(task);
res.status(201).json(task);
});
app.patch('/api/tasks/:id', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) return res.status(404).json({ error: 'Task not found' });
const { title, status, priority } = req.body;
if (title) task.title = title;
if (status) task.status = status;
if (priority) task.priority = priority;
res.json(task);
});
app.delete('/api/tasks/:id', (req, res) => {
const index = tasks.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Task not found' });
const deleted = tasks.splice(index, 1);
res.json(deleted[0]);
});
module.exports = { app, resetTasks };
الخطوة 2 — مجموعة الاختبارات الكاملة
// tasks-app.test.js
const request = require('supertest');
const { app, resetTasks } = require('./tasks-app');
beforeEach(() => {
resetTasks();
});
describe('Tasks API — CRUD operations', () => {
test('should create a task', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Write tests', priority: 'high' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: 1,
title: 'Write tests',
priority: 'high',
status: 'pending',
});
});
test('should reject task without title', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ priority: 'high' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Title is required');
});
test('should list all tasks', async () => {
await request(app).post('/api/tasks').send({ title: 'Task 1' });
await request(app).post('/api/tasks').send({ title: 'Task 2' });
const response = await request(app).get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
});
test('should filter tasks by status', async () => {
await request(app).post('/api/tasks').send({ title: 'Pending task' });
const createRes = await request(app).post('/api/tasks').send({ title: 'Done task' });
await request(app).patch(`/api/tasks/${createRes.body.id}`).send({ status: 'done' });
const response = await request(app).get('/api/tasks?status=done');
expect(response.body).toHaveLength(1);
expect(response.body[0].status).toBe('done');
});
test('should update a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'Old title' });
const response = await request(app)
.patch(`/api/tasks/${createRes.body.id}`)
.send({ title: 'New title', status: 'done' });
expect(response.status).toBe(200);
expect(response.body.title).toBe('New title');
expect(response.body.status).toBe('done');
});
test('should delete a task', async () => {
const createRes = await request(app).post('/api/tasks').send({ title: 'To delete' });
const response = await request(app).delete(`/api/tasks/${createRes.body.id}`);
expect(response.status).toBe(200);
const listRes = await request(app).get('/api/tasks');
expect(listRes.body).toHaveLength(0);
});
test('should return 404 for non-existent task', async () => {
const response = await request(app).get('/api/tasks/999');
expect(response.status).toBe(404);
});
});
تشغيل:
npm test
Output:
PASS ./tasks-app.test.js
Tasks API — CRUD operations
✓ should create a task (15 ms)
✓ should reject task without title (3 ms)
✓ should list all tasks (4 ms)
✓ should filter tasks by status (8 ms)
✓ should update a task (5 ms)
✓ should delete a task (5 ms)
✓ should return 404 for non-existent task (2 ms)
Tests: 7 passed, 7 total
Time: 0.8s
❓ أسئلة شائعة
:memory:) أو نسخة قاعدة بيانات مخصصة للاختبار. وتستخدم اختبارات E2E قاعدة بيانات بيئة الاختبار.info، يتم إخراج مستوى info وما فوقه فقط.node --inspect-brk node_modules/.bin/jest --runInBand ثم صحح الأخطاء في Chrome DevTools. يضمن --runInBand تشغيل Jest على خيط واحد حتى تعمل نقاط التوقف.http.createServer(app) — لا يتم شغل أي منفذ، ويُغلق تلقائيًا عند انتهاء الاختبارات.📖 ملخص
- هرم الاختبارات: اختبارات الوحدة (كثيرة، سريعة، رخيصة) → اختبارات التكامل (متوسطة) → اختبارات E2E (قليلة، بطيئة، مكلفة)
- اختبار الوحدة مع Jest بدون تكوين: describe/it/expect + mocks للتبعيات الخارجية
- يختبر Supertest واجهة Express في الذاكرة: request(app).get/post/patch/delete
- مصحح الأخطاء المدمج
node inspectفي Node.js مع نقاط توقف مرئية في Chrome DevTools - يوفر winston تسجيلًا منظمًا: مستويات متعددة + transports متعددة + دورة السجلات
- تغطية الاختبار بنسبة 80%+ كافية؛ يحسّن سير عمل TDD (Red→Green→Refactor) جودة الكود
📝 تمارين
- اكتب اختبارات وحدة باستخدام Jest للدالة التالية:
function isPalindrome(str) { return str === str.split('').reverse().join(''); }، بحيث تغطي المتناظرات العادية، وغير المتناظرة، والسلاسل الفارغة، وحالات الأحرف المختلطة. - أنشئ واجهة برمجة تطبيقات Express (عمليات CRUD للمسار
/api/books)، واكتب 5 اختبارات تكامل على الأقل باستخدام Supertest (إنشاء، قراءة، تحديث، حذف، 404). - أضف برنامجًا وسيطًا للتسجيل باستخدام winston إلى تطبيق Express قائم، بحيث يسجل method/url/status/duration لكل طلب، مع دورة يومية للسجلات.
- استخدم TDD لتطوير دالة
formatCurrency(amount, currency): اكتب أولاً اختبارًا يفشل (مثلformatCurrency(1234.5, 'USD')→'$1,234.50')، ثم اكتب التنفيذ. - شغّل
jest --coverage، وابحث عن الملفات ذات التغطية الأقل من 80%، وأضف حالات اختبار لتحقيق هدف التغطية.