تنظيم الاختبارات (Test Organization)
كما ذكرنا في بداية الفصل، فإن الاختبار (testing) هو تخصص معقد، ويستخدم أشخاص مختلفون مصطلحات وتنظيمات مختلفة. يفكر مجتمع Rust في الاختبارات من حيث فئتين رئيسيتين: اختبارات الوحدة (unit tests) واختبارات التكامل (integration tests). الـ unit tests صغيرة وأكثر تركيزًا، حيث تختبر وحدة واحدة (module) في عزلة في كل مرة، ويمكنها اختبار الواجهات الخاصة (private interfaces). أما الـ integration tests فهي خارجية تمامًا عن مكتبتك وتستخدم كودك بنفس الطريقة التي يستخدمه بها أي كود خارجي آخر، وذلك باستخدام الواجهة العامة (public interface) فقط، ومن المحتمل أن تختبر عدة وحدات في كل اختبار.
تعد كتابة كلا النوعين من الاختبارات أمرًا مهمًا لضمان أن أجزاء مكتبتك تقوم بما تتوقعه منها، بشكل منفصل ومعًا.
اختبارات الوحدة (Unit Tests)
الغرض من اختبارات الوحدة هو اختبار كل وحدة من الكود بمعزل عن بقية الكود لتحديد مكان عمل الكود وعدم عمله كما هو متوقع بسرعة. ستضع اختبارات الوحدة في دليل src في كل ملف مع الكود الذي تختبره. الاصطلاح المتبع هو إنشاء وحدة تسمى tests في كل ملف لاحتواء دوال الاختبار وتمييز الوحدة بـ cfg(test).
وحدة الـ tests و #[cfg(test)]
يخبر التعليق التوضيحي (annotation) المسمى #[cfg(test)] على وحدة tests لغة Rust بترجمة وتشغيل كود الاختبار فقط عندما تقوم بتشغيل cargo test وليس عند تشغيل cargo build. هذا يوفر وقت الترجمة عندما تريد فقط بناء المكتبة ويوفر مساحة في الأداة المترجمة الناتجة لأن الاختبارات لا يتم تضمينها. سترى أنه نظرًا لأن اختبارات التكامل تذهب إلى دليل مختلف، فهي لا تحتاج إلى تعليق #[cfg(test)]. ومع ذلك، نظرًا لأن اختبارات الوحدة تذهب في نفس الملفات مع الكود، فستستخدم #[cfg(test)] لتحديد أنه لا ينبغي تضمينها في النتيجة المترجمة.
تذكر أنه عندما أنشأنا مشروع adder الجديد في القسم الأول من هذا الفصل، قام Cargo بإنشاء هذا الكود لنا:
اسم الملف: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
في وحدة tests التي تم إنشاؤها تلقائيًا، ترمز السمة cfg إلى الإعداد (configuration) وتخبر Rust أن العنصر التالي يجب تضمينه فقط في حالة وجود خيار إعداد معين. في هذه الحالة، خيار الإعداد هو test والذي توفره Rust لترجمة وتشغيل الاختبارات. باستخدام سمة cfg يقوم Cargo بترجمة كود الاختبار الخاص بنا فقط إذا قمنا بتشغيل الاختبارات بنشاط باستخدام cargo test. يتضمن ذلك أي دوال مساعدة قد تكون داخل هذه الوحدة، بالإضافة إلى الدوال المميزة بـ #[test].
اختبار الدوال الخاصة (Private Function Tests)
هناك نقاش داخل مجتمع الاختبار حول ما إذا كان ينبغي اختبار الدوال الخاصة (private functions) مباشرة أم لا، وتجعل لغات أخرى من الصعب أو المستحيل اختبار الدوال الخاصة. بغض النظر عن أيديولوجية الاختبار التي تلتزم بها، فإن قواعد الخصوصية في Rust تسمح لك باختبار الدوال الخاصة. فكر في الكود في القائمة 11-12 مع الدالة الخاصة internal_adder.
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
لاحظ أن دالة internal_adder غير مميزة بـ pub. الاختبارات هي مجرد كود Rust، ووحدة tests هي مجرد وحدة أخرى. كما ناقشنا في “مسارات الإشارة إلى عنصر في شجرة الوحدات”، يمكن للعناصر في الوحدات التابعة استخدام العناصر في الوحدات الأصلية. في هذا الاختبار، نقوم بإحضار جميع العناصر التي تنتمي إلى أصل وحدة tests إلى النطاق (scope) باستخدام use super::* ومن ثم يمكن للاختبار استدعاء internal_adder. إذا كنت لا تعتقد أنه يجب اختبار الدوال الخاصة، فلا يوجد شيء في Rust يجبرك على القيام بذلك.
اختبارات التكامل (Integration Tests)
في Rust، تكون اختبارات التكامل خارجية تمامًا عن مكتبتك. فهي تستخدم مكتبتك بنفس الطريقة التي يستخدمها بها أي كود آخر، مما يعني أنها لا يمكنها سوى استدعاء الدوال التي تعد جزءًا من واجهة برمجة التطبيقات العامة (public API) لمكتبتك. غرضها هو اختبار ما إذا كانت أجزاء كثيرة من مكتبتك تعمل معًا بشكل صحيح. وحدات الكود التي تعمل بشكل صحيح بمفردها قد تواجه مشاكل عند دمجها، لذا فإن تغطية الاختبار للكود المتكامل مهمة أيضًا. لإنشاء اختبارات التكامل، تحتاج أولاً إلى دليل tests.
دليل الـ tests
ننشئ دليل tests في المستوى الأعلى من دليل مشروعنا، بجانب src. يعرف Cargo أنه يجب البحث عن ملفات اختبار التكامل في هذا الدليل. يمكننا بعد ذلك إنشاء أي عدد نريده من ملفات الاختبار، وسيقوم Cargo بترجمة كل ملف كـ crate فردية.
لننشئ اختبار تكامل. مع بقاء الكود في القائمة 11-12 في ملف src/lib.rs أنشئ دليل tests وأنشئ ملفًا جديدًا يسمى tests/integration_test.rs. يجب أن تبدو بنية دليلك هكذا:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
أدخل الكود الموجود في القائمة 11-13 في ملف tests/integration_test.rs.
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
كل ملف في دليل tests هو crate منفصلة، لذا نحتاج إلى إحضار مكتبتنا إلى نطاق كل test crate. لهذا السبب، نضيف use adder::add_two; في أعلى الكود، وهو ما لم نحتجه في اختبارات الوحدة.
لا نحتاج إلى تمييز أي كود في tests/integration_test.rs بـ #[cfg(test)]. يعامل Cargo دليل tests بشكل خاص ويقوم بترجمة الملفات في هذا الدليل فقط عندما نقوم بتشغيل cargo test. قم بتشغيل cargo test الآن:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
تتضمن الأقسام الثلاثة للمخرجات اختبارات الوحدة، واختبار التكامل، واختبارات التوثيق (doc tests). لاحظ أنه إذا فشل أي اختبار في قسم ما، فلن يتم تشغيل الأقسام التالية. على سبيل المثال، إذا فشل اختبار وحدة، فلن يكون هناك أي مخرجات لاختبارات التكامل والتوثيق، لأن تلك الاختبارات لن يتم تشغيلها إلا إذا نجحت جميع اختبارات الوحدة.
القسم الأول لاختبارات الوحدة هو نفسه الذي كنا نراه: سطر واحد لكل اختبار وحدة (واحد يسمى internal أضفناه في القائمة 11-12) ثم سطر ملخص لاختبارات الوحدة.
يبدأ قسم اختبارات التكامل بالسطر Running tests/integration_test.rs. بعد ذلك، هناك سطر لكل دالة اختبار في اختبار التكامل هذا وسطر ملخص لنتائج اختبار التكامل قبل بدء قسم Doc-tests adder.
كل ملف اختبار تكامل له قسمه الخاص، لذا إذا أضفنا المزيد من الملفات في دليل tests فسيكون هناك المزيد من أقسام اختبار التكامل.
لا يزال بإمكاننا تشغيل دالة اختبار تكامل معينة عن طريق تحديد اسم دالة الاختبار كوسيط لـ cargo test. لتشغيل جميع الاختبارات في ملف اختبار تكامل معين، استخدم الوسيط --test لـ cargo test متبوعًا باسم الملف:
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
يقوم هذا الأمر بتشغيل الاختبارات الموجودة في ملف tests/integration_test.rs فقط.
الوحدات الفرعية في اختبارات التكامل (Submodules in Integration Tests)
بينما تضيف المزيد من اختبارات التكامل، قد ترغب في إنشاء المزيد من الملفات في دليل tests للمساعدة في تنظيمها؛ على سبيل المثال، يمكنك تجميع دوال الاختبار حسب الوظيفة التي تختبرها. كما ذكرنا سابقًا، يتم ترجمة كل ملف في دليل tests كـ crate منفصلة خاصة به، وهو أمر مفيد لإنشاء نطاقات منفصلة لتقليد الطريقة التي سيستخدم بها المستخدمون النهائيون حزمتك بشكل أوثق. ومع ذلك، هذا يعني أن الملفات في دليل tests لا تتشارك في نفس السلوك الذي تتشاركه الملفات في src كما تعلمت في الفصل 7 بخصوص كيفية فصل الكود إلى وحدات وملفات.
يكون السلوك المختلف لملفات دليل tests أكثر وضوحًا عندما يكون لديك مجموعة من الدوال المساعدة لاستخدامها في ملفات اختبار تكامل متعددة، وتحاول اتباع الخطوات الواردة في قسم “فصل الوحدات إلى ملفات مختلفة” في الفصل 7 لاستخراجها إلى وحدة مشتركة. على سبيل المثال، إذا أنشأنا tests/common.rs ووضعنا دالة تسمى setup فيه، فيمكننا إضافة بعض الكود إلى setup الذي نريد استدعاءه من دوال اختبار متعددة في ملفات اختبار متعددة:
اسم الملف: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
عندما نشغل الاختبارات مرة أخرى، سنرى قسمًا جديدًا في مخرجات الاختبار لملف common.rs على الرغم من أن هذا الملف لا يحتوي على أي دوال اختبار ولم نستدعِ دالة setup من أي مكان:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
ظهور common في نتائج الاختبار مع عرض running 0 tests له ليس هو ما أردناه. أردنا فقط مشاركة بعض الكود مع ملفات اختبار التكامل الأخرى. لتجنب ظهور common في مخرجات الاختبار، بدلاً من إنشاء tests/common.rs سننشئ tests/common/mod.rs. يبدو دليل المشروع الآن هكذا:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
هذا هو اصطلاح التسمية القديم الذي تفهمه Rust أيضًا والذي ذكرناه في “مسارات الملفات البديلة” في الفصل 7. تسمية الملف بهذه الطريقة تخبر Rust بعدم معاملة وحدة common كملف اختبار تكامل. عندما ننقل كود دالة setup إلى tests/common/mod.rs ونحذف ملف tests/common.rs لن يظهر القسم في مخرجات الاختبار بعد الآن. الملفات الموجودة في الأدلة الفرعية لدليل tests لا يتم ترجمتها كـ crates منفصلة أو يكون لها أقسام في مخرجات الاختبار.
بعد أن أنشأنا tests/common/mod.rs يمكننا استخدامه من أي من ملفات اختبار التكامل كوحدة. إليك مثال على استدعاء دالة setup من اختبار it_adds_two في tests/integration_test.rs:
اسم الملف: tests/integration_test.rs
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
لاحظ أن إعلان mod common; هو نفسه إعلان الوحدة الذي عرضناه في القائمة 7-21. ثم، في دالة الاختبار، يمكننا استدعاء دالة common::setup().
اختبارات التكامل للحزم الثنائية (Integration Tests for Binary Crates)
إذا كان مشروعنا عبارة عن حزمة ثنائية (binary crate) تحتوي فقط على ملف src/main.rs ولا تحتوي على ملف src/lib.rs فلا يمكننا إنشاء اختبارات تكامل في دليل tests وإحضار الدوال المعرفة في ملف src/main.rs إلى النطاق باستخدام عبارة use. فقط حزم المكتبات (library crates) هي التي تعرض الدوال التي يمكن للحزم الأخرى استخدامها؛ الحزم الثنائية مخصصة للتشغيل بمفردها.
هذا هو أحد الأسباب التي تجعل مشاريع Rust التي توفر ملفًا ثنائيًا تمتلك ملف src/main.rs بسيطًا يستدعي المنطق الموجود في ملف src/lib.rs. باستخدام هذه البنية، يمكن لاختبارات التكامل اختبار حزمة المكتبة باستخدام use لجعل الوظائف المهمة متاحة. إذا كانت الوظائف المهمة تعمل، فإن الكمية الصغيرة من الكود في ملف src/main.rs ستعمل أيضًا، وهذه الكمية الصغيرة من الكود لا تحتاج إلى اختبار.
ملخص (Summary)
توفر ميزات الاختبار في Rust طريقة لتحديد كيفية عمل الكود لضمان استمراره في العمل كما تتوقع، حتى عند إجراء تغييرات. تختبر اختبارات الوحدة أجزاء مختلفة من المكتبة بشكل منفصل ويمكنها اختبار تفاصيل التنفيذ الخاصة. تتحقق اختبارات التكامل من أن أجزاء كثيرة من المكتبة تعمل معًا بشكل صحيح، وتستخدم واجهة برمجة التطبيقات العامة للمكتبة لاختبار الكود بنفس الطريقة التي سيستخدمه بها الكود الخارجي. على الرغم من أن نظام الأنواع وقواعد الملكية في Rust يساعدان في منع بعض أنواع الأخطاء، إلا أن الاختبارات لا تزال مهمة لتقليل الأخطاء المنطقية المتعلقة بكيفية توقع سلوك كودك.
دعنا نجمع المعرفة التي تعلمتها في هذا الفصل وفي الفصول السابقة للعمل على مشروع!