الاختبارات Tests هي مجموعة من دوال رست تتأكد من أن الشيفرة البرمجية الأساسية تعمل كما هو مطلوب منها، ويؤدي متن دوال الاختبار عادةً هذه العمليات الثلاث:
- تجهيز أي بيانات أو حالة ضرورية.
- تنفيذ الشيفرة البرمجية التي تريد اختبارها.
- التأكد من أن النتائج وفق المتوقع.
لننظر إلى المزايا التي توفرها رست لكتابة الاختبارات التي تؤدي العمليات الثلاث السابقة، ويتضمن ذلك السمة test
وبعض الماكرو والسمة should_panic
.
بنية دالة الاختبار
تُوصَّف دالة الاختبار في رست باستخدام السمة test
؛ والسمات attributes هي بيانات وصفية metadata تصف أجزاءً من شيفرة رست البرمجية، ومثال على هذه السمات هي السمة derive
التي استخدمناها مع الهياكل سابقًا. لتغيير دالة عادية إلى دالة اختبار نُضيف #[test]
قبل السطر الذي نكتب فيه fn
، إذ تبني رست ملف تنفيذ اختبار ثنائي عند تنفيذ الاختبارات باستخدام الأمر cargo test
، وتختبر الدوال المُشار إليها بأنها دوال اختبار وتعرض لك تقريرًا يوضح أي الدوال التي فشلت وأيها التي نجحت.
تُولَّد وحدة اختبار test module مع دالة اختبار تلقائيًا عندما نُنشئ مشروع مكتبة جديدة باستخدام كارجو Cargo، وتمنحنا هذه الوحدة قالبًا لكتابة الاختبارات التي نريد، بحيث لا يتوجب عليك النظر إلى هيكل الاختبار وطريقة كتابته كل مرة تُنشئ فيها مشروعًا جديدًا، ويمكنك إضافة دوال اختبار ووحدات اختبار إضافية قدر ما تشاء.
سننظر سويًا إلى بعض جوانب عمل الاختبارات بتجربة قالب الاختبار قبل اختبار أي شيفرة برمجية فعليًا، ثم سنكتب اختبارات واقعية تستدعي شيفرةً برمجيةً كتبناها لاحقًا وتتأكد من صحة سلوكها.
ننشئ مشروع مكتبة جديدة نسميه adder
يضيف رقمين إلى بعضهما:
$ cargo new adder --lib Created library `adder` project $ cd adder
يجب أن تبدو محتويات الملف src/lib.rs في مكتبة adder
كما هو موضح في الشيفرة 1.
اسم الملف: src/lib.rs
#[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }
الشيفرة 1: وحدة الاختبار والدالة المولّدة تلقائيًا باستخدام cargo new
لنتجاهل أول سطرين ونركّز على الدالةحاليًا. لاحظ التوصيف #[test]
: تُشير هذه السمة إلى أن هذه دالة اختبار، ما يعني أن منفّذ الاختبار سيعامل هذه الدالة على أنها اختبار، وقد تحتوي شيفرتنا البرمجية أيضًا على دوال ليست بدوال اختبار في وحدة tests
وذلك بهدف مساعدتنا لضبط حالات معينة، أو إجراء عمليات شائعة، لذا نحدّد دومًا فيما إذا كانت الدالة دالة اختبار.
يُستخدم متن الدالة في المثال الماكرو assert_eq!
للتأكد من أن result
تحتوي على القيمة 4 (وهي تحتوي على نتيجة جمع الرقم 2 مع 2). تمثّل عملية التأكد هذه عملية اختبارًا تقليديًا، دعنا ننفذ الاختبار لنرى إذا ما كان سينجح أم لا.
ينفِّذ الأمر cargo test
جميع الاختبارات الموجودة في المشروع كما هو موضح في الشيفرة 2.
$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.57s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_works ... 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
الشيفرة 2: الخرج الناتج عن عملية تنفيذ الاختبارات المولّدة تلقائيًا
يُصرّف كارجو الاختبار ويشغّله؛ إذ نجد في أحد السطور running 1 test
، ثم سطر يليه يوضح اسم دالة الاختبار المولّدة التي تدعى it_works
وأن نتيجة ذلك الاختبار هي ok
، وتعني النتيجة النهائية test result: ok.
أن جميع الاختبارات نجحت، بينما يشير الجزء 1 passed; 0 failed
إلى عدد الاختبارات الناجحة وعدد الاختبارات الفاشلة.
من الممكن تجاهل الاختبار بحيث لا يُنفّذ في حالات معينة وسنتكلم عن هذا الأمر لاحقًا، إلا أن ملخص نتيجة الاختبارات يوضح 0 ignored
لأننا لم نفعل ذلك هنا، كما يمكننا تمرير وسيط إلى الأمر cargo test
بحيث ينفذ الاختبارات التي يوافق اسمها السلسلة النصية ويدعى هذا بالترشيح filtering وسنتكلم عن هذا الموضوع لاحقًا. تظهر نهاية الملخص 0 filtered out
لأننا لم نستخدم الترشيح على الاختبارات التي ستُنفّذ.
يشير 0 measured
إلى الاختبارات المعيارية التي تقيس الأداء، إذ أن الاختبارات المعيارية benchmark tests متاحةٌ فقط في رست الليلية nightly Rust -في وقت كتابة هذه الكلمات- ويمكنك النظر إلى التوثيق المتعلق بالاختبارات المعيارية لتعلُّم المزيد.
يبدأ الجزء التالي من خرج الاختبار بالجملة Doc-tests adder
وهو نتيجة لأي من اختبارات التوثيق، إلا أنه لا توجد لدينا أي اختبارات توثيق حاليًا، لكن يمكن لرست تصريف أي مثال شيفرة برمجية موجودة في توثيق واجهتنا البرمجية API، وتساعدنا هذه الميزة بالمحافظة على التوثيق وشيفرتنا البرمجية على نحوٍ متوافق. سنناقش كيفية كتابة اختبارات التوثيق لاحقًا، وسنتجاهل قسم الخرج Doc-tests
حاليًا.
لنبدأ بتخصيص الاختبار ليوافق حاجتنا؛ إذ سنغيّر أولًا اسم الدالة it_works
إلى اسم مختلف مثل exploration
كما يلي:
اسم الملف: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } }
نشغِّل cargo test
مجددًا. يعرض لنا الخرج الآن exploration
بدلًا من it_works
:
$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.59s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::exploration ... 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
نضيف الآن اختبارًا جديدًا، إلا أننا سنجعل هذا الاختبار يفشل عمدًا، إذ تفشل الاختبارات عندما يهلع panic شيءٌ ما داخل دالة الاختبار. يُجرى كل اختبار ضمن خيط thread جديد وعندما يرى الخيط الرئيس أن خيط الاختبار قد "انتهى" يُعَلَّم الاختبار بأنه فشل.
تكلمنا سابقًا عن طرق أبسط لهلع الشيفرة البرمجية باستخدام الماكرو panic!
. الآن، نُدخل الاختبار الجديد مثل دالة تسمى another
إلى الملف src/lib.rs كما هو موضح في الشيفرة 3.
اسم الملف: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!("Make this test fail"); } }
الشيفرة 3: إضافة اختبار جديد يفشل لأننا نستدعي الماكرو panic!
شغِّل الاختبارات مجددًا باستخدام cargo test
، يجب أن يكون الخرج مشابهًا لما هو موجود في الشيفرة 4، وهو يوضّح أن اختبار exploration
نجح بينما فشل another
.
$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.72s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests test tests::another ... FAILED test tests::exploration ... ok failures: ---- tests::another stdout ---- thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::another 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'
الشيفرة 4: نتائج الاختبارات، إذ نجح اختبار وفشل آخر
يعرض السطر test tests::another
النتيجة FAILED
بدلًا من ok
، ويظهر لنا قسمين جديدين بين النتائج الفردية والملخص: إذ يعرض الأول السبب لفشل كل من الاختبارات بالتفصيل، وفي هذه الحالة نحصل على التفاصيل الخاصة بفشل another
وهي أن panicked at 'Make this test fail'
في السطر 10 ضمن الملف src/lib.rs؛ بينما يعرض القسم التالي أسماء جميع الاختبارات التي فشلت، وهي معلومة مفيدة في حال وجد لدينا العديد من الاختبارات مع العديد من التفاصيل لكل اختبار فشل. يمكن استخدام اسم الاختبار الذي فشل لتشغيل الاختبار وحده والحصول على معلومات أدق لتنقيح الأخطاء، وسنتكلم عن طرق تشغيل الاختبارات لاحقًا.
يعرض سطر الملخص في النهاية نتيجة الاختبارات كاملةً، إذ أن نتيجة الاختبار هي FAILED
ووجِد لدينا اختبارٌ نجح وآخر فشل.
الآن بعد أن تعرفنا إلى كيفية عرض نتائج الاختبار في حالات مختلفة، ننظر إلى ماكرو مختلفة عن panic!
مفيدة في الاختبارات.
التحقق من النتائج باستخدام الماكرو assert!
تُعد الماكرو assert!
الموجودة في المكتبة القياسية مفيدةً عندما تريد التأكد من أن شرطًا ما ضمن الاختبار يُقيَّم إلى true
، ونمرّر للماكرو assert!
وسيطًا يمكن تقييمه لقيمة بوليانية boolean؛ فإذا كانت القيمة true
لا يحدث شيء عندها ونجتاز الاختبار بنجاح؛ وإذا حصلنا على القيمة false
فهذا يعني فشل الاختبار، ويستدعي الماكرو assert!
عندها الماكرو panic!
. يساعدنا استخدام الماكرو assert!
في التحقق من شيفرتنا البرمجية بالطريقة التي نريدها.
استخدمنا في الأمثلة البرمجية سابقًا هيكلًا يدعى Rectangle
وتابع can_hold
، وتجد المثال مكررًا هنا في الشيفرة 5. دعنا نضع هذه الشيفرة البرمجية في الملف src/lib.rs ومن ثم نكتب بعض الاختبارات باستخدام الماكرو assert!
.
اسم الملف: src/lib.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } }
الشيفرة 5: استخدام الهيكل Rectangle
وتابعه can_hold
من مثال سابق
يمكن أن يُعيد التابع can_hold
قيمةً بوليانية، وهذا يعني أننا نستطيع استخدامه مع الماكرو assert!
. سنكتب اختبارًا لنتدرّب على التابع can_hold
بإنشاء نسخة Rectangle
في الشيفرة 6، وتحمل النسخة عرضًا بمقدار 8 وطولًا بمقدار 7، ومن ثم نتأكد من أنها تستطيع حمل hold نسخة Rectangle
أخرى بعرض 5 وطول 1.
اسم الملف: src/lib.rs
#[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } }
الشيفرة 6: اختبار للتابع can_hold
يتحقق فيما إذا كان المستطيل الكبير يتسع مستطيلًا أصغر
لاحظ أننا أضفنا سطرًا جديدًا داخل وحدة tests
ألا وهو use super::*;
، إذ تمثّل وحدة test
وحدةً اعتيادية تتبع قواعد الظهورالاعتيادية visibility rules التي ناقشناها سابقًا في مقال المسارات paths وشجرة الوحدة module tree في رست Rust، ولأن وحدة tests
هي وحدة داخلية inner module، فنحن بحاجة لإضافة الشيفرة البرمجية التي نريد إجراء الاختبار عليها في الوحدة الخارجية outer module إلى نطاق scope الوحدة الداخلية، ونستخدم هنا glob
بحيث يكون كل شيء نعرفه في الوحدة الخارجية متاحًا للوحدة tests
.
سمّينا الاختبار بالاسم larger_can_hold_smaller
وأنشأنا نسختين من الهيكل Rectangle
ومن ثم استدعينا الماكرو assert!
ومرّرنا النتيجة باستدعاء larger.can_hold(&smaller)
. يجب أن يُعيد هذا التعبير القيمة true
إذا اجتاز الاختبار بنجاح، دعنا نرى بأنفسنا.
$ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 1 test test tests::larger_can_hold_smaller ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
اجتاز الاختبار فعلًا. دعنا نضيف اختبارًا آخر بالتأكد من أن المستطيل الصغير لا يتسع داخل المستطيل الكبير:
اسم الملف: src/lib.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --snip-- let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); } }
لأن النتيجة الصحيحة من الدالة can_hold
هي false
في هذه الحالة، فنحن بحاجة لنفي النتيجة قبل أن نمررها إلى الماكرو assert!
وبالتالي سيجتاز الاختبار إذا أعادت can_hold
القيمة false
:
$ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests test tests::larger_can_hold_smaller ... ok test tests::smaller_cannot_hold_larger ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
اجتزنا اختبارين متتالين، مرحى لنا. دعنا نرى ما الذي سيحدث الآن لنتائج الاختبار إذا أضفنا خطأً برمجيًا عن عمد في شيفرتنا البرمجية؛ نفعل ذلك بالتغيير من كتابة متن التابع can_hold
باستبدال إشارة أكبر من إلى إشارة أصغر من عند المقارنة بين عرض كل من المستطيلين:
impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width < other.width && self.height > other.height } }
نحصل على التالي عند تنفيذ الاختبارات:
$ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests test tests::larger_can_hold_smaller ... FAILED test tests::smaller_cannot_hold_larger ... ok failures: ---- tests::larger_can_hold_smaller stdout ---- thread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::larger_can_hold_smaller 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'
تنبّهت اختباراتنا للخطأ، لأن larger.width
هو 8 و smaller.width
هو 5، والمقارنة بين العرضَين في can_hold
تُعيد النتيجة false
إذ أن 8 ليست أقل من 5.
اختبار المساواة باستخدام الماكرو assert_eq! و assert_ne!
نستخدم المساواة كثيرًا لاختبار البرنامج وذلك بين القيمة التي تعيدها الشيفرة البرمجية عند التنفيذ والقيمة التي تتوقع من الشيفرة البرمجية أن تعيدها، ويمكننا تحقيق ذلك باستخدام الماكرو assert!
وتمرير تعبير باستخدام العامل ==
، إلا أن هناك طريقة أخرى أكثر شيوعًا موجودة في المكتبة القياسية ألا وهي الماكرو assert_eq!
و assert_ne!
؛ إذ يقارن كلًا من الماكرو المذكورَين سابقًا القيمة للتحقق من المساواة أو عدم المساواة، كما أننا سنحصل على طباعة للقيمتين إذا فشل الاختبار، بينما يدلنا الماكرو assert!
فقط على حصوله على القيمة false
من التعبير ==
دون طباعة القيم التي أدت لحصولنا للقيمة false
في المقام الأول.
نكتب في الشيفرة 11 دالة تدعى add_two
تُضيف القيمة "2" إلى معاملها، ثم نفحص هذه الدالة باستخدام الماكرو assert_eq!
.
اسم الملف: src/lib.rs
pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } }
الشيفرة 7: اختبار الدالة add_two
باستخدام الماكرو assert_eq!
لنتحقّق من عمل الدالة:
$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::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
نمرّر القيمة "4" على أنها وسيط إلى الماكرو assert_eq!
وهي قيمة تساوي قيمة استدعاء add_two(2)
. السطر الخاص بالاختبار هو كالآتي:
test tests::it_adds_two ... ok
وتُشير ok
إلى أن نجاح الاختبار.
دعنا نضيف خطأً برمجيًا متعمّدًا على شيفرتنا البرمجية لنرى كيف ستكون رسالة فشل الاختبار باستخدام الماكرو assert_eq!
. لنغيّر من تطبيق add_two
لنضيف قيمة "3" بدلًا من "2":
pub fn add_two(a: i32) -> i32 { a + 3 }
نشغِّل الاختبارات مجددًا:
$ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
نجح الاختبار بالتعرف على الخطأ، إذ أن الاختبار it_adds_two
فشل وتخبرنا رسالة الخطأ أن سبب الفشل هو:
assertion failed: (left == right)
بالإضافة لتحديد قيمة كل من left
و right
، وتساعدنا رسالة الخطأ هذه بالبدء بعملية تنقيح الأخطاء، إذ أن قيمة left
هي "4" إلا أن right
-القيمة التي حصلنا عليها من add_two(2)
- هي "5". يصبح هذا الأمر مفيدًا جدًا عندما يكون لدينا الكثير من الاختبارات.
يُطلق على معامل التأكد من المساواة للتوابع في بعض لغات البرمجة وأطر العمل اسم expected
و actual
ويكون ترتيب تحديد المعاملات مهمًّا، إلا أنهما يحملان اسم left
و right
في رست ولا يهمّ ترتيبهما في عند تمريرهما للماكرو، إذ يمكننا كتابة التأكيد assertion في الاختبار بالشكل:
assert_eq!(add_two(2), 4)
الذي سينتج رسالة الخطأ ذاتها التي حصلنا عليها سابقًا، وهي:
assertion failed: (left == right)
ينجح الاختبار باستخدام الماكرو assert_ne!
إذا كانت القيمتين المُرّرتين له غير متساويتين، ويفشل في حال المساواة. نستفيد من هذا الماكرو عندما لا نعلم ماذا ستكون القيمة الناتجة تحديدًا إلا أننا نعلم أنها لا يجب أن تكون مساويةً لقيمة معينة؛ على سبيل المثال إذا كنا نختبر دالة يتغير دخلها بحسب يوم الأسبوع الذي ننفّذ فيه الاختبار، سيكون الأفضل في هذه الحالة هو التأكد من أن القيمة التي حصلنا عليها من خرج الدالة لا تساوي قيمة الدخل.
يُستخدم كل من الماكرو assert_eq!
و assert_ne!
كلًا من العامل ==
و =!
على الترتيب، وعندما تفشل عملية التأكيد، يطبع الماكرو معاملاته باستخدام تنسيق تنقيح الأخطاء، ما يعني أن القيم التي ستُقارن فيما بينها يجب أن تُطبّق السمتين PartialEq
و Debug
، وتطبق جميع الأنواع البدائية ومعظم أنواع المكتبة القياسية هاتين السمتين.
ستحتاج لتطبيق PartialEq
للهياكل والمعددات enums التي تعرّفها بنفسك للتأكد من مساواة النوعين، كما ستحتاج أيضًا لتطبيق Debug
لطباعة القيم عندما يفشل التأكيد. كلا من السمتين المذكورتين هي سمات قابلة للاشتقاق derivable كما ذكرنا سابقًا في الشيفرة 12 من فصل استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust، وهذا يعني أننا نستطيع إضافة الشيفرة التالية مباشرةً في تعريف الهيكل أو المعدّد:
#[derive(PartialEq, Debug)]
إضافة رسائل فشل مخصصة
يمكنك إضافة رسائل مخصّصة لطباعتها مع رسالة الخطأ مثل معامل اختياري للماكرو assert!
و assert_eq!
و assert_ne!
. تمرّر أي معاملات تُحدد بعد المعاملات المطلوبة للماكرو بدورها إلى الماكرو format!
(الذي ناقشناه في مقال تخزين لائحة من القيم باستخدام الأشعة Vectors)، بحيث يمكنك تمرير سلسلة نصية منسّقة تحتوي على {}
بمثابة موضع مؤقت والقيم بداخلها. الرسائل المخصصة مفيدة لتوثيق معنى التأكيد؛ إذ سيكون لديك فهم واضح عن المشكلة في الشيفرة البرمجية عندما يفشل الاختبار.
على سبيل المثال، دعنا نفترض وجود دالة تحيّي الناس بالاسم ونريد أن نختبر إذا كان الاسم الذي نمرّره إلى الدالة يظهر في الخرج:
اسم الملف: src/lib.rs
pub fn greeting(name: &str) -> String { format!("Hello {}!", name) } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol")); } }
لم يُتّفق على متطلبات هذا البرنامج بعد، ونحن متأكدون أن النص "Hello" في بداية نص التحية سيتغير لاحقًا، لذا قررنا أننا لا نريد أن نعدل النص عند تغيير المتطلبات، وتحققنا بدلًا من ذلك من المساواة بين القيمة المُعادة من الدالة greeting
للتأكد من أن الخرج يحتوي على النص الموجود في معامل الدخل.
لنضيف خطأ برمجي متعمد على الشيفرة البرمجية بتغيير greeting
بحيث لا تحتوي على الاسم لنرى ما ستكون نتيجة فشل الاختبار الافتراضي:
pub fn greeting(name: &str) -> String { String::from("Hello!") }
تكون نتيجة تشغيل الاختبار على النحو التالي:
$ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test test tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ---- thread 'main' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
تشير النتيجة إلى أن التأكيد فشل في السطر الذي يحتويه، إلا أن رسالةً أكثر إفادة ستحتوي على القيمة الموجودة في دالة greeting
. نضيف رسالة فشل مخصصة مؤلفة من سلسلة نصية منسّقة تحتوي على موضع مؤقت يُملأ بالقيمة الفعلية التي نحصل عليها من الدالة greeting
:
#[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!( result.contains("Carol"), "Greeting did not contain name, value was `{}`", result ); }
نحصل عندما نشغّل الاختبار الآن على رسالة خطأ أكثر وضوحًا:
$ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.93s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test test tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ---- thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
يمكننا استخدام القيمة التي حصلنا عليها من خرج الاختبار مما سيساعدنا في تنقيح الخطأ الذي تسبب بذلك بدلًا من الفعل الذي توقعناه من الدالة.
التحقق من حالات الهلع باستخدام should_panic
من المهم بالإضافة للتحقق من القيم المعادة التحقق من أن شيفرتنا البرمجية تتعامل مع حالات الخطأ كما نتوقع، على سبيل المثال تذكر النوع Guess
الذي أنشأناه في أمثلة سابقة في مقال الأخطاء والتعامل معها، إذ استخدمت بعض الشيفرات البرمجية Guess
واعتمدت على أن نسخ Guess
ستحتوي على قيمة تترواح بين 1 و100. يمكننا كتابة اختبار يتسبب بحالة هلع إذا حاولنا إنشاء نسخة من Guess
خارج المجال، وذلك عن طريق إضافة سمة should_panic
إلى دالة الاختبار، وينجح الاختبار إذا كانت الشيفرة البرمجية داخل الدالة تهلع، وإلا يفشل الاختبار في حالة عدم هلع الدالة.
توضح الشيفرة 8 اختبارًا يتأكد أن حالات الخطأ المتعلقة بالدالة Guess::new
تحصل عندما نتوقع حصولها.
اسم الملف: 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 } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); } }
الشيفرة 8: اختبار أن حالة ما ستتسبب بالهلع panic!
نضع السمة #[should_panic]
بعد السمة #[test]
وقبل دالة الاختبار التي نريد تطبيق السمة عليها.دعنا نلقي نظرةًعلى النتيجة بعد نجاح الاختبار:
$ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests guessing_game running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
يبدو أن الأمر على ما يرام. لنضيف خطأ برمجي عن عمد إلى شيفرتنا البرمجية بإزالة الشرط الذي يتسبب بهلع الدالة new
إذا كانت القيمة أكبر من 100:
impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } }
يفشل الاختبار في الشيفرة 8 عندما نشغّله:
$ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- note: test did not panic as expected failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
لا نحصل على رسالة مفيدة جدًافي هذه الحالة، لكن إذا نظرنا إلى دالة الاختبار نرى توصيفها بالتالي: #[should_panic]
. حالة الفشل التي حصلنا عليها تعني أن الشيفرة البرمجية في دالة الاختبار لم تتسبب بحالة هلع.
يمكن أن تصبح الاختبارات التي تستخدم should_panic
غير دقيقة، إذ ينجح اختبار should_pass
في حال هلع الاختبار لسبب مختلف عمّا كنا متوقعين، ولجعل اختبارات should_panic
أكثر دقة، يمكننا إضافة معامل اختياري يدعى expected
للسمة should_panic
،وسيتأكد عندها الاختبار من أن رسالة الخطأ تحتوي على النص الذي زوّدناه به. على سبيل المثال، لنفترض أن الشيفرة البرمجية الخاصة بالدالة Guess
في الشيفرة 9 احتوت على هلع الدالة new
برسائل مختلفة بحسب إذا ما كانت القيمة صغيرة أو كبيرة.
اسم الملف: src/lib.rs
impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic(expected = "less than or equal to 100")] fn greater_than_100() { Guess::new(200); } }
الشيفرة 9: اختبار panic!
برسالة هلع تحتوي على سلسلة نصية جزئية محددة
سينجح الاختبار لأن القيمة التي نضعها في معامل expected
الخاص بالسمة should_panic
هي سلسلة نصية جزئية للرسالة التي تعرضها الدالة Guess::new
عند الهلع. يمكننا تحديد كامل رسالة الهلع التي نتوقعها، وستكون في هذه الحالة:
Guess value must be less than or equal to 100, got 200.
يعتمد ما تختار أن تحدده على رسالة الهلع إذا ما كانت مميزة أو ديناميكية ودقة الاختبار التي تريدها، وتُعد سلسلةً نصيةً جزئيةً لرسالة الهلع كافية في هذه الحالة للتأكد من أن الشيفرة البرمجية في دالة الاختبار تنفّذ حالة else if value > 100
.
دعنا نضيف خطأ برمجي جديد مجددًا لرؤية ما الذي سيحصل للاختبار should_panic
باستخدام رسالة expected
وذلك بتبديل محتوى الكتلة if value < 1
مع else if value > 100
:
if value < 1 { panic!( "Guess value must be less than or equal to 100, got {}.", value ); } else if value > 100 { panic!( "Guess value must be greater than or equal to 1, got {}.", value ); }
يفشل الاختبار should_panic
هذه المرة عند تشغيله:
$ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test test tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ---- thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace note: panic did not contain expected string panic message: `"Guess value must be greater than or equal to 1, got 200."`, expected substring: `"less than or equal to 100"` failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'
تشير رسالة الخطأ إلى أن هذا الاختبار هلع فعلًا كما توقعنا إلا أن رسالة الهلع لم تتوافق مع السلسلة النصية الجزئية:
'Guess value must be less than or equal to 100'
إذ حصلنا على رسالة الهلع التالية في هذه الحالة:
Guess value must be greater than or equal to 1, got 200.
لنبدأ الآن بالبحث عن الخطأ.
استخدام Result في الاختبارات
تهلع جميع اختباراتنا لحد هذه اللحظة عند الفشل، إلا أنه يمكننا كتابة اختبارات تستخدم Result<T, E>
. إليك اختبارًا من الشيفرة 1 إذ أعدنا كتابته باستخدام Result<T, E>
ليعيد Err
بدلًا من الهلع:
#[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } } }
للدالة it_works
الآن النوع المُعاد Result<(), String>
، ونُعيد Ok(())
عندما ينجح الاختبار و Err
مع Sring
بداخله عندما يفشل وذلك بدلًا من استدعاء الماكرو assert_eq!
.
تمكّنك كتابة الاختبارات بحيث تعيد Result<T, E>
من استخدام عامل إشارة الاستفهام في محتوى الاختبار بحيث تكون وسيلةً ملائمةً لكتابة الاختبارات التي يجب أن تفشل إذا أعادت أي عملية داخلها المتغاير Err
.
لا يمكنك استخدام التوصيف #[should_panic]
على الاختبارات التي تستخدم Result<T, E>
. لا تستخدم عامل إشارة الاستفهام على Result<T, E>
للتأكد من أن عملية ما تُعيد المتغاير Err
بل استخدم assert!(value.is_err())
بدلًا من ذلك.
الآن وبعد أن تعلمنا الطرق المختلفة لكتابة الاختبارات، حان الوقت لننظر إلى ما يحدث عندما ننفذ الاختبارات ونكتشف الخيارات المختلفة التي يمكننا استخدامها مع cargo test
في المقال التالي.
ترجمة -وبتصرف- لقسم من الفصل Writing Automated Tests من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.