EduFlow
اختبار تطبيقات JavaScript
⁦0%⁩

الوحدة 1 · لماذا نختبر، وماذا نختبر

إعداد Vitest

16 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تضيف Vitest إلى مشروع Vite، وتكتب أول ملف اختبار وتشغّله، وتستخدم وضع المراقبة والمرشّحات كي تبقى دورة التغذية الراجعة أقل من ثانية.

  1. 1التثبيت والتشغيل
  2. 2أول ملف اختبار
  3. 3أين توجد الإعدادات
  4. 4تشغيل جزء من مجموعة الاختبارات
  5. 5في ساحة التجربة

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

أسرع طريقة للتوقّف عن كتابة الاختبارات هي إعداد بطيء ومزعج. إن كان تشغيل اختبار واحد يستغرق 15 ثانية ويتطلّب ثلاثة ملفات إعدادات، فستتخطّاه “هذه المرة فقط” إلى أن لا يبقى أي اختبار. وُجدت Vitest في الأساس لحلّ هذه المشكلة: فهي تتّصل بإعداد Vite الموجود أصلًا في تطبيقك، فلا يستغرق أول اختبار أكثر من دقيقتين تقريبًا.

التثبيت والتشغيل

في مشروع Vite (React أو Vue أو JavaScript صرف، لا فرق):

Terminal
npm install --save-dev vitest

أضف سكربتين إلى package.json:

package.json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

يبدأ npm test وضع المراقبة (watch mode): تشغّل Vitest مجموعة الاختبارات ثم تنتظر. وحين تحفظ ملفًا، تعيد تشغيل الاختبارات التي تستورده فقط، معتمدةً على مخطّط الوحدات (module graph) في Vite. على حاسوب محمول يستغرق ذلك عادةً أقل بكثير من ثانية، وهذا ما يجعل الاختبار جزءًا من البرمجة لا عبئًا تؤجّله إلى النهاية. أما npm run test:run فيشغّل كل شيء مرة واحدة ثم يخرج؛ وهذا هو الأمر المناسب لـ CI (كما تُطفئ Vitest وضع المراقبة تلقائيًا حين يكون متغيّر البيئة CI مضبوطًا).

إن كنت تفضّل مشغّلًا مرئيًا، فثبّت @vitest/ui وشغّل vitest --ui: سيفتح لوحة في المتصفّح تعرض كل ملف وكل اختبار، مع حالات الفشل ومخرجات الـ console ومخطّط الوحدات، ويعيد التشغيل عند الحفظ تمامًا كما تفعل الطرفية. وإضافة Vitest الرسمية لـ VS Code تعطيك أزرار التشغيل وتصحيح الأخطاء نفسها بجانب كل اختبار داخل المحرّر. اختر ما يُبقيك في حالة تركيز؛ فالاختبارات هي نفسها.

أول ملف اختبار

يحفظ Splitwise-lite المبالغ المالية كسنتات صحيحة (سنشرح السبب في الدرس القادم)، لذا يحتاج إلى دالة تحوّل 1234 إلى "$12.34":

src/money.js
export function formatMoney(cents) {
  const sign = cents < 0 ? '-' : '';
  const abs = Math.abs(cents);
  const dollars = Math.floor(abs / 100);
  const rest = String(abs % 100).padStart(2, '0');
  return `${sign}${dollars}.${rest}`;
}

ضع الاختبار بجوارها، باسم يجعل Vitest تجده:

src/money.test.js
import { describe, test, expect } from 'vitest';
import { formatMoney } from './money.js';

describe('formatMoney', () => {
  test('formats dollars and cents', () => {
    expect(formatMoney(1234)).toBe('$12.34');
  });

  test('pads single-digit cents', () => {
    expect(formatMoney(505)).toBe('$5.05');
  });
});

يجمع describe الاختبارات المترابطة (ويظهر في التقرير بصيغة formatMoney > pads single-digit cents). ويعرّف test (واسمه البديل it) اختبارًا واحدًا. أما expect(value).toBe(expected) فهو الـ assertion (التأكيد): يرمي خطأً حين تختلف القيمتان، تمامًا مثل expectEqual التي كتبتها في الدرس السابق.

أين توجد الإعدادات

تقرأ Vitest ملف vite.config.js الموجود لديك، فتعمل الـ aliases والـ plugins في الاختبارات دون أي جهد. وتوضع خيارات الاختبار تحت المفتاح test:

vite.config.js
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'node',
  },
});

سطر الشرطات المائلة الثلاث يعطي محرّرك الأنواع الخاصة بالمفتاح test. وenvironment: 'node' هو الافتراضي، كتبناه هنا صراحةً كي تعرف أن هذا المفتاح موجود. المنطق الصافي مثل تنسيق المبالغ يعمل في Node، وهو سريع. أما الاختبارات التي تحتاج إلى document (وستكتبها في القسم 3) فتستخدم jsdom أو happy-dom، وكلاهما يُثبَّت على حدة، ويُضبط إمّا هنا أو لكل ملف بتعليق // @vitest-environment jsdom في أعلاه.

