دليل شامل لاختبارات JavaScript
تُعدّ الاختبارات جانبًا أساسيًا في تطوير البرمجيات الاحترافي. فهي تضمن أن شيفرتك تعمل كما هو متوقع، وتسهّل الصيانة، وتتيح اكتشاف حالات الانحدار (régressions). يستعرض هذا المقال مختلف المقاربات والأدوات لاختبار تطبيقات JavaScript الخاصة بك بفعالية.
لماذا تختبر شيفرتك؟
- الثقة: التأكد من أن شيفرتك تعمل بشكل صحيح
- التوثيق: تعمل الاختبارات كتوثيق حيّ
- إعادة هيكلة آمنة (Refactoring): تعديل الشيفرة دون الخوف من كسر بعض الوظائف
- الجودة: تقليل الأخطاء وتحسين التصميم
- التعاون: تسهيل العمل ضمن الفريق
- النشر المستمر: أتمتة عمليات التحقق قبل النشر
أنواع الاختبارات
الاختبارات الوحدوية
تتحقق الاختبارات الوحدوية من حسن عمل أصغر وحدات الشيفرة (الدوال، والأساليب، والأصناف) بمعزل عن غيرها.
// Fonction à tester
function somme(a, b) {
return a + b;
}
// Test unitaire avec Jest
test('additionne correctement deux nombres', () => {
expect(somme(2, 3)).toBe(5);
expect(somme(-1, 1)).toBe(0);
expect(somme(0, 0)).toBe(0);
});
اختبارات التكامل
تتحقق اختبارات التكامل من أن أجزاء مختلفة من النظام تعمل معًا بشكل صحيح.
// Test d'intégration avec Jest
describe('Service utilisateur', () => {
test('crée un utilisateur et le récupère de la base de données', async () => {
// Créer un utilisateur
const userId = await userService.createUser({
name: 'Alice',
email: 'alice@example.com'
});
// Récupérer l'utilisateur
const user = await userService.getUserById(userId);
// Vérifier que l'utilisateur est correctement récupéré
expect(user).toEqual({
id: userId,
name: 'Alice',
email: 'alice@example.com'
});
});
});
الاختبارات الوظيفية / الاختبارات الشاملة (End-to-End, E2E)
تتحقق اختبارات E2E من سلوك التطبيق من وجهة نظر المستخدم النهائي.
// Test E2E avec Cypress
describe('Formulaire de connexion', () => {
it('permet à un utilisateur de se connecter', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('utilisateur@exemple.fr');
cy.get('input[name="password"]').type('motdepasse123');
cy.get('button[type="submit"]').click();
// Vérifier que l'utilisateur est redirigé vers le tableau de bord
cy.url().should('include', '/dashboard');
cy.contains('Bienvenue, Utilisateur').should('be.visible');
});
});
اختبارات المكوّنات
في التطبيقات القائمة على المكوّنات (React وVue وAngular)، تتحقق اختبارات المكوّنات من عرض مكوّنات واجهة المستخدم وسلوكها.
// Test de composant React avec React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import Compteur from './Compteur';
test('incrémente le compteur lorsque le bouton est cliqué', () => {
render(<Compteur />);
// Vérifier l'état initial
expect(screen.getByText('Compteur: 0')).toBeInTheDocument();
// Cliquer sur le bouton
fireEvent.click(screen.getByText('Incrémenter'));
// Vérifier que le compteur a été incrémenté
expect(screen.getByText('Compteur: 1')).toBeInTheDocument();
});
اختبارات الأداء
تتحقق اختبارات الأداء من أن تطبيقك يلبّي متطلبات السرعة والكفاءة.
// Test de performance avec Lighthouse CI
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/'],
numberOfRuns: 3,
},
assert: {
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'interactive': ['error', { maxNumericValue: 3000 }],
'max-potential-fid': ['error', { maxNumericValue: 100 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
اختبارات الأمان
تحدد اختبارات الأمان الثغرات المحتملة في تطبيقك.
// Exemple avec OWASP ZAP (via CLI)
// zap-cli quick-scan --self-contained --start-options "-config api.disablekey=true" https://example.com
أطر العمل والأدوات الشائعة للاختبار
Jest
Jest هو إطار عمل متكامل لاختبار JavaScript بحد أدنى من الإعدادات.
// Installation
// npm install --save-dev jest
// Configuration minimale (package.json)
{
"scripts": {
"test": "jest"
}
}
// Exemple de test
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
test('somme additionne deux nombres', () => {
expect(somme(1, 2)).toBe(3);
});
test('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).toBe(-3);
});
test('somme avec zéro retourne l\'autre nombre', () => {
expect(somme(0, 5)).toBe(5);
});
});
Mocha + Chai
Mocha هو إطار عمل مرن للاختبار، ويُستخدم غالبًا مع Chai لكتابة التأكيدات (assertions).
// Installation
// npm install --save-dev mocha chai
// Exemple de test
const { expect } = require('chai');
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
it('somme additionne deux nombres', () => {
expect(somme(1, 2)).to.equal(3);
});
it('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).to.equal(-3);
});
});
Cypress
Cypress هي أداة حديثة لاختبارات E2E تعمل داخل المتصفح.
// Installation
// npm install --save-dev cypress
// Exemple de test
describe('Page d\'accueil', () => {
beforeEach(() => {
cy.visit('/');
});
it('affiche le titre correct', () => {
cy.get('h1').should('contain', 'Bienvenue sur notre site');
});
it('navigue vers la page À propos', () => {
cy.get('nav').contains('À propos').click();
cy.url().should('include', '/about');
cy.get('h1').should('contain', 'À propos de nous');
});
});
Playwright
Playwright هي أداة لاختبارات E2E تدعم عدة متصفحات.
// Installation
// npm install --save-dev @playwright/test
// Exemple de test
const { test, expect } = require('@playwright/test');
test('page d\'accueil a le bon titre', async ({ page }) => {
await page.goto('https://example.com');
// Vérifier le titre
const title = await page.title();
expect(title).toBe('Example Domain');
// Vérifier le contenu
const heading = await page.textContent('h1');
expect(heading).toBe('Example Domain');
});
Testing Library
Testing Library هي عائلة من المكتبات لاختبار واجهات المستخدم بطريقة تتمحور حول المستخدم.
// Installation pour React
// npm install --save-dev @testing-library/react @testing-library/jest-dom
// Exemple de test
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Formulaire from './Formulaire';
test('soumet le formulaire avec les valeurs correctes', () => {
const handleSubmit = jest.fn();
render(<Formulaire onSubmit={handleSubmit} />);
// Remplir le formulaire
fireEvent.change(screen.getByLabelText(/nom/i), {
target: { value: 'Alice' },
});
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'alice@example.com' },
});
// Soumettre le formulaire
fireEvent.click(screen.getByRole('button', { name: /soumettre/i }));
// Vérifier que la fonction onSubmit a été appelée avec les bonnes valeurs
expect(handleSubmit).toHaveBeenCalledWith({
nom: 'Alice',
email: 'alice@example.com',
});
});
Storybook
يتيح Storybook تطوير مكوّنات واجهة المستخدم واختبارها بمعزل عن غيرها.
// Installation
// npx sb init
// Exemple de story
// Button.stories.js
import React from 'react';
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
مقاربات الاختبار
التطوير المدفوع بالاختبارات (Test-Driven Development, TDD)
يتبع TDD دورة من ثلاث خطوات:
- Red: كتابة اختبار يفشل
- Green: كتابة الحد الأدنى من الشيفرة لجعل الاختبار ينجح
- Refactor: تحسين الشيفرة دون تغيير سلوكها
// Exemple de TDD
// 1. Red: Écrire un test qui échoue
test('convertit les degrés Celsius en Fahrenheit', () => {
expect(celsiusToFahrenheit(0)).toBe(32);
expect(celsiusToFahrenheit(100)).toBe(212);
});
// 2. Green: Écrire le code minimal pour faire passer le test
function celsiusToFahrenheit(celsius) {
return celsius * 9/5 + 32;
}
// 3. Refactor: Améliorer le code (si nécessaire)
function celsiusToFahrenheit(celsius) {
const factor = 9/5;
const freezingPointDiff = 32;
return celsius * factor + freezingPointDiff;
}
التطوير المدفوع بالسلوك (Behavior-Driven Development, BDD)
يركّز BDD على السلوك المتوقع للنظام من وجهة نظر المستخدم.
// Exemple avec Cucumber.js
// Feature: Connexion utilisateur
// En tant qu'utilisateur enregistré
// Je veux pouvoir me connecter
// Afin d'accéder à mon compte
// Scénario: Connexion réussie
// Étant donné que je suis sur la page de connexion
// Quand je saisis mon email "utilisateur@exemple.fr"
// Et je saisis mon mot de passe "motdepasse123"
// Et je clique sur le bouton "Se connecter"
// Alors je devrais être redirigé vers la page "Tableau de bord"
// Et je devrais voir "Bienvenue, Utilisateur"
// Implémentation des étapes
const { Given, When, Then } = require('@cucumber/cucumber');
Given('je suis sur la page de connexion', async function() {
await this.navigateTo('/login');
});
When('je saisis mon email {string}', async function(email) {
await this.fillField('email', email);
});
// ... autres étapes
الكائنات المحاكية (Mocks) والبدائل (Stubs) والمراقِبات (Spies)
Mocks
تستبدل الـ mocks التبعيات الحقيقية بنسخ محاكية.
// Exemple avec Jest
test('envoie un email de bienvenue lors de l\'inscription', () => {
// Mock du service d'email
const emailService = {
sendWelcomeEmail: jest.fn()
};
// Système sous test
const userService = new UserService(emailService);
// Action
userService.registerUser('alice@example.com', 'password123');
// Assertion
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('alice@example.com');
});
Stubs
توفّر الـ stubs ردودًا محددة مسبقًا على استدعاءات الدوال.
// Exemple avec Sinon.js
const sinon = require('sinon');
const database = require('./database');
test('récupère les données utilisateur', async () => {
// Créer un stub pour la méthode query
const queryStub = sinon.stub(database, 'query');
// Configurer le stub pour retourner des données spécifiques
queryStub.withArgs('SELECT * FROM users WHERE id = ?', [42])
.resolves([{ id: 42, name: 'Alice' }]);
// Système sous test
const userService = new UserService(database);
// Action
const user = await userService.getUserById(42);
// Assertion
expect(user).toEqual({ id: 42, name: 'Alice' });
// Restaurer le stub
queryStub.restore();
});
Spies
تراقب الـ spies استدعاءات الدوال دون تعديل سلوكها.
// Exemple avec Jest
test('enregistre les erreurs dans la console', () => {
// Créer un spy sur console.error
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// Action qui devrait générer une erreur
const calculator = new Calculator();
calculator.divide(10, 0);
// Assertion
expect(errorSpy).toHaveBeenCalledWith('Division par zéro');
// Restaurer le spy
errorSpy.mockRestore();
});
الاختبارات غير المتزامنة
Promises
// Test avec Jest
test('récupère les données utilisateur', () => {
return userService.getUserById(42)
.then(user => {
expect(user.name).toBe('Alice');
});
});
// Ou avec async/await
test('récupère les données utilisateur', async () => {
const user = await userService.getUserById(42);
expect(user.name).toBe('Alice');
});
Callbacks
// Test avec Jest
test('lit un fichier', done => {
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
done(err);
return;
}
try {
expect(data).toContain('Hello, world');
done();
} catch (error) {
done(error);
}
});
});
تغطية الشيفرة
تقيس تغطية الشيفرة (code coverage) النسبة التي تُنفَّذ من شيفرتك أثناء الاختبارات.
// Configuration Jest pour la couverture
// package.json
{
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
التكامل المستمر (CI)
ينفّذ التكامل المستمر اختباراتك تلقائيًا عند كل تعديل على الشيفرة.
# Exemple avec GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v2
أفضل الممارسات
- اختبر الوظائف الحرجة أولًا: ركّز على الشيفرة الأكثر أهمية
- اجعل الاختبارات بسيطة وسهلة القراءة: يجب أن يكون الاختبار سهل الفهم
- اختبار واحد، تأكيد واحد: ينبغي أن يتحقق كل اختبار من أمر واحد فقط
- اعزل اختباراتك: يجب ألا تعتمد الاختبارات على بعضها البعض
- استخدم بيانات اختبار واقعية: تجنّب البيانات المبسّطة أكثر من اللازم
- تجنّب الاختبارات الهشّة: يجب ألا تفشل الاختبارات لأسباب سطحية
- نفّذ الاختبارات بانتظام: ويُفضّل عند كل تعديل على الشيفرة
- حافظ على اختباراتك: الاختبارات هي شيفرة وتتطلب صيانة
أخطاء شائعة يجب تجنبها
- اختبار التنفيذ بدلاً من السلوك: ركّز على ما يفعله الكود، لا على كيفية قيامه بذلك
- تجاهل الحالات الحدّية: اختبر القيم القصوى وحالات الخطأ
- اختبار المسار السعيد فقط: لا تنسَ اختبار حالات الفشل
- اختبارات شديدة الارتباط بالكود: الاختبارات المفرطة في التحديد تنكسر بسهولة عند إعادة الهيكلة (refactoring)
- إهمال اختبارات الانحدار: أضف اختبارات للأخطاء التي تم إصلاحها
- الاختبارات البطيئة: تؤدي الاختبارات البطيئة إلى العزوف عن تنفيذها بشكل متكرر
الخاتمة
الاختبارات استثمار يحقق عوائد طوال دورة حياة المشروع. فهي تحسّن جودة الكود، وتسهّل الصيانة، وتتيح عمليات نشر أكثر تكراراً وموثوقية.
لا يوجد نهج واحد يناسب جميع الحالات في الاختبار. فالاستراتيجية المثلى تعتمد على طبيعة مشروعك وفريقك والقيود المفروضة عليك. والمهم هو أن تبدأ من نقطة ما وأن تحسّن تغطية الاختبارات لديك تدريجياً.
وباعتماد ثقافة الاختبار داخل فريقك واتباع أفضل الممارسات المعروضة في هذا المقال، يمكنك تحسين جودة تطبيقات JavaScript الخاصة بك وقابليتها للصيانة بشكل كبير.
نبذة عن InSkillCoach
خبير في التدريب والتقنيات
مدرّب متخصص في التقنيات المتقدمة والذكاء الاصطناعي، تدعمه شركة GNeurone Inc.
الشهادات:
- AWS Certified Solutions Architect – Professional
- شهادات Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش