الوحدة 2 · اختبارات وحدة لها قيمة
بدائل الاختبار، ومتى يضرّ الـ mocking
20 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
تستبدل المتعاونين بـ vi.fn وvi.spyOn، وتتحكّم في الوقت بالمؤقّتات المزيّفة، وتميّز الحالات التي يجعل فيها الـ mocking الاختبار ينجح بينما الكود الحقيقي معطّل.
- 1المصطلحات، باختصار
- 2vi.fn: دالة تتذكّر
- 3vi.spyOn: مراقبة دالة حقيقية
- 4المؤقّتات المزيّفة: تخطّي الانتظار
- 5متى يضرّ الـ mocking
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
في الدرس الماضي كتبت API بديلًا (stub) باليد، مع عدّاد. نجح الأمر، واستغرق نحو خمسة عشر سطرًا. أدوات vi في Vitest تفعل الشيء نفسه في سطر واحد، وتضيف قوة كبيرة. وهذه القوة هي المشكلة: الـ mocking أسرع طريقة لكتابة اختبار ينجح بينما الكود الحقيقي معطّل. يغطّي هذا الدرس الأدوات، ويغطّي حسن التقدير في استخدامها.
المصطلحات، باختصار
كل هذه test doubles (بدائل الاختبار)، أي أشياء تقوم مقام متعاون حقيقي:
في Vitest، يعطيك vi.fn() الـ stub والـ spy (الجاسوس) في كائن واحد، والناس يسمّون كل ذلك “mocks” (بدائل وهمية). الأسماء أقل أهمية من السؤال الذي يجيب عنه كل منها: ماذا يعيد هذا المتعاون؟ (stub)، وكيف استُدعي؟ (spy).
vi.fn: دالة تتذكّر
يرسل Splitwise-lite تذكيرًا إلى كل من عليه مال. دالة الإرسال تُمرَّر كمعامل، فيستطيع الاختبار استبدالها:
import { test, expect, vi } from 'vitest';
import { remindDebtors } from './remind.js';
test('reminds people who owe money, with the amount', () => {
const sendReminder = vi.fn();
const sent = remindDebtors({ Ana: 1500, Ben: -500, Cai: 0 }, sendReminder);
expect(sent).toBe(1);
expect(sendReminder).toHaveBeenCalledTimes(1);
expect(sendReminder).toHaveBeenCalledWith('Ben', 'You owe $5.00 to the group');
});يعيد vi.fn() القيمة undefined افتراضيًا. أعطه سلوكًا حين يحتاج الكود الخاضع للاختبار إلى إجابة:
const post = vi.fn().mockResolvedValue({ id: 'e1' }); // async success
const failing = vi.fn().mockRejectedValue(new Error('Offline')); // async failure
const send = vi.fn().mockImplementation((name) => {
if (name === 'Ben') throw new Error('Mailbox full');
});يُسجَّل كل استدعاء في send.mock.calls، وهي مصفوفة من مصفوفات المعاملات، للحالات النادرة التي لا تغطّيها الـ matchers.
vi.spyOn: مراقبة دالة حقيقية
أحيانًا يكون المتعاون دالة على كائن موجود، مثل console.warn. يلفّها vi.spyOn: افتراضيًا تظلّ الدالة الحقيقية تعمل، ويسجّل الـ spy الاستدعاءات. أضف mockImplementation لإسكاتها أو استبدالها:
import { afterEach, test, expect, vi } from 'vitest';
afterEach(() => {
vi.restoreAllMocks(); // put console.warn back
});
test('warns and carries on when one reminder fails', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const send = vi.fn().mockImplementationOnce(() => { throw new Error('Mailbox full'); });
const sent = remindDebtors({ Ben: -500, Dee: -300 }, send);
expect(sent).toBe(1);
expect(send).toHaveBeenCalledTimes(2);
expect(warn).toHaveBeenCalledWith('Could not remind Ben: Mailbox full');
});يستبدل الـ spy دالة على كائن مشترك. انسَ استعادتها، وسيعمل كل اختبار لاحق في الملف مع console.warn صامت. وجود vi.restoreAllMocks() داخل afterEach، أو restoreMocks: true في الإعدادات، يجعل التنظيف تلقائيًا.
المؤقّتات المزيّفة: تخطّي الانتظار
الكود الذي ينتظر (الحفظ التلقائي المؤجَّل بـ debounce، والتباطؤ بين المحاولات، ورسائل “تراجع” المؤقّتة) اختباره بالوقت الحقيقي مؤلم. تستبدل المؤقّتات المزيّفة (fake timers) كلًّا من setTimeout وsetInterval وDate بساعة يتحكّم فيها الاختبار:
import { afterEach, test, expect, vi } from 'vitest';
afterEach(() => vi.useRealTimers());
test('autosaves the draft 2 seconds after the last change', () => {
vi.useFakeTimers();
const save = vi.fn();
const draft = createDraft({ save, delayMs: 2000 }); // debounced
draft.update({ description: 'Din' });
draft.update({ description: 'Dinner' });
vi.advanceTimersByTime(1999);
expect(save).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(save).toHaveBeenCalledWith({ description: 'Dinner' });
});يشغّل vi.advanceTimersByTime(ms) فورًا كل مؤقّت يحين موعده خلال تلك الفترة. ويثبّت vi.setSystemTime(date) قيمة new Date()، وهكذا تختبر أن “التذكيرات تُرسل في أول كل شهر”. وحين تجدول المؤقّتات promises، استخدم النسخ غير المتزامنة مثل vi.advanceTimersByTimeAsync. (ساحة التجربة لا تشغّل إلا المؤقّتات الحقيقية، لذا يبقى هذا المثال على الصفحة فقط.)
متى يضرّ الـ mocking
كل mock ادّعاء عن طريقة عمل الشيء الحقيقي. وحين يبتعد الواقع عن الادّعاء، يستمرّ الاختبار في النجاح. الأنماط التي تؤذي:
- استخدام الـ mocks مكان وحداتك أنت. استبدال
parseAmountداخل اختبار النموذج يعني أن النموذج والمحلّل لا يُختبران معًا أبدًا، وهذا كان الهدف من الاختبار. كودك الصافي سريع؛ استخدمه. - استخدام mock مكان الشيء الخاضع للاختبار نفسه. إن تجسّست على
remindDebtorsوأنت تختبرremindDebtors، فأنت تختبر الـ spy. - تأكيد كل استدعاء. فحص التسلسل الدقيق لأربعة عشر استدعاءً داخليًا يجمّد التفاصيل الداخلية. أكّد الاستدعاءات التي تهمّ المستدعي، مثل “وصل تذكير إلى Ben”.
- mocks تكذب. الـ API البديل لديك يعيد
{ id }؛ والحقيقي يعيد{ data: { id } }. الـ unit tests خضراء، والتطبيق معطّل. ومحاكاة الشبكة في القسم التالي واختبارات الـ end-to-end في القسم 4 موجودة لالتقاط هذا بالضبط.
فحص سريع لأي mock تكتبه: احذف سطرًا واحدًا من الكود الحقيقي الذي يقوم مقامه. إن لم يفشل أي اختبار في أي مكان من المجموعة، فذلك الـ mock كان يُخفي السلوك بدل أن يختبر ما حوله، وتحتاج في مكان ما إلى اختبار يشغّل الشيء الحقيقي.
قاعدتي: استخدم الـ mocks عند حدود نظامك (الشبكة، والبريد، والدفع، والوقت، والعشوائية)، وشغّل الكود الحقيقي في كل ما بداخله.
في التمرين، استخدم vi.fn وvi.spyOn لالتقاط أربعة أخطاء في remindDebtors. وينتقل القسم 3 من الدوال إلى الـ DOM.
أهم النقاط
- ينشئ
vi.fn()دالة تسجّل كل استدعاء، وتستطيع أن تحدّد ما تعيده باستخدامmockReturnValueأوmockResolvedValueأوmockImplementation. - يراقب
vi.spyOn(object, 'method')دالة حقيقية ويستطيع استبدالها؛ أعِد الـ spies دائمًا إلى أصلها، مثلًا بـvi.restoreAllMocks()داخلafterEach. - استخدم الـ mocks عند حدود نظامك (الشبكة، والبريد، والوقت، والعشوائية)، واستخدم الكود الحقيقي لوحداتك أنت.
- تسمح المؤقّتات المزيّفة (
vi.useFakeTimersوvi.advanceTimersByTime) للاختبار بأن يقفز فوق فترات الانتظار بدل أن ينتظرها. - كل mock ادّعاء عن طريقة عمل الشيء الحقيقي؛ وحين يبتعد هذا الادّعاء عن الواقع، يبقى الاختبار أخضر بينما ينكسر الإنتاج.