টেস্ট সংগঠন (Test Organization)
এই অধ্যায়ের শুরুতে যেমন উল্লেখ করা হয়েছে, টেস্টিং একটি জটিল বিষয়, এবং ভিন্ন ভিন্ন মানুষ ভিন্ন পরিভাষা (terminology) এবং সংগঠন (organization) ব্যবহার করে। Rust কমিউনিটি টেস্ট সম্পর্কে দুটি প্রধান বিভাগের পরিপ্রেক্ষিতে চিন্তা করে: ইউনিট টেস্ট (unit tests) এবং ইন্টিগ্রেশন টেস্ট (integration tests)। ইউনিট টেস্টগুলি ছোট এবং আরও ফোকাসড (focused), একটি মডিউলকে আলাদাভাবে টেস্ট করে এবং প্রাইভেট ইন্টারফেসগুলিও টেস্ট করতে পারে। ইন্টিগ্রেশন টেস্টগুলি সম্পূর্ণরূপে আপনার লাইব্রেরির বাইরে থাকে এবং আপনার কোডকে একইভাবে ব্যবহার করে যেভাবে অন্য কোনো বহিরাগত (external) কোড ব্যবহার করবে, শুধুমাত্র পাবলিক ইন্টারফেস ব্যবহার করে এবং প্রতিটি টেস্টে সম্ভাব্য একাধিক মডিউল পরীক্ষা করে।
উভয় ধরনের টেস্ট লেখাই গুরুত্বপূর্ণ, এটা নিশ্চিত করার জন্য যে আপনার লাইব্রেরির অংশগুলি আলাদাভাবে এবং একসাথে আপনার প্রত্যাশা অনুযায়ী কাজ করছে।
ইউনিট টেস্ট (Unit Tests)
ইউনিট টেস্টের উদ্দেশ্য হল কোডের প্রতিটি ইউনিটকে বাকি কোড থেকে আলাদা করে টেস্ট করা, যাতে দ্রুত শনাক্ত করা যায় কোথায় কোড প্রত্যাশিতভাবে কাজ করছে এবং কোথায় করছে না। আপনি ইউনিট টেস্টগুলিকে src ডিরেক্টরির মধ্যে প্রতিটি ফাইলে রাখবেন, সেই কোডের সাথে যা তারা টেস্ট করছে। কনভেনশন হল প্রতিটি ফাইলে tests
নামে একটি মডিউল তৈরি করা, টেস্ট ফাংশনগুলি ধারণ করার জন্য এবং মডিউলটিকে cfg(test)
দিয়ে চিহ্নিত করা।
টেস্ট মডিউল এবং #[cfg(test)]
(The Tests Module and #[cfg(test)]
)
tests
মডিউলে #[cfg(test)]
অ্যানোটেশন Rust-কে বলে যে টেস্ট কোড শুধুমাত্র তখনই কম্পাইল এবং রান করতে হবে যখন আপনি cargo test
চালাবেন, cargo build
চালানোর সময় নয়। এটি কম্পাইলের সময় বাঁচায় যখন আপনি শুধুমাত্র লাইব্রেরি তৈরি করতে চান এবং ফলস্বরূপ কম্পাইল করা আর্টিফ্যাক্টে (artifact) জায়গা বাঁচায় কারণ টেস্টগুলি অন্তর্ভুক্ত করা হয় না। আপনি দেখবেন যে ইন্টিগ্রেশন টেস্টগুলি একটি ভিন্ন ডিরেক্টরিতে যায় বলে তাদের #[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
দিয়ে টেস্ট চালাই। এর মধ্যে এই মডিউলের মধ্যে থাকা যেকোনো হেল্পার (helper) ফাংশন অন্তর্ভুক্ত, #[test]
দিয়ে চিহ্নিত ফাংশনগুলি ছাড়াও।
প্রাইভেট ফাংশন টেস্ট করা (Testing Private Functions)
টেস্টিং কমিউনিটির মধ্যে বিতর্ক রয়েছে যে প্রাইভেট ফাংশনগুলি সরাসরি টেস্ট করা উচিত কিনা, এবং অন্যান্য ভাষাগুলি প্রাইভেট ফাংশনগুলি টেস্ট করা কঠিন বা অসম্ভব করে তোলে। আপনি যে টেস্টিং আইডিওলজি (ideology)-ই মেনে চলুন না কেন, Rust-এর প্রাইভেসি (privacy) নিয়মগুলি আপনাকে প্রাইভেট ফাংশনগুলি টেস্ট করার অনুমতি দেয়। internal_adder
প্রাইভেট ফাংশন সহ লিস্টিং 11-12-এর কোডটি বিবেচনা করুন।
pub fn add_two(a: usize) -> usize {
internal_adder(a, 2)
}
fn internal_adder(left: usize, right: usize) -> usize {
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
মডিউলটি অন্য একটি মডিউল। যেমনটি আমরা ["পাথস ফর রেফারring টু এন আইটেম ইন দা মডিউল ট্রি"][paths] তে আলোচনা করেছি, চাইল্ড মডিউলের আইটেমগুলি তাদের অ্যানসেস্টর (ancestor) মডিউলের আইটেমগুলি ব্যবহার করতে পারে। এই টেস্টে, আমরা tests
মডিউলের প্যারেন্টের সমস্ত আইটেমকে use super::*
দিয়ে স্কোপে আনি, এবং তারপর টেস্টটি internal_adder
কল করতে পারে। আপনি যদি মনে করেন যে প্রাইভেট ফাংশনগুলি টেস্ট করা উচিত নয়, তবে Rust-এ এমন কিছু নেই যা আপনাকে তা করতে বাধ্য করবে।
ইন্টিগ্রেশন টেস্ট (Integration Tests)
Rust-এ, ইন্টিগ্রেশন টেস্টগুলি সম্পূর্ণরূপে আপনার লাইব্রেরির বাইরে থাকে। তারা আপনার লাইব্রেরি ব্যবহার করে একইভাবে যেভাবে অন্য কোনো কোড ব্যবহার করবে, যার মানে হল তারা শুধুমাত্র সেই ফাংশনগুলিকে কল করতে পারে যেগুলি আপনার লাইব্রেরির পাবলিক API-এর অংশ। তাদের উদ্দেশ্য হল আপনার লাইব্রেরির অনেকগুলি অংশ একসাথে সঠিকভাবে কাজ করে কিনা তা পরীক্ষা করা। কোডের ইউনিটগুলি যেগুলি নিজেরা সঠিকভাবে কাজ করে, ইন্টিগ্রেট (integrate) করার সময় সমস্যা হতে পারে, তাই ইন্টিগ্রেটেড কোডের টেস্ট কভারেজও (coverage) গুরুত্বপূর্ণ। ইন্টিগ্রেশন টেস্ট তৈরি করতে, আপনাকে প্রথমে একটি tests ডিরেক্টরি তৈরি করতে হবে।
_tests_
ডিরেক্টরি (The tests Directory)
আমরা আমাদের প্রোজেক্ট ডিরেক্টরির উপরের স্তরে, src-এর পাশে একটি tests ডিরেক্টরি তৈরি করি। Cargo জানে যে এই ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট ফাইলগুলি খুঁজতে হবে। আমরা তারপর যতগুলি খুশি টেস্ট ফাইল তৈরি করতে পারি, এবং Cargo প্রতিটি ফাইলকে একটি আলাদা ক্রেট হিসাবে কম্পাইল করবে।
আসুন একটি ইন্টিগ্রেশন টেস্ট তৈরি করি। লিস্টিং 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 ডিরেক্টরির প্রতিটি ফাইল একটি পৃথক ক্রেট, তাই আমাদের লাইব্রেরিকে প্রতিটি টেস্ট ক্রেটের স্কোপে আনতে হবে। সেই কারণে আমরা কোডের শীর্ষে 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
আউটপুটের তিনটি বিভাগে ইউনিট টেস্ট, ইন্টিগ্রেশন টেস্ট এবং ডক টেস্ট অন্তর্ভুক্ত রয়েছে। লক্ষ্য করুন যে যদি কোনও বিভাগের কোনও টেস্ট ব্যর্থ হয়, তবে নিম্নলিখিত বিভাগগুলি চালানো হবে না। উদাহরণস্বরূপ, যদি একটি ইউনিট টেস্ট ব্যর্থ হয়, তাহলে ইন্টিগ্রেশন এবং ডক টেস্টের জন্য কোনও আউটপুট থাকবে না কারণ সেই টেস্টগুলি শুধুমাত্র তখনই চালানো হবে যদি সমস্ত ইউনিট টেস্ট পাস করে।
ইউনিট টেস্টের জন্য প্রথম বিভাগটি আমরা যেভাবে দেখছি তেমনই: প্রতিটি ইউনিট টেস্টের জন্য একটি লাইন (লিস্টিং 11-12-এ যোগ করা 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 ফাইলের টেস্টগুলি চালায়।
ইন্টিগ্রেশন টেস্টে সাবমডিউল (Submodules in Integration Tests)
আপনি আরও ইন্টিগ্রেশন টেস্ট যোগ করার সাথে সাথে, আপনি সেগুলিকে সংগঠিত করতে সহায়তা করার জন্য tests ডিরেক্টরিতে আরও ফাইল তৈরি করতে চাইতে পারেন; উদাহরণস্বরূপ, আপনি যে কার্যকারিতা পরীক্ষা করছেন তার দ্বারা টেস্ট ফাংশনগুলিকে গ্রুপ করতে পারেন। আগেই যেমন উল্লেখ করা হয়েছে, tests ডিরেক্টরির প্রতিটি ফাইলকে তার নিজস্ব আলাদা ক্রেট হিসাবে কম্পাইল করা হয়, যা আলাদা স্কোপ তৈরি করার জন্য দরকারী, এন্ড ইউজাররা (end users) আপনার ক্রেট কীভাবে ব্যবহার করবে তা আরও ঘনিষ্ঠভাবে অনুকরণ করতে। যাইহোক, এর মানে হল tests ডিরেক্টরির ফাইলগুলি src-এর ফাইলগুলির মতো একই আচরণ শেয়ার করে না, যেমনটি আপনি চ্যাপ্টার ৭-এ শিখেছেন কীভাবে কোডকে মডিউল এবং ফাইলগুলিতে আলাদা করতে হয়।
tests ডিরেক্টরি ফাইলগুলির ভিন্ন আচরণ সবচেয়ে বেশি লক্ষণীয় হয় যখন আপনার কাছে একাধিক ইন্টিগ্রেশন টেস্ট ফাইলে ব্যবহার করার জন্য একগুচ্ছ হেল্পার ফাংশন থাকে এবং আপনি সেগুলিকে একটি সাধারণ মডিউলে এক্সট্রাক্ট (extract) করার জন্য চ্যাপ্টার ৭-এর ["সেপারেটিং মডিউলস ইনটু ডিফারেন্ট ফাইলস"][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-ও বোঝে, যা আমরা চ্যাপ্টার ৭-এ ["অল্টারনেট ফাইল পাথস"][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;
ডিক্লারেশনটি (declaration) লিস্টিং 7-21-এ প্রদর্শিত মডিউল ডিক্লারেশনের মতোই। তারপর, টেস্ট ফাংশনে, আমরা common::setup()
ফাংশনটি কল করতে পারি।
বাইনারি ক্রেটের জন্য ইন্টিগ্রেশন টেস্ট (Integration Tests for Binary Crates)
যদি আমাদের প্রোজেক্টটি একটি বাইনারি ক্রেট হয় যাতে শুধুমাত্র একটি src/main.rs ফাইল থাকে এবং একটি src/lib.rs ফাইল না থাকে, তাহলে আমরা tests ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট তৈরি করতে পারি না এবং use
স্টেটমেন্ট দিয়ে src/main.rs ফাইলে সংজ্ঞায়িত ফাংশনগুলিকে স্কোপে আনতে পারি না। শুধুমাত্র লাইব্রেরি ক্রেটগুলি ফাংশন প্রকাশ করে যা অন্য ক্রেটগুলি ব্যবহার করতে পারে; বাইনারি ক্রেটগুলি নিজে থেকেই চালানোর জন্য তৈরি।
এটি একটি কারণ যে Rust প্রোজেক্টগুলি যেগুলি একটি বাইনারি সরবরাহ করে সেগুলির একটি সরল src/main.rs ফাইল থাকে যা src/lib.rs ফাইলে থাকা লজিককে কল করে। সেই কাঠামো ব্যবহার করে, ইন্টিগ্রেশন টেস্টগুলি গুরুত্বপূর্ণ কার্যকারিতা উপলব্ধ করতে use
সহ লাইব্রেরি ক্রেট পরীক্ষা করতে পারে। যদি গুরুত্বপূর্ণ কার্যকারিতা কাজ করে, তাহলে src/main.rs ফাইলের অল্প পরিমাণ কোডও কাজ করবে এবং সেই অল্প পরিমাণ কোড টেস্ট করার প্রয়োজন নেই।
সারসংক্ষেপ (Summary)
Rust-এর টেস্টিং ফিচারগুলি কোড কীভাবে কাজ করা উচিত তা নির্দিষ্ট করার একটি উপায় সরবরাহ করে, যাতে আপনি পরিবর্তন করলেও এটি আপনার প্রত্যাশা অনুযায়ী কাজ করে। ইউনিট টেস্টগুলি একটি লাইব্রেরির বিভিন্ন অংশ আলাদাভাবে পরীক্ষা করে এবং প্রাইভেট ইমপ্লিমেন্টেশনের বিস্তারিত পরীক্ষা করতে পারে। ইন্টিগ্রেশন টেস্টগুলি পরীক্ষা করে যে লাইব্রেরির অনেকগুলি অংশ একসাথে সঠিকভাবে কাজ করে কিনা এবং তারা লাইব্রেরির পাবলিক API ব্যবহার করে কোডটিকে একইভাবে পরীক্ষা করে যেভাবে বহিরাগত কোড এটি ব্যবহার করবে। যদিও Rust-এর টাইপ সিস্টেম এবং ওনারশিপ (ownership) নিয়মগুলি কিছু ধরণের বাগ প্রতিরোধ করতে সহায়তা করে, তবুও আপনার কোড কীভাবে আচরণ করবে বলে আশা করা হচ্ছে তার সাথে সম্পর্কিত লজিক বাগগুলি কমাতে টেস্টগুলি গুরুত্বপূর্ণ।
আসুন এই অধ্যায়ে এবং পূর্ববর্তী অধ্যায়গুলিতে আপনি যা শিখেছেন তা একত্রিত করে একটি প্রোজেক্টে কাজ করি!