مقدمه

به فصل «نوشتن تست‌های خودکار» خوش آمدید. صحت و درستی عملکرد کد، یک بخش حیاتی از مهندسی نرم‌افزار است. تست‌نویسی به ما اجازه می‌دهد تا به صورت خودکار و قابل تکرار، اطمینان حاصل کنیم که کدمان همان کاری را که از آن انتظار داریم، انجام می‌دهد. Rust با فراهم کردن یک فریم‌ورک تست داخلی و قدرتمند، تست‌نویسی را به بخش جدایی‌ناپذیری از فرآیند توسعه تبدیل کرده است. این قابلیت‌ها به ما کمک می‌کنند تا باگ‌ها را در مراحل اولیه پیدا کرده و با اطمینان بیشتری کد خود را تغییر داده یا بازسازی (refactor) کنیم.

آناتومی یک تابع تست

در ساده‌ترین حالت، یک تست در Rust یک تابع است که برای تأیید صحت عملکرد یک بخش از کد نوشته می‌شود. برای اینکه به کامپایلر Rust بگوییم یک تابع، یک تابع تست است، باید قبل از تعریف آن از اتریبیوت #[test] استفاده کنیم.

وقتی شما پروژه خود را با دستور cargo test اجرا می‌کنید، Cargo کدهای شما را در حالت تست کامپایل کرده و تمام توابعی را که با این اتریبیوت علامت‌گذاری شده‌اند، اجرا می‌کند. اگر تابع تست بدون panic کردن به پایان برسد، تست موفق (passed) تلقی می‌شود. اگر panic کند، تست شکست‌خورده (failed) در نظر گرفته می‌شود.

بررسی نتایج با ماکروهای assert!

برای بررسی اینکه آیا کد ما به درستی کار می‌کند یا نه، از ماکروهای ارائه شده توسط کتابخانه استاندارد استفاده می‌کنیم.

بررسی مقادیر بولی با assert!

ماکروی assert! یک آرگومان از نوع بولی می‌گیرد. اگر مقدار آرگومان true باشد، هیچ اتفاقی نمی‌افتد. اگر false باشد، ماکرو panic می‌کند و باعث شکست خوردن تست می‌شود. این ماکرو برای بررسی شرایطی که باید همیشه برقرار باشند، بسیار مفید است.

بررسی برابری با assert_eq! و assert_ne!

دو ماکروی assert_eq! و assert_ne! دو مقدار را با هم مقایسه می‌کنند. assert_eq! در صورتی panic می‌کند که دو مقدار برابر نباشند و assert_ne! در صورتی panic می‌کند که برابر باشند. این ماکروها یک مزیت بزرگ نسبت به assert! دارند: در صورت شکست، مقادیر دو طرف را در پیام خطا چاپ می‌کنند که دیباگ کردن را بسیار آسان‌تر می‌کند.

Copy Icon src/lib.rs
pub fn add_two(a: i32) -> i32 {
    a + 2
}

// This attribute indicates this is a test function
#[test]
fn it_adds_two() {
    assert_eq!(4, add_two(2));
}

#[test]
fn another() {
    assert_ne!(5, add_two(2));
}

در این مثال، دو تابع تست برای تابع add_two نوشته‌ایم. تست اول با assert_eq! بررسی می‌کند که آیا خروجی add_two(2) برابر با 4 است یا خیر. تست دوم با assert_ne! بررسی می‌کند که آیا خروجی نابرابر با 5 است.

بررسی panicها با #[should_panic]

علاوه بر بررسی مقادیر بازگشتی، گاهی اوقات می‌خواهیم تأیید کنیم که کد ما در شرایط خاصی، همانطور که انتظار می‌رود، panic می‌کند. برای این کار، می‌توانیم اتریبیوت #[should_panic] را به تابع تست خود اضافه کنیم. این کار باعث می‌شود که اگر کد داخل تابع panic کند، تست موفق و اگر panic نکند، تست شکست‌خورده تلقی شود.

همچنین می‌توانیم یک پارامتر expected به این اتریبیوت اضافه کنیم تا مطمئن شویم که پیام panic حاوی یک متن مشخص است.

Copy Icon src/lib.rs
pub struct Guess {
    value: i32,
}
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }
        Guess { value }
    }
}

#[test]
#[should_panic(expected = "Guess value must be between 1 and 100")]
fn greater_than_100() {
    Guess::new(200);
}

این تست تأیید می‌کند که فراخوانی Guess::new با یک مقدار خارج از محدوده، منجر به panic می‌شود. پارامتر expected تضمین می‌کند که panic دقیقاً به دلیلی که ما انتظار داریم رخ داده است.

استفاده از Result<T, E> در تست‌ها

نوشتن تست‌ها نباید همیشه به panic منجر شود. شما می‌توانید از Result<T, E> به عنوان نوع بازگشتی در توابع تست خود استفاده کنید. در این صورت، اگر تست شما Ok را برگرداند، موفق و اگر Err را برگرداند، شکست‌خورده تلقی می‌شود. این به شما اجازه می‌دهد تا از اپراتور ? در داخل تست‌های خود استفاده کرده و آنها را تمیزتر بنویسید.

Copy Icon src/lib.rs
#[test]
fn it_works() -> Result<(), String> {
    if 2 + 2 == 4 {
        Ok(())
    } else {
        Err(String::from("two plus two does not equal four"))
    }
}

در این درس با اصول اولیه نوشتن تست‌های خودکار در Rust آشنا شدیم. دیدیم که چگونه با استفاده از اتریبیوت #[test] و ماکروهای assert! می‌توانیم صحت عملکرد کدمان را بررسی کنیم. تست‌نویسی یک بخش ضروری از توسعه نرم‌افزار قابل اعتماد است و Rust ابزارهای درجه یکی برای آن فراهم می‌کند. در درس بعدی، به بررسی نحوه «اجرای تست‌ها» با Cargo و گزینه‌های مختلفی که برای کنترل خروجی و فیلتر کردن تست‌ها در اختیار ما قرار می‌دهد، خواهیم پرداخت.