مقدمه
در دنیای تست نرمافزار، تستها معمولاً به دو دسته اصلی تقسیم میشوند: «تستهای واحد» (Unit
Tests) و «تستهای یکپارچهسازی» (Integration Tests). هرچند هر دو نوع تست برای تضمین صحت عملکرد
کد نوشته میشوند، اما حوزه و هدف آنها متفاوت است. زبان Rust و ابزار Cargo دارای یک درک
داخلی از این تفاوت هستند و ساختار مشخصی را برای سازماندهی هر دو نوع تست فراهم میکنند.
تستهای واحد (Unit Tests)
هدف یک تست واحد، آزمایش یک بخش کوچک و ایزوله از کد (معمولاً یک تابع یا یک متد) به صورت مجزا از
سایر بخشهای برنامه است. این تستها به ما کمک میکنند تا اطمینان حاصل کنیم که هر واحد از کد به
تنهایی و همانطور که انتظار میرود، کار میکند.
ساختار و قرارداد
طبق قرارداد در Rust، تستهای واحد در یک ماژول به نام tests در داخل همان فایلی قرار
میگیرند که کد مورد آزمایش در آن تعریف شده است. این ماژول با دو اتریبیوت خاص علامتگذاری میشود:
- #[cfg(test)]: این اتریبیوت به کامپایلر Rust میگوید که کد داخل این ماژول را تنها
زمانی کامپایل کند که ما در حال اجرای تست هستیم (یعنی با cargo test). این کار تضمین میکند
که کدهای تست در بیلد نهایی و پروداکشن شما گنجانده نمیشوند.
- #[test]: این اتریبیوت که با آن آشنا هستیم، هر تابع داخل ماژول تست را به عنوان یک تست
مجزا علامتگذاری میکند.
همچنین، از آنجایی که تستهای واحد در یک ماژول فرزند قرار دارند، برای دسترسی به کد موجود در ماژول
والد، معمولاً از use super::*; در ابتدای ماژول تست استفاده میکنیم.
src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
}
این ساختار به ما اجازه میدهد تا تستها را در کنار کدی که آزمایش میکنند، نگه داریم که این کار
نگهداری و بهروزرسانی آنها را آسانتر میکند. همچنین، از آنجایی که تستها به اعضای خصوصی ماژول
والد خود دسترسی دارند، تست کردن جزئیات پیادهسازی داخلی نیز با این روش ممکن است.
تستهای یکپارچهسازی (Integration Tests)
هدف یک تست یکپارچهسازی، آزمایش این است که آیا بخشهای مختلف کتابخانه شما به درستی با یکدیگر کار
میکنند یا خیر. این تستها برخلاف تستهای واحد، کاملاً خارج از crate شما قرار دارند و فقط
میتوانند به API عمومی (public) آن دسترسی داشته باشند، دقیقاً مانند یک کاربر نهایی. این کار
تضمین میکند که شما به صورت تصادفی جزئیات پیادهسازی داخلی را که قرار نیست عمومی باشند، تست
نمیکنید.
ساختار و قرارداد
برای ایجاد تستهای یکپارچهسازی، ابتدا باید یک پوشه به نام tests در سطح ریشه پروژه خود
(کنار پوشه src) بسازید. Cargo به صورت خودکار میداند که باید هر فایل Rust داخل این
پوشه را به عنوان یک crate تست مجزا کامپایل و اجرا کند.
adder/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
هر فایل تست در پوشه tests باید crate شما را به عنوان یک وابستگی خارجی با استفاده
از `use` وارد کند تا بتواند به API عمومی آن دسترسی داشته باشد.
tests/integration_test.rs
use adder;
#[test]
fn it_adds_two_from_integration_test() {
assert_eq!(4, adder::add_two(2));
}
وقتی cargo test را اجرا میکنیم، Cargo ابتدا تستهای واحد شما را اجرا کرده و سپس هر
کدام از فایلهای تست یکپارچهسازی را به صورت جداگانه کامپایل و اجرا میکند. خروجی به صورت
بخشهای مجزا نمایش داده خواهد شد.
شما نیازی به افزودن اتریبیوت #[cfg(test)] به فایلهای داخل پوشه tests ندارید، زیرا
Cargo این پوشه را تنها زمانی کامپایل میکند که شما تستها را اجرا میکنید.
ماژولهای زیرمجموعه در تستهای یکپارچهسازی
با بزرگتر شدن مجموعه تستهای یکپارچهسازی، ممکن است بخواهید آنها را نیز سازماندهی کنید. هر فایل
در پوشه tests به عنوان یک crate مجزا کامپایل میشود. شما میتوانید با ایجاد یک
پوشه مشترک و قرار دادن کد کمکی در آن، بین تستهای مختلف خود کد به اشتراک بگذارید. برای مثال،
میتوانید یک فایل tests/common.rs ساخته و سپس در سایر فایلهای تست با mod common; از آن
استفاده کنید.
در این درس با رویکرد Rust به سازماندهی تستها و تفاوت بین تستهای واحد و یکپارچهسازی آشنا شدیم.
با این درس، فصل «نوشتن تستهای خودکار» به پایان میرسد. ما یاد گرفتیم که چگونه تست بنویسیم، آنها
را اجرا و کنترل کنیم، و در نهایت چگونه آنها را در یک ساختار منظم و مقیاسپذیر سازماندهی کنیم. در
فصل بعدی، به سراغ یک پروژه عملی خواهیم رفت و با «ساخت یک برنامه خط فرمان (CLI)»، تمام مفاهیمی که
تاکنون یاد گرفتهایم را به کار خواهیم بست.