مقدمه

تاکنون، ما ابتدا منطق برنامه را نوشته و سپس برای آن تست‌ها را اضافه کرده‌ایم. اما یک رویکرد توسعه نرم‌افزار به نام «توسعه‌ی تست‌محور» یا Test-Driven Development (TDD) این فرآیند را برعکس می‌کند. در TDD، چرخه توسعه به صورت زیر است:

  1. یک تست می‌نویسیم که عملکرد مورد نظر را توصیف می‌کند و آن را اجرا می‌کنیم. تست به دلیل عدم وجود کد اصلی، شکست می‌خورد.
  2. حداقل کد لازم را می‌نویسیم تا تست با موفقیت پاس شود.
  3. کد نوشته شده را بازسازی (refactor) می‌کنیم تا تمیز و بهینه شود، و در هر مرحله با اجرای مجدد تست‌ها از صحت عملکرد آن اطمینان حاصل می‌کنیم.

این رویکرد به ما کمک می‌کند تا طراحی تمیزتری داشته باشیم و همیشه از وجود پوشش تستی برای تمام قابلیت‌های برنامه مطمئن باشیم. در این درس، ما منطق جستجوی برنامه minigrep خود را با استفاده از TDD پیاده‌سازی خواهیم کرد.

نوشتن یک تست شکست‌خورده

بیایید با نوشتن یک تست برای تابع جستجوی آینده‌مان شروع کنیم. این تابع باید یک query و متنی که باید در آن جستجو شود را دریافت کرده و لیستی (یک وکتور) از خطوطی که حاوی آن query هستند را برگرداند.

ما این تست را در فایل src/lib.rs اضافه می‌کنیم، زیرا منطق اصلی برنامه ما در این Crate کتابخانه‌ای قرار خواهد گرفت.

Copy Icon src/lib.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

اگر اکنون cargo test را اجرا کنیم، این تست با خطا کامپایل نمی‌شود، زیرا تابع search هنوز وجود ندارد. این اولین مرحله از چرخه TDD است.

نوشتن کد برای پاس کردن تست

حالا باید حداقل کد لازم را برای پاس شدن این تست بنویسیم. ما یک تابع search تعریف می‌کنیم که دو اسلایس رشته‌ای (&str) به عنوان ورودی گرفته و یک Vec<&str> را برمی‌گرداند.

Copy Icon src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

در این تابع، ما از متد .lines() برای پیمایش روی خطوط متن ورودی استفاده می‌کنیم. برای هر خط، با متد .contains() بررسی می‌کنیم که آیا حاوی query است یا خیر. اگر بود، آن خط را به وکتور نتایج اضافه می‌کنیم.

نکته مهم در اینجا، استفاده از پارامتر lifetime به نام 'a است. ما به کامپایلر می‌گوییم که رفرنس‌های رشته‌ای که در وکتور بازگشتی قرار دارند، باید حداقل به اندازه اسلایس رشته‌ای contents که به تابع پاس داده شده، عمر کنند. این کار تضمین می‌کند که ما یک رفرنس آویزان برنمی‌گردانیم.

حالا اگر cargo test را اجرا کنیم، تست ما با موفقیت پاس خواهد شد.

استفاده از تابع جدید در main

آخرین قدم، استفاده از تابع search جدید در تابع run برنامه ماست.

Copy Icon src/main.rs
// In run function in main.rs
fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in minigrep::search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

اکنون برنامه ما کامل شده است! ما یک منطق جستجوی کاربردی داریم که با استفاده از رویکرد TDD توسعه داده شده و توسط یک تست واحد پشتیبانی می‌شود.

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