مقدمة في اختبار التكامل لـ Rust باستخدام Docker
\nاختبار التكامل هو مرحلة أساسية في تطوير تطبيقات الخادم الموثوقة بلغة Rust. على عكس اختبارات الوحدة، تختبر اختبارات التكامل تفاعل المكونات مع التبعيات الحقيقية: قواعد البيانات، التخزين المؤقت، واجهات API الخارجية. يتيح استخدام Docker عزل بيئة الاختبار وضمان قابلية التكرار. في هذه المقالة، سنستعرض تقنيات متقدمة: التشغيل المتوازي للحاويات، إدارة الحالة، خدمات المحاكاة (mock-services)، والعمل مع البرمجة غير المتزامنة عبر Tokio.
\n\nإعداد بيئة الاختبار: Docker و testcontainers
\nلاختبارات التكامل في Rust، من المناسب استخدام الكريت testcontainers. يسمح بتشغيل حاويات Docker برمجياً، وانتظار جاهزيتها، وتنظيف الموارد بشكل صحيح. لنبدأ بالإعداد الأساسي.
تثبيت التبعيات
\nأضف إلى Cargo.toml:
[dev-dependencies]\ntestcontainers = "0.21"\ntokio = { version = "1", features = ["full"] }\nreqwest = { version = "0.12", features = ["json"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\nمثال أساسي: حاوية PostgreSQL
\nلننشئ اختباراً يقوم بتشغيل PostgreSQL، وينفذ الترحيل (migration)، ويتحقق من إدراج البيانات:
\nuse testcontainers::{clients, images::postgres::Postgres, Container};\nuse tokio_postgres::{NoTls, Client};\n\nasync fn setup_db() -> (Client, Container<'static, Postgres>) {\n let docker = clients::Cli::default();\n let pg_image = Postgres::default();\n let container = docker.run(pg_image);\n let host = container.get_host();\n let port = container.get_host_port_ipv4(5432);\n let connection_string = format!(\n "host={host} port={port} user=postgres dbname=postgres",\n );\n let (client, connection) = tokio_postgres::connect(&connection_string, NoTls)\n .await\n .expect("فشل الاتصال");\n tokio::spawn(async move {\n if let Err(e) = connection.await {\n eprintln!("خطأ في الاتصال: {e}");\n }\n });\n (client, container)\n}\n\n#[tokio::test]\nasync fn test_insert_user() {\n let (client, _container) = setup_db().await;\n client\n .simple_query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)")\n .await\n .unwrap();\n client\n .execute("INSERT INTO users (name) VALUES ($1)", &[&"أليس"])\n .await\n .unwrap();\n let rows = client.query("SELECT name FROM users", &[]).await.unwrap();\n assert_eq!(rows.len(), 1);\n assert_eq!(rows[0].get::<_, String>(0), "أليس");\n}\nسيتم حذف الحاوية تلقائياً بعد انتهاء الاختبار بفضل المدمر (destructor) الخاص بـ Container. هذا يلغي الحاجة إلى التنظيف اليدوي.
التشغيل المتوازي للاختبارات وإدارة الموارد
\nبشكل افتراضي، ينفذ Rust الاختبارات في خيوط متوازية. إذا قام كل اختبار بتشغيل حاويته الخاصة، فقد يؤدي ذلك إلى تعارض في المنافذ وزيادة تحميل Docker. سننظر في نهجين.
\nاستخدام منافذ مختلفة عبر التخصيص الديناميكي
\nيقوم كريت testcontainers تلقائياً بتعيين منافذ حرة على المضيف. هذا يحل مشكلة التصادمات، لكنه يزيد من استهلاك الموارد. للحاويات الثقيلة (مثل قواعد البيانات)، من الأفضل استخدام مثيل مشترك:
use once_cell::sync::Lazy;\nuse testcontainers::clients;\n\nstatic DOCKER: Lazy<clients::Cli> = Lazy::new(|| clients::Cli::default());\n\nasync fn get_shared_postgres() -> (tokio_postgres::Client, Container<'static, Postgres>) {\n let pg_image = Postgres::default();\n let container = DOCKER.run(pg_image);\n // ... الاتصال كما في المثال السابق\n (client, container)\n}\nومع ذلك، هذا النهج خطير: قد تؤثر الاختبارات على بعضها البعض من خلال الحالة المشتركة. يُوصى باستخدام قواعد بيانات منفصلة داخل حاوية واحدة أو إعادة تعيين الحالة