Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

التحكم في كيفية تشغيل الاختبارات

تماماً كما يقوم الأمر cargo run بتصريف الكود الخاص بك ثم تشغيل الملف الثنائي (binary) الناتج، يقوم الأمر cargo test بتصريف الكود في وضع الاختبار (test mode) وتشغيل ملف الاختبار الثنائي الناتج. السلوك الافتراضي للملف الثنائي الناتج عن cargo test هو تشغيل جميع الاختبارات (tests) بالتوازي والتقاط المخرجات (output) الناتجة أثناء تشغيل الاختبارات، مما يمنع عرض المخرجات ويسهل قراءة المخرجات المتعلقة بنتائج الاختبار. ومع ذلك، يمكنك تحديد خيارات سطر الأوامر (command line options) لتغيير هذا السلوك الافتراضي.

تذهب بعض خيارات سطر الأوامر إلى cargo test وبعضها يذهب إلى ملف الاختبار الثنائي الناتج. لفصل هذين النوعين من الوسائط (arguments)، تسرد الوسائط التي تذهب إلى cargo test متبوعة بالفاصل -- ثم تلك التي تذهب إلى ملف الاختبار الثنائي. يعرض تشغيل cargo test --help الخيارات التي يمكنك استخدامها مع cargo test ويعرض تشغيل cargo test -- --help الخيارات التي يمكنك استخدامها بعد الفاصل. هذه الخيارات موثقة أيضاً في قسم “الاختبارات” في كتاب rustc.

تشغيل الاختبارات بالتوازي أو بالتتابع

عند تشغيل اختبارات متعددة، فإنها تعمل افتراضياً بالتوازي (in parallel) باستخدام الخيوط (threads)، مما يعني أنها تنتهي من العمل بسرعة أكبر وتحصل على التغذية الراجعة في وقت أقرب. نظراً لأن الاختبارات تعمل في نفس الوقت، يجب عليك التأكد من أن اختباراتك لا تعتمد على بعضها البعض أو على أي حالة مشتركة (shared state)، بما في ذلك البيئة المشتركة، مثل مجلد العمل الحالي أو متغيرات البيئة.

على سبيل المثال، لنفترض أن كل اختبار من اختباراتك يشغل كوداً ينشئ ملفاً على القرص باسم test-output.txt ويكتب بعض البيانات في ذلك الملف. ثم يقرأ كل اختبار البيانات الموجودة في ذلك الملف ويؤكد (asserts) أن الملف يحتوي على قيمة معينة، والتي تختلف في كل اختبار. نظراً لأن الاختبارات تعمل في نفس الوقت، فقد يقوم أحد الاختبارات بالكتابة فوق الملف في الوقت بين كتابة اختبار آخر للملف وقراءته له. سيفشل الاختبار الثاني حينها، ليس لأن الكود غير صحيح ولكن لأن الاختبارات تداخلت مع بعضها البعض أثناء تشغيلها بالتوازي. أحد الحلول هو التأكد من أن كل اختبار يكتب في ملف مختلف؛ وحل آخر هو تشغيل الاختبارات واحداً تلو الآخر.

إذا كنت لا ترغب في تشغيل الاختبارات بالتوازي أو إذا كنت تريد تحكماً أكثر دقة في عدد threads المستخدمة، يمكنك إرسال علم --test-threads وعدد threads التي تريد استخدامها إلى ملف الاختبار الثنائي. ألقِ نظرة على المثال التالي:

$ cargo test -- --test-threads=1

لقد قمنا بضبط عدد خيوط الاختبار (test threads) على 1 لإخبار البرنامج بعدم استخدام أي توازي (parallelism). سيستغرق تشغيل الاختبارات باستخدام خيط واحد وقتاً أطول من تشغيلها بالتوازي، لكن الاختبارات لن تتداخل مع بعضها البعض إذا كانت تشترك في الحالة.

إظهار مخرجات الدالة

افتراضياً، إذا نجح الاختبار، فإن مكتبة اختبار Rust تلتقط أي شيء يتم طباعته إلى المخرج القياسي (standard output). على سبيل المثال، إذا استدعينا println! في اختبار ونجح الاختبار، فلن نرى مخرجات println! في الطرفية (terminal)؛ سنرى فقط السطر الذي يشير إلى نجاح الاختبار. إذا فشل الاختبار، فسنرى كل ما تمت طباعته إلى standard output مع بقية رسالة الفشل.

كمثال، تحتوي القائمة 11-10 على دالة بسيطة تطبع قيمة معاملها وتعيد 10، بالإضافة إلى اختبار ينجح واختبار يفشل.

fn prints_and_returns_10(a: i32) -> i32 {
    println!("I got the value {a}");
    10
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn this_test_will_pass() {
        let value = prints_and_returns_10(4);
        assert_eq!(value, 10);
    }

    #[test]
    fn this_test_will_fail() {
        let value = prints_and_returns_10(8);
        assert_eq!(value, 5);
    }
}

عندما نشغل هذه الاختبارات باستخدام cargo test فسنرى المخرجات التالية:

$ cargo test
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

لاحظ أنه لا نرى في أي مكان في هذه المخرجات عبارة I got the value 4 التي تُطبع عند تشغيل الاختبار الذي ينجح. لقد تم التقاط تلك المخرجات. تظهر مخرجات الاختبار الذي فشل، I got the value 8 في قسم مخرجات ملخص الاختبار، والذي يوضح أيضاً سبب فشل الاختبار.

