টেস্ট অর্গানাইজেশন
এই অধ্যায়ের শুরুতে যেমনটি উল্লেখ করা হয়েছে, টেস্টিং একটি জটিল বিষয় এবং বিভিন্ন মানুষ বিভিন্ন পরিভাষা ও সংগঠন ব্যবহার করে। Rust কমিউনিটি টেস্টগুলোকে প্রধানত দুটি ভাগে ভাগ করে: ইউনিট টেস্ট (unit tests) এবং ইন্টিগ্রেশন টেস্ট (integration tests)। ইউনিট টেস্ট ছোট এবং বেশি ফোকাসড হয়, যা একবারে একটি মডিউলকে আলাদাভাবে পরীক্ষা করে এবং প্রাইভেট ইন্টারফেসও (private interfaces) পরীক্ষা করতে পারে। ইন্টিগ্রেশন টেস্ট আপনার লাইব্রেরির সম্পূর্ণ বাইরে থাকে এবং আপনার কোডকে অন্য যেকোনো এক্সটার্নাল কোডের মতোই ব্যবহার করে, শুধুমাত্র পাবলিক ইন্টারফেস ব্যবহার করে এবং প্রতিটি টেস্টে একাধিক মডিউল পরীক্ষা করতে পারে।
আপনার লাইব্রেরির অংশগুলো আলাদাভাবে এবং একসঙ্গে প্রত্যাশা অনুযায়ী কাজ করছে কিনা তা নিশ্চিত করার জন্য উভয় প্রকারের টেস্ট লেখাই গুরুত্বপূর্ণ।
ইউনিট টেস্ট
ইউনিট টেস্টের উদ্দেশ্য হলো কোডের প্রতিটি ইউনিটকে বাকি কোড থেকে বিচ্ছিন্নভাবে পরীক্ষা করা, যাতে কোডের কোথায় প্রত্যাশা অনুযায়ী কাজ করছে এবং কোথায় করছে না তা দ্রুত চিহ্নিত করা যায়। আপনি ইউনিট টেস্টগুলোকে src ডিরেক্টরিতে প্রতিটি ফাইলের মধ্যে রাখবেন, যে কোডটি তারা পরীক্ষা করছে তার সাথে। প্রচলিত নিয়ম হলো, টেস্ট ফাংশনগুলো রাখার জন্য প্রতিটি ফাইলে tests
নামে একটি মডিউল তৈরি করা এবং মডিউলটিকে cfg(test)
দিয়ে অ্যানোটেট করা।
tests মডিউল এবং #[cfg(test)]
tests
মডিউলের উপর #[cfg(test)]
অ্যানোটেশনটি Rust-কে বলে যে শুধুমাত্র cargo test
চালানোর সময় টেস্ট কোড কম্পাইল এবং রান করতে হবে, cargo build
চালানোর সময় নয়। এটি কম্পাইলের সময় বাঁচায় যখন আপনি কেবল লাইব্রেরি তৈরি করতে চান এবং ফলে তৈরি হওয়া কম্পাইল্ড আর্টিফ্যাক্টে জায়গা বাঁচায় কারণ টেস্টগুলো অন্তর্ভুক্ত থাকে না। আপনি দেখবেন যে ইন্টিগ্রেশন টেস্টগুলো একটি ভিন্ন ডিরেক্টরিতে যাওয়ায় তাদের #[cfg(test)]
অ্যানোটেশনের প্রয়োজন হয় না। তবে, যেহেতু ইউনিট টেস্টগুলো কোডের সাথে একই ফাইলে থাকে, তাই কম্পাইল্ড ফলাফলে সেগুলো অন্তর্ভুক্ত করা উচিত নয় তা নির্দিষ্ট করতে আপনি #[cfg(test)]
ব্যবহার করবেন।
স্মরণ করুন, এই অধ্যায়ের প্রথম বিভাগে যখন আমরা নতুন adder
প্রজেক্ট তৈরি করেছিলাম, Cargo আমাদের জন্য এই কোডটি তৈরি করেছিল:
Filename: 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]
দিয়ে অ্যানোটেট করা ফাংশনগুলো ছাড়াও এই মডিউলের মধ্যে থাকা যেকোনো সাহায্যকারী ফাংশন অন্তর্ভুক্ত থাকে।
প্রাইভেট ফাংশন টেস্ট করা
টেস্টিং কমিউনিটিতে প্রাইভেট ফাংশন সরাসরি পরীক্ষা করা উচিত কিনা তা নিয়ে বিতর্ক রয়েছে, এবং অন্যান্য ভাষা প্রাইভেট ফাংশন পরীক্ষা করা কঠিন বা অসম্ভব করে তোলে। আপনি যে টেস্টিং মতাদর্শই অনুসরণ করুন না কেন, Rust-এর প্রাইভেসি নিয়ম আপনাকে প্রাইভেট ফাংশন পরীক্ষা করার অনুমতি দেয়। তালিকা ১১-১২-এর কোডটি বিবেচনা করুন যেখানে 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
মডিউলটি কেবল আরেকটি মডিউল। যেমনটি আমরা ["Paths for Referring to an Item in the Module Tree"][paths]-তে আলোচনা করেছি, চাইল্ড মডিউলের আইটেমগুলো তাদের পূর্বপুরুষ মডিউলের আইটেমগুলো ব্যবহার করতে পারে। এই টেস্টে, আমরা use super::*
দিয়ে tests
মডিউলের প্যারেন্টের সমস্ত আইটেমকে স্কোপে নিয়ে আসি এবং তারপর টেস্টটি internal_adder
কল করতে পারে। আপনি যদি মনে করেন যে প্রাইভেট ফাংশন পরীক্ষা করা উচিত নয়, তবে Rust-এ এমন কিছুই নেই যা আপনাকে তা করতে বাধ্য করবে।
ইন্টিগ্রেশন টেস্ট
Rust-এ, ইন্টিগ্রেশন টেস্টগুলো আপনার লাইব্রেরির সম্পূর্ণ বাইরে থাকে। তারা আপনার লাইব্রেরিটি অন্য যেকোনো কোডের মতোই ব্যবহার করে, যার মানে তারা কেবল সেই ফাংশনগুলোকেই কল করতে পারে যা আপনার লাইব্রেরির পাবলিক API-এর অংশ। তাদের উদ্দেশ্য হলো আপনার লাইব্রেরির অনেকগুলো অংশ একসাথে সঠিকভাবে কাজ করছে কিনা তা পরীক্ষা করা। যে কোডের ইউনিটগুলো একা একা সঠিকভাবে কাজ করে, সেগুলো ইন্টিগ্রেট করার সময় সমস্যা হতে পারে, তাই ইন্টিগ্রেটেড কোডের টেস্ট কভারেজও গুরুত্বপূর্ণ। ইন্টিগ্রেশন টেস্ট তৈরি করতে, আপনাকে প্রথমে একটি tests ডিরেক্টরি তৈরি করতে হবে।
tests ডিরেক্টরি
আমরা আমাদের প্রজেক্ট ডিরেক্টরির টপ লেভেলে, src এর পাশে একটি tests ডিরেক্টরি তৈরি করি। Cargo জানে যে এই ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট ফাইল খুঁজতে হবে। আমরা তখন যত খুশি টেস্ট ফাইল তৈরি করতে পারি, এবং Cargo প্রতিটি ফাইলকে একটি স্বতন্ত্র ক্রেট (crate) হিসাবে কম্পাইল করবে।
চলুন একটি ইন্টিগ্রেশন টেস্ট তৈরি করি। তালিকা ১১-১২-এর কোডটি এখনও src/lib.rs ফাইলে থাকা অবস্থায়, একটি tests ডিরেক্টরি তৈরি করুন এবং tests/integration_test.rs নামে একটি নতুন ফাইল তৈরি করুন। আপনার ডিরেক্টরি কাঠামোটি এমন হওয়া উচিত:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
তালিকা ১১-১৩-এর কোডটি tests/integration_test.rs ফাইলে প্রবেশ করান।
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
tests ডিরেক্টরির প্রতিটি ফাইল একটি পৃথক ক্রেট, তাই আমাদের প্রতিটি টেস্ট ক্রেটের স্কোপে আমাদের লাইব্রেরি আনতে হবে। এই কারণে আমরা কোডের শীর্ষে use adder;
যোগ করি, যা ইউনিট টেস্টে আমাদের প্রয়োজন হয়নি।
আমাদের 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
আউটপুটের তিনটি অংশে ইউনিট টেস্ট, ইন্টিগ্রেশন টেস্ট এবং ডক টেস্ট অন্তর্ভুক্ত রয়েছে। উল্লেখ্য, যদি কোনো একটি বিভাগের কোনো টেস্ট ফেইল করে, তাহলে পরবর্তী বিভাগগুলো চালানো হবে না। উদাহরণস্বরূপ, যদি একটি ইউনিট টেস্ট ফেইল করে, তাহলে ইন্টিগ্রেশন এবং ডক টেস্টের জন্য কোনো আউটপুট থাকবে না কারণ সেই টেস্টগুলো শুধুমাত্র তখনই চালানো হবে যদি সমস্ত ইউনিট টেস্ট পাস করে।
ইউনিট টেস্টের জন্য প্রথম বিভাগটি আমরা যা দেখে আসছি তার মতোই: প্রতিটি ইউনিট টেস্টের জন্য একটি লাইন (একটি internal
নামের যা আমরা তালিকা ১১-১২-এ যোগ করেছি) এবং তারপর ইউনিট টেস্টের জন্য একটি সারাংশ লাইন।
ইন্টিগ্রেশন টেস্ট বিভাগটি Running tests/integration_test.rs
লাইন দিয়ে শুরু হয়। এরপর, সেই ইন্টিগ্রেশন টেস্টের প্রতিটি টেস্ট ফাংশনের জন্য একটি লাইন এবং Doc-tests adder
বিভাগ শুরু হওয়ার ঠিক আগে ইন্টিগ্রেশন টেস্টের ফলাফলের জন্য একটি সারাংশ লাইন রয়েছে।
প্রতিটি ইন্টিগ্রেশন টেস্ট ফাইলের নিজস্ব বিভাগ রয়েছে, তাই আমরা যদি tests ডিরেক্টরিতে আরও ফাইল যোগ করি, তাহলে আরও ইন্টিগ্রেশন টেস্ট বিভাগ থাকবে।
আমরা এখনও cargo test
-এর আর্গুমেন্ট হিসাবে টেস্ট ফাংশনের নাম উল্লেখ করে একটি নির্দিষ্ট ইন্টিগ্রেশন টেস্ট ফাংশন চালাতে পারি। একটি নির্দিষ্ট ইন্টিগ্রেশন টেস্ট ফাইলের সমস্ত টেস্ট চালানোর জন্য, cargo test
-এর --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 ফাইলের টেস্টগুলো চালায়।
ইন্টিগ্রেশন টেস্টে সাবমডিউল
আপনি যখন আরও ইন্টিগ্রেশন টেস্ট যোগ করবেন, তখন সেগুলোকে সংগঠিত করতে সাহায্য করার জন্য আপনি tests ডিরেক্টরিতে আরও ফাইল তৈরি করতে চাইতে পারেন; উদাহরণস্বরূপ, আপনি যে কার্যকারিতা পরীক্ষা করছেন তার উপর ভিত্তি করে টেস্ট ফাংশনগুলোকে গ্রুপ করতে পারেন। আগে যেমন উল্লেখ করা হয়েছে, tests ডিরেক্টরির প্রতিটি ফাইল তার নিজস্ব পৃথক ক্রেট হিসাবে কম্পাইল করা হয়, যা পৃথক স্কোপ তৈরি করার জন্য দরকারী যাতে শেষ ব্যবহারকারীরা আপনার ক্রেট কীভাবে ব্যবহার করবে তার আরও কাছাকাছি অনুকরণ করা যায়। যাইহোক, এর মানে হল tests ডিরেক্টরির ফাইলগুলো src-এর ফাইলগুলোর মতো একই আচরণ শেয়ার করে না, যেমনটি আপনি অধ্যায় ৭-এ শিখেছিলেন কিভাবে কোডকে মডিউল এবং ফাইলে বিভক্ত করতে হয়।
tests ডিরেক্টরির ফাইলগুলোর ভিন্ন আচরণ সবচেয়ে বেশি লক্ষণীয় হয় যখন আপনার কাছে একাধিক ইন্টিগ্রেশন টেস্ট ফাইলে ব্যবহার করার জন্য একসেট সাহায্যকারী ফাংশন থাকে এবং আপনি সেগুলোকে একটি সাধারণ মডিউলে বের করে আনার জন্য অধ্যায় ৭-এর ["Separating Modules into Different Files"][separating-modules-into-files] বিভাগের ধাপগুলো অনুসরণ করার চেষ্টা করেন। উদাহরণস্বরূপ, যদি আমরা tests/common.rs তৈরি করি এবং এতে setup
নামে একটি ফাংশন রাখি, আমরা setup
-এ কিছু কোড যোগ করতে পারি যা আমরা একাধিক টেস্ট ফাইলের একাধিক টেস্ট ফাংশন থেকে কল করতে চাই:
Filename: 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 বোঝে এবং যা আমরা অধ্যায় ৭-এর ["Alternate File Paths"][alt-paths]-এ উল্লেখ করেছি। ফাইলটিকে এইভাবে নামকরণ করা Rust-কে বলে যে common
মডিউলটিকে একটি ইন্টিগ্রেশন টেস্ট ফাইল হিসাবে বিবেচনা না করতে। যখন আমরা setup
ফাংশনের কোডটি tests/common/mod.rs-এ নিয়ে যাই এবং tests/common.rs ফাইলটি মুছে ফেলি, তখন টেস্ট আউটপুটের বিভাগটি আর প্রদর্শিত হবে না। tests ডিরেক্টরির সাবডিরেক্টরির ফাইলগুলো পৃথক ক্রেট হিসাবে কম্পাইল হয় না বা টেস্ট আউটপুটে তাদের বিভাগ থাকে না।
আমরা tests/common/mod.rs তৈরি করার পরে, আমরা এটিকে যেকোনো ইন্টিগ্রেশন টেস্ট ফাইল থেকে একটি মডিউল হিসাবে ব্যবহার করতে পারি। এখানে tests/integration_test.rs-এর it_adds_two
টেস্ট থেকে setup
ফাংশন কল করার একটি উদাহরণ দেওয়া হলো:
Filename: 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;
ডিক্লারেশনটি আমরা তালিকা ৭-২১-এ দেখানো মডিউল ডিক্লারেশনের মতোই। তারপর, টেস্ট ফাংশনে, আমরা common::setup()
ফাংশন কল করতে পারি।
বাইনারি ক্রেটের জন্য ইন্টিগ্রেশন টেস্ট
যদি আমাদের প্রজেক্টটি একটি বাইনারি ক্রেট হয় যাতে শুধুমাত্র একটি src/main.rs ফাইল থাকে এবং কোনো src/lib.rs ফাইল না থাকে, আমরা tests ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট তৈরি করতে এবং src/main.rs ফাইলে সংজ্ঞায়িত ফাংশনগুলোকে একটি use
স্টেটমেন্ট দিয়ে স্কোপে আনতে পারি না। শুধুমাত্র লাইব্রেরি ক্রেটগুলো ফাংশন এক্সপোজ করে যা অন্যান্য ক্রেট ব্যবহার করতে পারে; বাইনারি ক্রেটগুলো নিজে থেকে চালানোর জন্য তৈরি।
এটি একটি কারণ যে কারণে Rust প্রজেক্ট যেগুলো একটি বাইনারি সরবরাহ করে, সেগুলোতে একটি সহজবোধ্য src/main.rs ফাইল থাকে যা src/lib.rs ফাইলে থাকা লজিককে কল করে। সেই কাঠামো ব্যবহার করে, ইন্টিগ্রেশন টেস্টগুলো use
দিয়ে লাইব্রেরি ক্রেট পরীক্ষা করতে পারে যাতে গুরুত্বপূর্ণ কার্যকারিতা উপলব্ধ করা যায়। যদি গুরুত্বপূর্ণ কার্যকারিতা কাজ করে, তাহলে src/main.rs ফাইলের অল্প পরিমাণ কোডও কাজ করবে, এবং সেই অল্প পরিমাণ কোড পরীক্ষা করার প্রয়োজন নেই।
সারসংক্ষেপ
Rust-এর টেস্টিং ফিচারগুলো কোড কীভাবে কাজ করা উচিত তা নির্দিষ্ট করার একটি উপায় সরবরাহ করে যাতে আপনি পরিবর্তন করার পরেও এটি আপনার প্রত্যাশা অনুযায়ী কাজ করে তা নিশ্চিত করা যায়। ইউনিট টেস্টগুলো একটি লাইব্রেরির বিভিন্ন অংশকে আলাদাভাবে পরীক্ষা করে এবং প্রাইভেট ইমপ্লিমেন্টেশন ডিটেইলস পরীক্ষা করতে পারে। ইন্টিগ্রেশন টেস্টগুলো পরীক্ষা করে যে লাইব্রেরির অনেকগুলো অংশ একসাথে সঠিকভাবে কাজ করছে কিনা, এবং তারা লাইব্রেরির পাবলিক API ব্যবহার করে কোডটি সেভাবেই পরীক্ষা করে যেভাবে এক্সটার্নাল কোড এটি ব্যবহার করবে। যদিও Rust-এর টাইপ সিস্টেম এবং ওনারশিপ নিয়ম কিছু ধরণের বাগ প্রতিরোধ করতে সাহায্য করে, আপনার কোড কীভাবে আচরণ করবে বলে আশা করা হচ্ছে সে সম্পর্কিত লজিক বাগ কমাতে টেস্টগুলো এখনও গুরুত্বপূর্ণ।
চলুন এই অধ্যায়ে এবং পূর্ববর্তী অধ্যায়গুলোতে শেখা জ্ঞান একত্রিত করে একটি প্রজেক্টে কাজ করা যাক