الاختبارات في Rust: ضمان جودة البرامج الصناعية قبل النشر
لماذا الاختبارات حاسمة في البرامج الصناعية
في المصنع، خطأ في كود المستشعر قد يعني:
- قراءة حرارة خاطئة ← تلف معدات بقيمة آلاف الدولارات
- إنذار لم يعمل ← إصابة عامل
- حساب معايرة خاطئ ← منتجات معيبة بالكامل
// هل هذه الدالة صحيحة؟ بدون اختبار لا نعرف
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
// خطأ بسيط: نسينا + 32 ← كل قراءات الحرارة خاطئة
fn celsius_to_fahrenheit_buggy(c: f64) -> f64 {
c * 9.0 / 5.0 // خطأ!
}
الاختبارات تكشف الأخطاء قبل أن تصل للمصنع.
اختبارات الوحدة مع #[test]
اختبارات الوحدة توضع في نفس الملف داخل وحدة tests:
/// تحويل قراءة خام من المستشعر إلى درجة مئوية
fn raw_to_celsius(raw: u16) -> f64 {
// المستشعر يُخرج 0-4095، يمثل -40°C إلى 125°C
let ratio = raw as f64 / 4095.0;
-40.0 + ratio * 165.0
}
/// تحقق من أن القراءة ضمن النطاق المقبول
fn is_valid_reading(celsius: f64) -> bool {
celsius >= -40.0 && celsius <= 125.0
}
#[cfg(test)] // يُجمَّع فقط عند الاختبار
mod tests {
use super::*; // استيراد كل دوال الوحدة الأصلية
#[test]
fn test_raw_zero_is_minimum() {
let temp = raw_to_celsius(0);
assert_eq!(temp, -40.0);
}
#[test]
fn test_raw_max_is_maximum() {
let temp = raw_to_celsius(4095);
// مقارنة أعداد عشرية بهامش خطأ
assert!((temp - 125.0).abs() < 0.01);
}
#[test]
fn test_midpoint_reading() {
let temp = raw_to_celsius(2048);
// تقريباً منتصف النطاق
assert!(temp > 40.0 && temp < 44.0);
}
#[test]
fn test_valid_range() {
assert!(is_valid_reading(25.0));
assert!(is_valid_reading(-40.0));
assert!(!is_valid_reading(-41.0));
assert!(!is_valid_reading(126.0));
}
}
شغّل الاختبارات: cargo test
اختبار حالات الخطأ
الأنظمة الصناعية يجب أن تتعامل مع المدخلات السيئة بأمان:
#[derive(Debug, PartialEq)]
enum CalibrationError {
DivisionByZero,
OutOfRange(f64),
InsufficientPoints,
}
fn calibrate(reference: &[f64], measured: &[f64]) -> Result<f64, CalibrationError> {
if reference.len() < 3 {
return Err(CalibrationError::InsufficientPoints);
}
let sum_measured: f64 = measured.iter().sum();
if sum_measured == 0.0 {
return Err(CalibrationError::DivisionByZero);
}
let sum_reference: f64 = reference.iter().sum();
let factor = sum_reference / sum_measured;
if factor < 0.5 || factor > 2.0 {
return Err(CalibrationError::OutOfRange(factor));
}
Ok(factor)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_calibration() {
let reference = vec![100.0, 200.0, 300.0];
let measured = vec![98.0, 196.0, 294.0];
let factor = calibrate(&reference, &measured).unwrap();
assert!((factor - 1.02).abs() < 0.01);
}
#[test]
fn test_insufficient_points() {
let result = calibrate(&[1.0, 2.0], &[1.0, 2.0]);
assert_eq!(result, Err(CalibrationError::InsufficientPoints));
}
#[test]
#[should_panic(expected = "يجب ألا تكون القائمة فارغة")]
fn test_empty_input_panics() {
// دالة أخرى تستخدم panic بدلاً من Result
validate_readings(&[]); // تُطلق panic إذا كانت فارغة
}
}
اختبارات التكامل في مجلد tests/
اختبارات التكامل تعيش في مجلد tests/ وتختبر تفاعل الوحدات معاً:
my_project/
├── src/
│ ├── sensor.rs
│ ├── alarm.rs
│ └── lib.rs
└── tests/
└── alarm_integration.rs ← اختبار تكامل
// tests/alarm_integration.rs
// يستورد المكتبة كمستخدم خارجي
use my_project::{Sensor, AlarmEngine, AlarmLevel};
#[test]
fn test_critical_temperature_triggers_alarm() {
let sensor = Sensor::new(1, "حرارة الفرن");
let mut engine = AlarmEngine::new();
engine.add_rule(sensor.id, 90.0, AlarmLevel::Critical);
// محاكاة قراءة عالية
let reading = sensor.simulate_reading(95.0);
let alarms = engine.evaluate(&reading);
assert_eq!(alarms.len(), 1);
assert_eq!(alarms[0].level, AlarmLevel::Critical);
}
#[test]
fn test_normal_reading_no_alarm() {
let sensor = Sensor::new(1, "حرارة الفرن");
let mut engine = AlarmEngine::new();
engine.add_rule(sensor.id, 90.0, AlarmLevel::Critical);
let reading = sensor.simulate_reading(70.0);
let alarms = engine.evaluate(&reading);
assert!(alarms.is_empty());
}
اختبارات التوثيق: توثيق يُجمَّع
أمثلة التوثيق في Rust تُنفَّذ كاختبارات — التوثيق لا يصبح قديماً أبداً:
/// تحويل ضغط من PSI إلى Bar
///
/// # أمثلة
///
/// ```
/// use my_project::psi_to_bar;
///
/// let bar = psi_to_bar(14.5);
/// assert!((bar - 1.0).abs() < 0.01);
///
/// // صفر يبقى صفراً
/// assert_eq!(psi_to_bar(0.0), 0.0);
/// ```
///
/// # ذعر
///
/// تُطلق panic إذا كانت القيمة سالبة:
/// ```should_panic
/// use my_project::psi_to_bar;
/// psi_to_bar(-1.0); // ضغط سالب غير ممكن فيزيائياً
/// ```
pub fn psi_to_bar(psi: f64) -> f64 {
if psi < 0.0 { panic!("ضغط سالب غير صالح"); }
psi * 0.0689476
}
شغّلها: cargo test --doc
تنظيم الاختبارات وأعلام cargo test
# تشغيل جميع الاختبارات
cargo test
# تشغيل اختبار محدد بالاسم
cargo test test_critical_temperature
# تشغيل اختبارات تحتوي كلمة "calibr"
cargo test calibr
# إظهار مخرجات println في الاختبارات
cargo test -- --nocapture
# تشغيل اختبارات الوحدة فقط (بدون التكامل)
cargo test --lib
# تشغيل اختبارات التكامل فقط
cargo test --test alarm_integration
# تشغيل الاختبارات على خيط واحد (مفيد لاختبارات تتشارك موارد)
cargo test -- --test-threads=1
مثال عملي: اختبار وحدة معايرة المستشعرات
pub struct CalibrationPoint {
pub reference: f64,
pub measured: f64,
}
pub struct CalibrationProfile {
points: Vec<CalibrationPoint>,
factor: Option<f64>,
}
impl CalibrationProfile {
pub fn new() -> Self {
Self { points: Vec::new(), factor: None }
}
pub fn add_point(&mut self, reference: f64, measured: f64) {
self.points.push(CalibrationPoint { reference, measured });
self.factor = None; // إعادة حساب مطلوبة
}
pub fn compute(&mut self) -> Result<f64, String> {
if self.points.len() < 3 {
return Err("نقاط غير كافية (الحد الأدنى 3)".into());
}
let sum_r: f64 = self.points.iter().map(|p| p.reference).sum();
let sum_m: f64 = self.points.iter().map(|p| p.measured).sum();
if sum_m.abs() < f64::EPSILON {
return Err("مجموع القياسات صفر".into());
}
let f = sum_r / sum_m;
self.factor = Some(f);
Ok(f)
}
pub fn apply(&self, raw: f64) -> Option<f64> {
self.factor.map(|f| raw * f)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_profile() -> CalibrationProfile {
let mut p = CalibrationProfile::new();
p.add_point(100.0, 98.0);
p.add_point(200.0, 196.0);
p.add_point(300.0, 294.0);
p
}
#[test]
fn test_compute_factor() {
let mut profile = sample_profile();
let factor = profile.compute().unwrap();
assert!((factor - 1.0204).abs() < 0.001);
}
#[test]
fn test_apply_correction() {
let mut profile = sample_profile();
profile.compute().unwrap();
let corrected = profile.apply(50.0).unwrap();
assert!(corrected > 50.0); // المعايرة ترفع القيمة قليلاً
}
#[test]
fn test_apply_before_compute() {
let profile = CalibrationProfile::new();
assert_eq!(profile.apply(50.0), None);
}
#[test]
fn test_not_enough_points() {
let mut profile = CalibrationProfile::new();
profile.add_point(100.0, 98.0);
assert!(profile.compute().is_err());
}
}
الخلاصة
- اختبارات الوحدة
#[test]تتحقق من كل دالة على حدة #[should_panic]يختبر أن الكود يرفض المدخلات الخاطئة- اختبارات التكامل في
tests/تتحقق من تفاعل الوحدات - اختبارات التوثيق تضمن أن الأمثلة في التوثيق تعمل فعلاً
cargo testمع أعلامه يوفر مرونة كاملة في تشغيل الاختبارات- في الدرس الأخير: نجمع كل ما تعلمناه في مشروع صناعي متكامل