إذا أردنا رؤية القيم المطبوعة للاختبارات الناجحة أيضاً، يمكننا إخبار Rust بإظهار مخرجات الاختبارات الناجحة أيضاً باستخدام --show-output:

$ cargo test -- --show-output

عندما نشغل الاختبارات في القائمة 11-10 مرة أخرى باستخدام علم --show-output نرى المخرجات التالية:

$ cargo test -- --show-output
   Compiling silly-function v0.1.0 (file:///projects/silly-function)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok

successes:

---- tests::this_test_will_pass stdout ----
I got the value 4


successes:
    tests::this_test_will_pass

failures:

---- tests::this_test_will_fail stdout ----
I got the value 8

thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
  left: 10
 right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

تشغيل مجموعة فرعية من الاختبارات بالاسم

قد يستغرق تشغيل مجموعة اختبارات (test suite) كاملة وقتاً طويلاً في بعض الأحيان. إذا كنت تعمل على كود في منطقة معينة، فقد ترغب في تشغيل الاختبارات المتعلقة بهذا الكود فقط. يمكنك اختيار الاختبارات التي تريد تشغيلها عن طريق تمرير اسم أو أسماء الاختبارات التي تريد تشغيلها كـ argument إلى cargo test.

لتوضيح كيفية تشغيل مجموعة فرعية من الاختبارات، سننشئ أولاً ثلاثة اختبارات لدالة add_two الخاصة بنا، كما هو موضح في القائمة 11-11، ونختار أي منها سنشغل.

pub fn add_two(a: u64) -> u64 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_and_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }

    #[test]
    fn add_three_and_two() {
        let result = add_two(3);
        assert_eq!(result, 5);
    }

    #[test]
    fn one_hundred() {
        let result = add_two(100);
        assert_eq!(result, 102);
    }
}

إذا قمنا بتشغيل الاختبارات دون تمرير أي arguments كما رأينا سابقاً، فستعمل جميع الاختبارات بالتوازي:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok

test result: ok. 3 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

تشغيل اختبارات فردية

يمكننا تمرير اسم أي دالة اختبار إلى cargo test لتشغيل ذلك الاختبار فقط:

$ cargo test one_hundred
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

تم تشغيل الاختبار الذي يحمل الاسم one_hundred فقط؛ ولم يتطابق الاختباران الآخران مع هذا الاسم. تخبرنا مخرجات الاختبار أن لدينا المزيد من الاختبارات التي لم يتم تشغيلها من خلال عرض 2 filtered out في النهاية.

لا يمكننا تحديد أسماء اختبارات متعددة بهذه الطريقة؛ سيتم استخدام القيمة الأولى فقط المعطاة لـ cargo test. ولكن هناك طريقة لتشغيل اختبارات متعددة.

التصفية لتشغيل اختبارات متعددة

يمكننا تحديد جزء من اسم الاختبار، وسيتم تشغيل أي اختبار يتطابق اسمه مع تلك القيمة. على سبيل المثال، نظراً لأن اسمي اثنين من اختباراتنا يحتويان على add فيمكننا تشغيل هذين الاختبارين عن طريق تشغيل cargo test add:

$ cargo test add
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

قام هذا الأمر بتشغيل جميع الاختبارات التي تحتوي على add في الاسم وقام بتصفية الاختبار المسمى one_hundred. لاحظ أيضاً أن الوحدة (module) التي يظهر فيها الاختبار تصبح جزءاً من اسم الاختبار، لذا يمكننا تشغيل جميع الاختبارات في module عن طريق التصفية على اسم module.

تجاهل الاختبارات ما لم يطلب ذلك تحديداً

أحياناً قد يستغرق تنفيذ بعض الاختبارات المحددة وقتاً طويلاً جداً، لذا قد ترغب في استبعادها أثناء معظم عمليات تشغيل cargo test. بدلاً من إدراج جميع الاختبارات التي تريد تشغيلها كـ arguments، يمكنك بدلاً من ذلك تمييز الاختبارات التي تستغرق وقتاً طويلاً باستخدام سمة ignore لاستبعادها، كما هو موضح هنا:

اسم الملف: 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);
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // code that takes an hour to run
    }
}

بعد #[test] نضيف سطر #[ignore] إلى الاختبار الذي نريد استبعاده. الآن عندما نشغل اختباراتنا، سيعمل it_works ولكن لن يعمل expensive_test:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 1 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

يتم إدراج الدالة expensive_test على أنها متجاهلة (ignored). إذا أردنا تشغيل الاختبارات المتجاهلة فقط، يمكننا استخدام cargo test -- --ignored:

$ cargo test -- --ignored
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::expensive_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 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

من خلال التحكم في الاختبارات التي يتم تشغيلها، يمكنك التأكد من عودة نتائج cargo test بسرعة. عندما تصل إلى نقطة يكون فيها من المنطقي التحقق من نتائج الاختبارات المتجاهلة (ignored) ويكون لديك وقت لانتظار النتائج، يمكنك تشغيل cargo test -- --ignored بدلاً من ذلك. إذا كنت تريد تشغيل جميع الاختبارات سواء كانت متجاهلة أم لا، يمكنك تشغيل cargo test -- --include-ignored.