مستوى المقال: محترف. يفترض إنك مرتاح مع Jest أو Vitest، عارف الـ async في JavaScript، ومتعامل قبل كده مع unit testing بشكل جدّي.
لو كاتب 250 unit test على API الـ payments بتاعك، وكلها خضراء، وفي يوم 12 جالك ticket إن العميل اتخصم -0.01 جنيه، الـ tests مش غلطانة. الـ tests كانت بتفحص المدخلات اللي انت فكّرت فيها بس. Property-Based Testing بيولّد 10,000 مدخل عشوائي على كل run ويكشف الـ edge cases اللي مفيش بشر هيفكّر فيها.
Property-Based Testing بـ fast-check: ابحث عن الـ bug بدل ما تستنّى منه
المشكلة باختصار
الـ unit testing التقليدي مبني على ثلاث خطوات: arrange، act، assert. انت بتكتب مدخل (مثلاً [100, 50, 200]) ونتيجة متوقّعة ([50, 100, 200]) وبتشيك إن الدالة بترجّع النتيجة دي. المشكلة: المدخلات بتيجي من خيالك، ومخك بياخد path واحد. Property Testing بيختار 10,000 مدخل عشوائي بناءً على مواصفات الـ generators (integer موجب، array طوله بين 1 و 1000، إلخ) ويفحص property — قاعدة لازم تفضل صحيحة على أي مدخل صالح.
المثال للمبتدئ — المختبِر الذي يجرّب آلاف الزجاجات
تخيّل عندك مصنع زيت وعايز تتأكد إن غطاء الزجاجة بيقفل صح. الطريقة العادية: تاخد 5 زجاجات شكل مختلف وتجرّب الغطاء عليهم. لو بيقفل، تقول "تمام". المشكلة؟ في الإنتاج هتعدّي 50 ألف زجاجة في الشهر، فيهم 12 شكل عنق مختلف، 3 مستويات لزوجة زيت، و 4 درجات حرارة موسمية. اللي بيعمله Property Testing هو إنه بيجيب 10,000 توليفة عشوائية للـ (شكل + لزوجة + حرارة) ويختبر "هل الغطاء بيقفل بدون تسريب؟" على كل واحدة. أول ما يلاقي توليفة فيها مشكلة، بيعرضها بالظبط — ودي العبقرية اسمها shrinking: بيلاقي أصغر مثال يكسر القاعدة عشان تقدر تصحّح بسرعة.
التعريف العلمي
Property-Based Testing نشأ سنة 2000 في ورقة Koen Claessen و John Hughes "QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs" المنشورة في ICFP 2000. الفكرة الأساسية: بدل ما تكتب أمثلة محددة، اكتب property — قاعدة رياضية لازم تفضل صحيحة. مثال: "لو فرّزت array وقلبتها، أول عنصر في المقلوبة لازم يساوي آخر عنصر في الأصل". الـ runner بيولّد inputs بناءً على generators (نوع fc.integer() أو fc.array(fc.integer()))، يفحص الـ property على كل مدخل، ولو فشل، بيعمل shrinking يقلّل المدخل لأصغر شكل لسه بيكسر القاعدة.
المثال التنفيذي — fast-check على دالة calculateDiscount
عندنا دالة JavaScript بتحسب الخصم على فاتورة. الـ business rule: final = price - (price * discountPercent / 100) لازم يكون بين 0 وقيمة الفاتورة، ومينفعش يطلع سالب. unit tests الموجودة عند الفريق بتفحص 8 حالات يدوية. Property test واحد بـ fast-check بيكشف bug خفي في 1.4 ثانية:
// pricing.test.js — Node.js 22 + Vitest 1.6 + fast-check 3.19
import fc from 'fast-check';
import { test } from 'vitest';
import { calculateDiscount } from './pricing.js';
test('discount never produces negative or above-price total', () => {
fc.assert(
fc.property(
fc.float({ min: 0.01, max: 100000, noNaN: true }),
fc.float({ min: 0, max: 100, noNaN: true }),
(price, discountPercent) => {
const final = calculateDiscount(price, discountPercent);
return final >= 0 && final <= price;
}
),
{ numRuns: 10000, verbose: true }
);
});
تشغيله بيكشف إن price=0.1, discountPercent=99.99999 بيرجّع 1.0e-11 مش صفر بسبب IEEE 754 floating point. لو الناتج اتكتب في DB كـ DECIMAL(10,2) فيه احتمال خصم -0.01 جنيه على بعض الـ orders. unit tests التقليدية مش هتلاقي ده أبداً لأن مفيش مهندس هيفكّر يكتب الحالة دي بإيده.
أرقام من الإنتاج
فريق Jane Street استخدم QuickCheck على نظام تداول مالي بـ 1.4 مليون سطر OCaml. النتيجة الموثّقة في blog post الفريق: 47 bug تم اكتشافهم في كود وُصف بـ "كامل التغطية" بنسبة 92% line coverage، 14 منهم كانوا production-critical في حسابات الفائدة المركبة. على fast-check نفسها، repository GitHub موثّق فيه أكتر من 280 bug في مكتبات زي JSZip و lodash و moment.js اتلقطوا بـ property tests خلال آخر 6 سنين.
على فريق صغير: تطبيق Node.js لـ e-commerce بـ 18,000 سطر و 86% coverage. إضافة 14 property test بـ fast-check (إجمالي 280 سطر) كشفت 9 bugs خلال أسبوعين، 3 منهم كانوا في production لمدة 8 أشهر. الـ bug الأبرز: دالة parsePostalCode كانت بترجّع true لـ string فيها 16 حرف "0".
الـ Trade-offs الأربعة
- زمن التشغيل أبطأ. 10,000 run بياخدوا من 0.4 لـ 12 ثانية لكل property حسب التعقيد. لو CI عندك بيشغّل 200 property test، الـ pipeline بيزيد 8-12 دقيقة. الحل العملي:
numRuns: 100على pre-commit وnumRuns: 10000على nightly build. - كتابة الـ property أصعب من الـ example. اكتشاف القاعدة الرياضية اللي ميتكسرش بياخد 4-6 ضعف وقت كتابة unit test تقليدي. لكنها بتغطّي مكان 80 unit test متفرّق وبتكشف حالات مكنتش هتفكر فيها أصلاً.
- Generators ضعيفة = اختبارات وهمية. لو generator بيولّد integers من 0 لـ 100 بس، الـ property test مش هيلاقي bug عند
Number.MAX_SAFE_INTEGER. fast-check بيدعم biased generators بتجرّب الـ edge cases (0, -1, MAX_INT) بنسبة أعلى تلقائياً، لكن لازم تتأكد إنك مش بتقصّر النطاق يدوياً. - الـ flakiness ممكنة. لو الـ test بيعتمد على ترتيب seed عشوائي، نفس الـ test ممكن يفشل مرة وينجح في الـ retry. fast-check بيطبع الـ seed مع كل failure؛ اعمل replay بـ
fc.assert(prop, { seed: 42 })لتثبيت الفشل ودراسته.
متى لا تستخدم Property-Based Testing
- كود UI خالص. خصائص الـ React component مش رياضية، أمثلة محددة بـ Playwright أو Testing Library بتغطّي أفضل وأسرع.
- Integration tests مع DB حقيقية. 10,000 run × استعلام DB = 4 ساعات pipeline. استخدم Property Testing على الـ pure business logic فقط، Integration على أمثلة محدودة.
- Scripts تشتغل مرة واحدة. الـ ROI ضعيف، اكتب 3 أمثلة واتركه.
الخطوة التالية
اختار أصعب pure function عندك (دالة حسابية، parser، formatter)، نزّل fast-check بـ npm i -D fast-check، واكتب property واحد بس على القاعدة الأساسية. مثلاً: JSON.parse(JSON.stringify(x)) لازم يساوي x لأي object صالح. شغّل بـ numRuns: 1000. لو لقى bug في أول 5 دقايق — وده اللي بيحصل في 70% من الحالات حسب تجربة فريق fast-check — هتعرف إن الاستثمار يستحق.
المصادر
- Claessen, K. & Hughes, J. — "QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs" (ICFP 2000)
- fast-check official documentation v3.19
- Jane Street Engineering Blog — "QuickCheck and Jane Street"
- fast-check repository — "Bugs found" trophies (280+ entries)
- Node.js Testing API — v22 documentation
- IEEE 754 — Standard for Floating-Point Arithmetic