مقدمة: لماذا الاختبارات في Rust ليست مملة
إذا سبق لك كتابة اختبارات لكود بلغة Python أو JavaScript، فبالتأكيد واجهت موقفًا: طريقتك تستدعي API خارجيًا، أو تقرأ ملفًا من القرص، أو تصل إلى قاعدة بيانات. تشغيل مثل هذه الاختبارات في البيئة الحقيقية هو ألم. فهي بطيئة، وغير موثوقة (ماذا لو تعطل API؟)، وتتطلب إعداد البيئة.
في Rust، الوضع لا يختلف. لكن Rust هي لغة نظامية، وهنا يجب أن تكون الاختبارات سريعة وموثوقة ومحددة. ولهذا تحديدًا نحتاج إلى كائنات المحاكاة (mock-objects) والإعدادات المسبقة (fixtures).
في هذه المقالة، سنشرح كيفية كتابة اختبارات في Rust لا تعتمد على العالم الخارجي. ستتعلم كيفية استبدال التبعيات، وإعداد البيانات للاختبارات، والقيام بذلك بشكل أنيق. هيا بنا!
ما هي كائنات المحاكاة والإعدادات المسبقة؟
كائنات المحاكاة (Mocks)
كائن المحاكاة (Mock-object) هو محاكاة لمكون حقيقي من النظام. تقوم بإنشاء كائن يتصرف بالطريقة التي تحتاجها للاختبار: يعيد قيمًا محددة، أو يرمي أخطاء، أو يتحقق من أن طرقه قد تم استدعاؤها بالوسائط الصحيحة.
تخيل أن لديك خدمة إرسال بريد إلكتروني. في الاختبار، لا تريد إرسال رسائل حقيقية - أنت تستخدم محاكيًا (mock) يتحقق فقط من أن الخدمة قد تم استدعاؤها بالعنوان والنص الصحيحين.
الإعدادات المسبقة (Fixtures)
الإعدادات المسبقة (Fixtures) هي بيانات أو حالة مُعدة مسبقًا مطلوبة لتنفيذ الاختبار. على سبيل المثال، قاعدة بيانات اختبارية، ملف تكوين، هياكل محددة مسبقًا. في Rust، غالبًا ما يتم تنفيذ الإعدادات المسبقة من خلال دوال عادية تعيد كائنات جاهزة.
معًا، تمنحك المحاكيات والإعدادات المسبقة تحكمًا كاملاً في بيئة الاختبار. أنت تختبر فقط منطقك، وليس عمل الخدمات الخارجية.
المبادئ الأساسية للاختبار في Rust
قبل الغوص في المحاكيات، دعنا نتذكر كيف يتم تنظيم الاختبارات في Rust. يبدو اختبار الوحدة النمطي كالتالي:
#[cfg(test)]mod tests { use super::*;
#[test] fn test_addition() { assert_eq!(2 + 2, 4); }}كل شيء بسيط. ولكن عندما يبدأ الكود الخاص بك في الاعتماد على موارد خارجية (ملفات، طلبات HTTP، قواعد بيانات)، تظهر المشاكل. الحل هو عكس التبعيات واستخدام نمط حقن التبعية (Dependency Injection).
مثال عملي: خدمة تحميل البيانات
لنفترض أن لدينا هيكل DataLoader يقوم بتحميل البيانات من API خارجي ومعالجتها. نريد اختبار المعالجة دون استدعاء API الحقيقي.
أولاً، نحدد trait (واجهة) لمصدر البيانات:
pub trait DataSource { fn fetch_data(&self) -> Result<String, String>;}الآن، نقوم بتنفيذ مصدر حقيقي يقوم بطلب HTTP (مبسط):
pub struct RealDataSource { pub url: String,}
impl DataSource for RealDataSource { fn fetch_data(&self) -> Result<String, String> { // هنا سيكون طلب HTTP حقيقي Ok(format!("بيانات من {}", self.url)) }}والخدمة التي تستخدم هذا المصدر:
pub struct DataLoader<T: DataSource> { source: T,}
impl<T: DataSource> DataLoader<T> { pub fn new(source: T) -> Self { DataLoader { source } }
pub fn load_and_process(&self) -> Result<String, String> { let raw_data = self.source.fetch_data()?; // بعض المعالجة Ok(format!("تمت المعالجة: {}", raw_data)) }}لاحظ: DataLoader لا يعتمد على تنفيذ محدد لـ DataSource. إنه يعمل مع أي نوع ينفذ هذا الـ trait. هذا هو مفتاح قابلية الاختبار.
إنشاء كائن محاكاة (mock) يدويًا
أبسط طريقة هي إنشاء هيكل اختباري