البرمجة

اختبار الأكواد في لغة رست

كتابة الاختبارات في لغة رست (Rust): منهجية متكاملة لضمان الجودة والموثوقية

تُعد لغة رست (Rust) واحدة من أقوى اللغات الحديثة التي تم تصميمها لتقديم مزيج فريد من الأمان، الأداء العالي، وإدارة الذاكرة بدون الحاجة إلى جامع قمامة (Garbage Collector). وبينما يُشيد بها المطورون لتوفيرها أدوات قوية لتفادي الأخطاء المتعلقة بالذاكرة، فإن عملية تطوير البرامج الكبيرة والمعقدة تتطلب كذلك أساليب دقيقة لضمان صحة الوظائف وسلامة الأداء عبر الزمن، وتأتي كتابة الاختبارات (Testing) في طليعة هذه الأساليب.

أهمية الاختبارات في مشاريع Rust

الاختبارات في أي مشروع برمجي ليست ترفًا أو خطوة اختيارية، بل تشكّل حجر الزاوية في دورة تطوير البرمجيات الحديثة. في Rust، يتم تقديم منظومة اختبار قوية ومدمجة ضمن النظام، ما يجعلها أداة لا غنى عنها لضمان أن الأكواد تعمل كما هو متوقع، وأن التعديلات المستقبلية لا تؤدي إلى كسر السلوك السابق.

Rust يعتمد على مبدأ “البرمجة الآمنة افتراضيًا”، لكن هذا لا يمنع ظهور أخطاء منطقية أو خلل في السلوك المتوقع. لهذا، فإن الاختبارات تغطي الثغرات التي قد تمر أثناء الكتابة الأولية للكود.

البنية العامة للاختبارات في Rust

توفر لغة رست إطارًا داخليًا للاختبار يُعرف باسم test harness، ويأتي مفعلًا تلقائيًا عند استخدام أداة البناء cargo. وتبدأ كتابة الاختبارات بإضافة ملف أو وحدة تحتوي على الوظائف التي تُختبر باستخدام التعليمة #[test].

في ما يلي مثال بسيط على اختبار في Rust:

rust
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 3), 5); } }

في المثال أعلاه:

  • #[cfg(test)] تشير إلى أن وحدة الاختبارات يتم تضمينها فقط عند تشغيل الاختبارات.

  • #[test] تُستخدم لتحديد دالة اختبار.

  • assert_eq! هي ماكرو يُستخدم للتحقق من أن القيمتين متساويتان.

أنواع الاختبارات في Rust

1. الاختبارات الوحدوية (Unit Tests)

تهدف إلى اختبار وظائف أو وحدات محددة من الكود. تُكتب عادة في نفس الملف الذي يحتوي على الكود المُختبر ضمن وحدة فرعية mod tests. هذه النوعية من الاختبارات هي الأقرب إلى الكود، وتسمح بتحديد مكان الخطأ بدقة.

rust
#[test] fn test_subtraction() { assert_eq!(10 - 4, 6); }

2. الاختبارات التكاملية (Integration Tests)

تُكتب هذه الاختبارات لاختبار التفاعل بين وحدات متعددة، وغالبًا ما توضع في مجلد خاص يدعى tests/ داخل المشروع. كل ملف .rs داخل هذا المجلد يُعامل كوحدة اختبار تكاملي منفصلة.

rust
// في tests/integration_test.rs use my_crate::some_function; #[test] fn integration_test_case() { assert_eq!(some_function(), "Expected Output"); }

3. اختبارات الأداء (Benchmark Tests)

رغم أن هذه الميزة لا تزال في مراحل تجريبية في Rust، فإنها توفر إمكانيات مهمة لقياس أداء الكود. يمكن تفعيلها باستخدام #[bench] وتتطلب تمكين خاصية test في الملف Cargo.toml.

4. اختبارات التحمل (Property-Based Tests)

يمكن استخدام مكتبات خارجية مثل quickcheck لإجراء اختبارات تعتمد على الخصائص بدلاً من القيم المحددة، مما يزيد من القدرة على اكتشاف الأخطاء غير المتوقعة.

toml
# Cargo.toml [dev-dependencies] quickcheck = "1.0"
rust
#[cfg(test)] mod tests { use quickcheck::quickcheck; fn commutative_addition(a: i32, b: i32) -> bool { a + b == b + a } #[test] fn test_commutativity() { quickcheck(commutative_addition as fn(i32, i32) -> bool); } }

آليات التحقق داخل الاختبارات

Rust يوفّر عددًا من الماكروز المهمة لاستخدامها أثناء كتابة الاختبارات:

الماكرو الاستخدام
assert! التحقق من شرط معين بدون مقارنة.
assert_eq! التحقق من أن القيمتين متساويتان.
assert_ne! التحقق من أن القيمتين مختلفتان.
panic! لإطلاق حالة فشل يدوية داخل الاختبار.
should_panic لتعريف أن الاختبار يجب أن يفشل.
rust
#[test] #[should_panic] fn test_divide_by_zero() { let _ = 1 / 0; }

