مقدمة: لماذا الاختبار المتقدم ضروري لمشروع JavaScript
في التطوير الحديث باستخدام Node.js، لم يعد الاختبار مرحلة اختيارية. بدون تغطية اختبارية جيدة للكود، فإن أي إعادة هيكلة أو إضافة وظائف جديدة قد تتحول إلى "روليت روسي". ومع ذلك، مجرد كتابة بعض التأكيدات (assertions) لا يكفي. على المستوى المتقدم، يجب على المطور إتقان ثلاث طبقات رئيسية: الاختبارات الوحدوية (التحقق من الوحدات المنعزلة)، الاختبارات التكاملية (تفاعل المكونات، قواعد البيانات، واجهات API)، والاختبارات الشاملة (E2E) (محاكاة إجراءات المستخدم). في هذه المقالة، سنشرح الأساليب العملية لكل طبقة باستخدام Node.js مع أدوات حديثة: Jest وSupertest وPlaywright.
1. الاختبارات الوحدوية: العزل، الكائنات الوهمية (Mocks) والبدائل (Stubs) على المستوى المتقدم
تتحقق الاختبارات الوحدوية من عمل دالة واحدة أو وحدة نمطية في عزلة عن التبعيات الخارجية (قواعد البيانات، نظام الملفات، طلبات الشبكة). المهمة الرئيسية هي إنشاء كائنات وهمية (Mock) لهذه التبعيات بشكل صحيح، بحيث يكون الاختبار سريعًا ومحددًا.
تقنيات متقدمة لإنشاء الكائنات الوهمية في Jest
لنأخذ مثالاً: دالة تتصل بواجهة API خارجية وتخزن النتيجة مؤقتًا في Redis. نحتاج إلى اختبار منطق التخزين المؤقت دون تنفيذ طلبات حقيقية.
// cacheService.jsconst axios = require('axios');const redis = require('redis');
class CacheService { constructor() { this.client = redis.createClient(); }
async fetchData(url) { const cached = await this.client.get(url); if (cached) return JSON.parse(cached);
const response = await axios.get(url); await this.client.setEx(url, 3600, JSON.stringify(response.data)); return response.data; }}
module.exports = CacheService;// cacheService.test.jsconst CacheService = require('./cacheService');const axios = require('axios');const redis = require('redis');
jest.mock('axios');jest.mock('redis', () => ({ createClient: jest.fn(() => ({ get: jest.fn(), setEx: jest.fn() }))}));
describe('CacheService', () => { let service; let mockRedisClient;
beforeEach(() => { mockRedisClient = redis.createClient(); service = new CacheService(); });
test('should return cached data if present', async () => { const url = 'https://api.example.com/data'; const cachedData = { id: 1, value: 'cached' }; mockRedisClient.get.mockResolvedValue(JSON.stringify(cachedData));
const result = await service.fetchData(url);
expect(result).toEqual(cachedData); expect(axios.get).not.toHaveBeenCalled(); });
test('should fetch and cache data if not cached', async () => { const url = 'https://api.example.com/data'; const apiData = { id: 2, value: 'fresh' }; mockRedisClient.get.mockResolvedValue(null); axios.get.mockResolvedValue({ data: apiData });
const result = await service.fetchData(url);
expect(result).toEqual(apiData); expect(mockRedisClient.setEx).toHaveBeenCalledWith(url, 3600, JSON.stringify(apiData)); });});النقاط الرئيسية:
- يقوم
jest.mock('axios')تلقائيًا بإنشاء كائن وهمي لوحدة axios بأكملها. - بالنسبة لـ Redis، نستخدم مصنعًا (factory) يُرجع كائنًا وهميًا بالدوال
getوsetEx. - نتحقق من أنه عند وجود بيانات مخزنة مؤقتًا، فإن الاستدعاء الخارجي لا يحدث.
2. الاختبارات التكاملية: اختبار التفاعل مع قاعدة بيانات حقيقية وواجهة API
تتحقق الاختبارات التكاملية من كيفية عمل أجزاء النظام المختلفة معًا. بالنسبة لـ Node.js، يعني هذا غالبًا اختبار مسارات Express مع قاعدة بيانات حقيقية (أو اختبارية). أفضل أداة هي Supertest مع Jest.
إعداد قاعدة بيانات اختبارية و Supertest
مثال: API لإدارة المستخدمين. نستخدم SQLite في الذاكرة للعزل.