سترى أيضًا globals: true في بعض المشاريع. هذا الخيار يجعل test وexpect متاحين دون استيراد، على طريقة Jest. لكن الاستيراد الصريح هو الافتراضي، ويتوافق بشكل أفضل مع المحرّرات وأدوات الـ lint، لذا تستوردهما هذه الدورة.

تشغيل جزء من مجموعة الاختبارات

كلما كبرت مجموعة الاختبارات، سترغب في تشغيل جزء أصغر منها:

Terminal
npx vitest money            # only files whose path contains "money"
npx vitest -t "pads"        # only tests whose name matches "pads"
npx vitest run --reporter=verbose   # one pass, every test name listed

داخل الملف، يشغّل test.only(...) ذلك الاختبار وحده، ويركن test.skip(...) اختبارًا جانبًا. كلاهما مفيد أثناء تتبّع الأخطاء، وخطير إن دخل في commit، لأن .only منسيًّا يُطفئ بقية الملف بصمت. وتحمي Vitest من ذلك في CI: فحين يكون متغيّر البيئة CI مضبوطًا، يفشل أي اختبار عليه .only برسالة “Unexpected .only modifier” بدل أن يتخطّى جيرانه بهدوء.

في ساحة التجربة

تعمل التمارين التالية في بيئة معزولة داخل المتصفّح، فيها جزء متوافق مع Vitest: الدوال describe وtest وexpect وvi والـ hooks متاحة مباشرة، والكود الخاضع للاختبار (الـ subject) محمَّل لك، فلا حاجة إلى أي استيراد. حين تتحقّق من حلّك، تعمل اختباراتك على الـ subject الحقيقي ويجب أن تنجح، ثم على بضع نسخ معطوبة عمدًا ويجب أن تفشل على كل واحدة منها.

ينظر الدرس القادم عن قرب في ما يوضع داخل الاختبار، وفي كيفية قراءة المخرجات حين يفشل.

أهم النقاط

  • تعيد Vitest استخدام إعدادات Vite لديك، فتنطبق الـ aliases والـ plugins والتحويلات نفسها على اختباراتك دون إعداد يُذكر.
  • الملفات المسمّاة *.test.js أو *.spec.js (ونظيراتها بامتدادات ts وjsx وtsx) تُلتقط تلقائيًا؛ ضع كل ملف منها بجوار الكود الذي يختبره.
  • شغّل vitest أثناء العمل (فوضع المراقبة يعيد تشغيل الاختبارات المتأثّرة عند الحفظ)، وvitest run في CI لتشغيلة واحدة.
  • البيئة الافتراضية هي Node؛ لا تنتقل إلى jsdom أو happy-dom إلا في الاختبارات التي تحتاج إلى DOM.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تشغّل npx vitest على حاسوبك، ولا ينتهي الأمر بعد انتهاء الاختبارات. ما الذي يحدث؟

السؤال 2 من 3

أيّ ملف ستلتقطه Vitest كملف اختبار بالإعدادات الافتراضية؟

السؤال 3 من 3

يفشل اختبار لـ formatMoney بالرسالة ReferenceError: document is not defined، مع أن formatMoney لا تلمس الـ DOM أبدًا. ما السبب الأرجح؟

تدريب

بيئة تجريب الكود

أول اختبارات لـ formatMoney

في تبويب الـ subject توجد الدالة formatMoney(cents)، التي تحوّل السنتات الصحيحة إلى نص بالدولار: 1234 تصبح "$12.34" و-505 تصبح "-$5.05".

في الكود المبدئي اختبار واحد. أضف اختبارات حتى تنجح مجموعتك على الكود الحقيقي وتفشل على كل واحدة من ثلاث نسخ معطوبة أخفاها المدقّق: واحدة تنسى إكمال السنتات المكوّنة من رقم واحد بصفر، وواحدة تُسقط إشارة السالب، وواحدة تقرّب الدولارات بدل أن تأخذ الجزء الصحيح منها. استخدم describe وtest وexpect(...).toBe(...)؛ لا يوجد ما تستورده.

// Splitwise-lite stores money as integer cents.
// formatMoney(1234) -> "$12.34", formatMoney(-505) -> "-$5.05"
function formatMoney(cents) {
  const sign = cents < 0 ? '-' : '';
  const abs = Math.abs(cents);
  const dollars = Math.floor(abs / 100);
  const rest = String(abs % 100).padStart(2, '0');
  return `${sign}${dollars}.${rest}`;
}
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

النتائج

(لا مخرجات)

الفحوص

  • كل اختباراتك تنجح على formatMoney الحقيقية
  • تلتقط النسخة التي تنسى إكمال السنتات المكوّنة من رقم واحد بصفر
  • تلتقط النسخة التي تُسقط إشارة السالب
  • تلتقط النسخة التي تقرّب الدولارات بدل أخذ الجزء الصحيح