إدارة وتنسيق الاختبارات مع Cargo

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

الأمر الوظيفة
cargo test تشغيل جميع الاختبارات.
cargo test test_name تشغيل اختبار باسم معين.
cargo test -- --nocapture عرض الإخراج داخل الاختبارات.
cargo test --release تنفيذ الاختبارات في وضع الأداء العالي.

التحكم في الحالات المختلفة للاختبارات

يمكن تنفيذ اختبارات مشروطة باستخدام خاصية التكوين #[cfg(...)] لتحديد حالات خاصة يتم اختبارها فقط في بيئة معينة (مثلاً على نظام Windows فقط):

rust
#[cfg(target_os = "windows")] #[test] fn test_on_windows() { assert!(true); }

كتابة اختبارات منظمة: أفضل الممارسات

  1. عزل الاختبارات: يجب أن تكون كل دالة اختبار مستقلة تمامًا، وألا تعتمد على حالة خارجية أو تتغير نتائجها بحسب ترتيب التنفيذ.

  2. إعادة الاستخدام: يمكن تعريف إعدادات متكررة في دوال خاصة يتم استدعاؤها في كل اختبار لتفادي التكرار.

  3. تنظيم الإخراج: استخدام --nocapture يمكن أن يكون مفيدًا في فهم الأخطاء عند فشل اختبار، إذ يسمح بطباعة الإخراجات في وقت التنفيذ.

  4. مجموعات الاختبار: يمكن تقسيم الاختبارات باستخدام وحدات فرعية وعناوين منطقية لتسهل عملية التنظيم والقراءة.

  5. الاعتماديات الخارجية: يُفضّل استخدام مكتبات موثوقة ومدعومة جيدًا عند الحاجة إلى اختبارات متقدمة مثل المحاكاة أو اختبارات الخصائص.

مقارنة مع لغات أخرى

لغة Rust تتميّز بدمج إطار الاختبارات في نظام البناء والتجميع، على عكس العديد من اللغات الأخرى التي تتطلب تركيب مكتبات خارجية. كما أن النظام يعمل بتوافق كبير مع إدارة الحزم عبر cargo، مما يجعل كتابة وتشغيل الاختبارات جزءًا لا يتجزأ من عملية التطوير.

اللغة دعم الاختبارات الافتراضي الحاجة إلى مكتبة خارجية
Rust مدمج داخل cargo لا
Python محدود (unittest) نعم (pytest، nose…)
JavaScript لا نعم (jest، mocha…)
Go مدمج عبر testing لا غالبًا
C++ لا نعم (Google Test)

كتابة اختبارات للبرمجيات الواقعية

عند تطوير مكتبات أو خدمات RESTful، أو حتى برامج CLI، لا بد من كتابة اختبارات تغطي كل حالة منطقية، وحالات الحافة، وتعامل النظام مع الأخطاء. تُعتبر القدرة على محاكاة الحالات في Rust من خلال أدوات مثل mockall وproptest عاملاً محوريًا في تحسين جودة الاختبارات.

على سبيل المثال، عند بناء مكتبة مالية تقوم بعملية تحويل عملات، يمكن اختبار كل حالة محتملة:

  • التحويل من عملة A إلى B

  • محاولة تحويل عملة غير مدعومة

  • التحويل بمبلغ سلبي

  • التحويل بعملة بدون سعر صرف معرف

كل حالة منها تُكتب باختبار منفصل، يضمن أن الكود يتعامل معها بطريقة آمنة ومضبوطة.

التكامل المستمر (CI) واختبارات Rust

تكامل الاختبارات مع أنظمة التكامل المستمر مثل GitHub Actions أو GitLab CI/CD يضمن تشغيل كل اختبارات المشروع عند كل تحديث، مما يسمح بالكشف المبكر عن الأخطاء. باستخدام أمر cargo test ضمن ملفات YAML الخاصة بـ CI، يمكن توثيق فعالية الكود مع كل دمج جديد.

yaml
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable - name: Run tests run: cargo test --all

الجدول: مقارنة بين أنواع الاختبارات في Rust

النوع الموقع الهدف قابلية التوسع الأفضلية
Unit Tests داخل ملفات الكود اختبار وظائف صغيرة عالية في مراحل التطوير الأولية
Integration Tests في مجلد tests/ اختبار تكامل المكونات متوسطة إلى عالية عند إتمام الوحدات
Benchmarks مجلد خاص مع #[bench] قياس الأداء محدودة (تجريبية) في مراحل التحسين
Property-Based باستخدام quickcheck اختبار الخصائص الرياضية عالية جدًا للكود الرياضي أو الحساس

المصادر والمراجع