এই পর্বে যা শিখবেন
- “Unit” বলতে কী বোঝায় — London আর Detroit ধারার পার্থক্য, solitary বনাম sociable test
- Arrange-Act-Assert কাঠামোয় একটি test ধাপে ধাপে লেখা, আর ভালো নাম দেওয়া
- সমতা, exception, floating point আর collection-এর জন্য ঠিক assertion বেছে নেওয়া
setUp/tearDown, pytest fixture, test data builder আর parameterized test দিয়ে পুনরাবৃত্তি কমানো- সময়, এলোমেলোতা আর ক্রমের কারণে অস্থির test চেনা ও deterministic করা, আর পরিচিত test smell সারানো
১ · “Unit” আসলে কী — function, class, না একটি আচরণ?
পর্ব ৪৫-এ (Testing-এর মৌলিক) আপনি test-এর স্তরগুলো দেখেছেন: unit, integration, system আর acceptance. এই পর্বে আমরা সবচেয়ে নিচের আর সবচেয়ে বেশি সংখ্যায় লেখা স্তরটিতে নামব — unit test. সংজ্ঞাটি শুনতে সহজ: কোডের একটি ছোট অংশ একা একা ঠিক কাজ করে কি না, তার স্বয়ংক্রিয় যাচাই। কিন্তু “ছোট অংশ” কতটুকু — এই প্রশ্নে অভিজ্ঞ প্রকৌশলীরাও একমত নন।
ক্রিকেট দলের নেট প্র্যাকটিসের কথা ভাবুন। একজন ব্যাটসম্যান নেটে একা দাঁড়িয়ে বোলিং মেশিনের বল খেলছেন — মাঠ নেই, প্রতিপক্ষ নেই, দর্শক নেই। প্রতিটি বল একই গতিতে আসে, তাই কোন শটে দুর্বলতা, তা মিনিটেই ধরা পড়ে। এটাই unit test: দ্রুত, নিয়ন্ত্রিত, একটি দক্ষতা আলাদা করে। কিন্তু কোচ কখনো কখনো দুজন ব্যাটসম্যানকে একসাথে নামান, রান নেওয়ার ডাক মিলছে কি না দেখতে — বোলার তখনো মেশিন, কিন্তু দুজন আসল খেলোয়াড়। আর পুরো ম্যাচ হলো system test. প্রশ্ন হলো, “দুজন একসাথে” অনুশীলনকে কি এখনো নেট প্র্যাকটিস বলবেন? Unit test-এর সংজ্ঞার বিতর্কটি ঠিক এখানেই।
দুই ধারা: London বনাম Detroit (Chicago)
Martin Fowler তাঁর ২০০৭ সালের প্রবন্ধ “Mocks Aren't Stubs”-এ দুটি ধারার নাম দেন classicist আর mockist. পরে এরা জায়গার নামেও পরিচিত হয়: Kent Beck-এর মূল XP দলের কাজ যুক্তরাষ্ট্রের Detroit-এ একটি গাড়ি কোম্পানির প্রকল্পে হয়েছিল বলে classicist ধারাকে বলা হয় Detroit (কেউ কেউ বলেন Chicago) school; আর লন্ডনের XP সম্প্রদায় থেকে আসা, Steve Freeman ও Nat Pryce-এর Growing Object-Oriented Software, Guided by Tests (২০০৯) বইয়ে বিস্তারিত হওয়া ধারাকে বলা হয় London school. নামগুলো প্রচলিত ডাকনাম; কোনো আনুষ্ঠানিক মানদণ্ড নয়।
| প্রশ্ন | Detroit / classicist | London / mockist |
|---|---|---|
| “Unit” কী | একটি আচরণ — দরকার হলে কয়েকটি class মিলে | সাধারণত একটি class |
| সহযোগী object | আসলটিই চলে; শুধু ধীর বা বাইরের জিনিস (network, database, ঘড়ি) বদলানো হয় | প্রায় সব সহযোগীর জায়গায় test double |
| কী যাচাই করে | শেষ অবস্থা বা ফেরত মান (state verification) | সহযোগীকে ঠিকভাবে ডাকা হলো কি না (behaviour verification) |
| সুবিধা | ভেতরের গঠন বদলালেও test টেকে; refactoring সহজ | ব্যর্থ হলে ঠিক কোন class দোষী, স্পষ্ট; নকশাকে interface-এর দিকে ঠেলে দেয় |
| ঝুঁকি | একটি ভুলে অনেক test একসাথে লাল হতে পারে | Test বাস্তবায়নের খুঁটিনাটিতে বাঁধা পড়ে, ফলে fragile হয় |
এই দুই রকম test-এর জন্য Jay Fields দুটি সহজ নাম প্রস্তাব করেন, আর Fowler ২০১৪ সালের একটি লেখায় সেগুলো জনপ্রিয় করেন বলে জানা যায়: solitary test (পরীক্ষাধীন class একা, সহযোগীরা নকল) আর sociable test (আসল সহযোগীসহ)। Test double-এর প্রকারভেদ — dummy, stub, fake, spy আর mock — আর কখন mock করা উচিত নয়, তা পর্ব ৪৯-এর (Test double) বিষয়; এখানে শুধু মনে রাখুন যে দুটি ধারাই বৈধ, আর বেশিরভাগ দল দুটোর মিশ্রণ ব্যবহার করে।
ধারা যা-ই হোক, unit test-এর ব্যবহারিক মাপকাঠি প্রায় সবাই মানেন: দ্রুত (মিলিসেকেন্ডে), বিচ্ছিন্ন (network, আসল database বা অন্য test-এর ওপর নির্ভর নয়), আর ব্যর্থ হলে কারণ স্পষ্ট. Michael Feathers ২০০৫ সালের একটি লেখায় প্রায় এভাবে বলেছিলেন বলে উদ্ধৃত হয়: যে test database-এ কথা বলে, network ব্যবহার করে, file system ছোঁয়, অন্য test-এর সাথে একসাথে চলতে পারে না বা চালানোর আগে পরিবেশে বিশেষ কিছু করতে হয় — সেটি unit test নয়। তার মানে এই নয় যে এমন test খারাপ; সেগুলো integration test, যা পর্ব ৫১-এ (Test pyramid আর integration test) আসবে।
xUnit পরিবারের জন্ম: Kent Beck Smalltalk ভাষার জন্য একটি ছোট testing framework লেখেন, পরে যার নাম হয় SUnit; এর বিবরণ “Simple Smalltalk Testing: With Patterns” লেখায় পাওয়া যায়, যা সাধারণত ১৯৯৪ সালের বলে উল্লেখ করা হয় (তারিখ নিয়ে সূত্রভেদে সামান্য পার্থক্য আছে)। ১৯৯৭ সালে Beck আর Erich Gamma (Gang of Four-এর একজন) মিলে Java-র জন্য JUnit লেখেন — প্রচলিত গল্প অনুযায়ী একটি সম্মেলনে যাওয়ার বিমানযাত্রায়। এরপর প্রায় প্রতিটি ভাষায় একই নকশার framework আসে: NUnit, CppUnit, আর Python-এ Steve Purcell-এর PyUnit, যা Python 2.1 (২০০১) থেকে standard library-র unittest হয়ে আছে। এদের সবাইকে একসাথে বলা হয় xUnit পরিবার, কারণ সবার ভেতরে একই চারটি ধারণা: test case, fixture, assertion আর test runner.
এই পর্বের Python উদাহরণগুলো standard library-র unittest দিয়ে, কারণ এটি কিছু install না করেই চলে আর xUnit-এর ধারণাগুলো সরাসরি দেখায়। বাস্তবে Python দলগুলো প্রায়ই pytest ব্যবহার করে; তার খুঁটিনাটি পাবেন Python কোর্সের testing পর্বে, আর JavaScript-এর Vitest/Jest পাবেন JavaScript কোর্সের testing পর্বে. এখানে আমরা ভাষা-নিরপেক্ষ নীতিগুলো দেখব — যেগুলো যেকোনো framework-এ একই।
২ · Arrange–Act–Assert — একটি test-এর তিনটি অংশ
বিয়েবাড়ির রান্নার একটি ছোট পরীক্ষা ভাবুন। বাবুর্চি নতুন মসলার মিশ্রণ চেখে দেখবেন। প্রথমে তিনি এক বাটি ভাত আর এক চামচ মাংস আলাদা করে সাজান (প্রস্তুতি), তারপর মসলা মেশান (একটি কাজ), শেষে চেখে বলেন “লবণ ঠিক আছে” বা “কম হয়েছে” (যাচাই)। তিনি একই সাথে তিনটি মসলা বদলান না — তাহলে স্বাদ খারাপ হলে বোঝা যেত না কোনটির দোষ।
প্রায় প্রতিটি ভালো unit test এই তিন ভাগে লেখা যায়। নামটি Arrange-Act-Assert, সংক্ষেপে AAA; Bill Wake প্রায় ২০০১ সালে এই নাম দেন বলে সাধারণত উল্লেখ করা হয়, যদিও ধারণাটি তার আগেও প্রচলিত ছিল।
একটি unit test-এর তিন অংশ
Arrange → Act → Assert; ডান পাশে আচরণের ভাষায় একই কথা (Given–When–Then)
- Arrange: পরীক্ষাধীন object আর input তৈরি। এটিই সবচেয়ে লম্বা হতে পারে; বারবার একই জিনিস লাগলে fixture-এ সরান (অংশ ৫)।
- Act: যে আচরণ যাচাই করছেন, সেটি একবার ডাকা। দুটি Act মানে সাধারণত দুটি test লুকিয়ে আছে।
- Assert: ফল প্রত্যাশার সাথে মেলানো। এখানে একাধিক assertion থাকতে পারে, যদি সবগুলো একই আচরণের দিক হয় (যেমন ফেরত দেওয়া রসিদের মোট আর আইটেম সংখ্যা)।
আচরণের ভাষায় একই কাঠামোর নাম Given–When–Then: Given (এই অবস্থা দেওয়া আছে), When (এই ঘটনা ঘটলে), Then (এই ফল হওয়া উচিত)। পর্ব ২২-এ (Use case আর user story) আপনি এটি acceptance criteria লিখতে দেখেছেন; আর পর্ব ৪৮-এ (TDD আর BDD) এটি কীভাবে চালানো যায় এমন specification হয়, তা দেখবেন। Unit test-এ সাধারণত AAA বলা হয়, কারণ ভাষাটি কোডের কাছাকাছি।
এবার একটি test শূন্য থেকে লিখি। কাল্পনিক একটি কুরিয়ার সার্ভিসের delivery fee-র নিয়ম: শহরের ভেতরে প্রথম ১ কেজি ৬০ টাকা, প্রতি বাড়তি (বা আংশিক) কেজি ১৫ টাকা; শহরের বাইরে প্রথম কেজি ১২০ টাকা, প্রতি বাড়তি কেজি ২৫ টাকা। ২.৫ কেজি শহরের বাইরে গেলে ওজন ওপরের দিকে ৩ কেজি ধরা হয়, তাই fee = ১২০ + ২৫ × ২ = ১৭০ টাকা।
1def test_fee_outside_part_kg_rounds_up(self):2 # Arrange3 weight, zone = 2.5, "outside"4 # Act5 result = self.calc.fee(weight, zone)6 # Assert7 self.assertEqual(result, 170)
| weight, zone | |
| result | |
| প্রত্যাশা |
- আগে নাম। নামেই তিনটি তথ্য: কোন unit (
fee), কোন পরিস্থিতি (শহরের বাইরে, আংশিক কেজি), কী প্রত্যাশা (ওপরের দিকে গোল হয়)। ব্যর্থ হলে শুধু নাম পড়েই বোঝা যাবে কোন নিয়ম ভাঙল। - Arrange. শুধু এই আচরণের জন্য যা দরকার: ওজন ২.৫ আর zone
"outside".self.calcআগেইsetUp-এ তৈরি (অংশ ৫)। ২.৫ বেছে নেওয়া হয়েছে যাতে “গোল করা” নিয়মটি সত্যিই পরীক্ষা হয়। - Act. ঠিক একটি ডাক:
fee(2.5, "outside"), ফল 170. Act অংশে কোনো হিসাব নেই — শুধু ডাকা। - Assert. প্রত্যাশা লেখা হলো হাতে হিসাব করা সংখ্যা হিসেবে: ১২০ + ২৫ × ২ = ১৭০. সূত্রটি (
120 + 25 * (ceil(w) - 1)) test-এ আবার লিখলে test শুধু কোডের আয়না হতো; ভুল সূত্র দুই জায়গাতেই একই ভুল দিত। - Test কি ব্যর্থ হতে পারে? একবার ইচ্ছা করে প্রত্যাশা ১৬০ করে চালান। runner লাল দেখায়:
AssertionError: 170 != 160— প্রথমে আসল মান, পরে প্রত্যাশা। যে test কখনো লাল হতে দেখেননি, সেটি আদৌ কিছু যাচাই করছে কি না, নিশ্চিত নন। - আবার ১৭০ — সবুজ। Runner দেখায়
... ok. এখন test-টি একটি জীবন্ত দলিল: “বাইরে আংশিক কেজি ওপরের দিকে গোল হয়।” পরে কেউceil-এর জায়গায়roundলিখলে ২.৫ থেকে ২ হয়ে fee ১৪৫ আসবে, আর এই test তখনই ধরবে। (Python-এরround(2.5)“banker's rounding” মেনে 2 দেয়।)
Assert-এ প্রত্যাশা আগে না পরে? Python-এর assertEqual(first, second) কোনো ক্রম বাধ্য করে না (এখানে আমরা আসল মান আগে লিখেছি); JUnit-এর assertEquals(expected, actual) উল্টো ক্রম চায়। দলে একটি নিয়ম মানুন, নইলে ব্যর্থতার বার্তা ভুল দিকে ইঙ্গিত করে।
৩ · ভালো test-এর গুণ — FIRST, এক আচরণ, ভালো নাম, logic নয়
Test-ও কোড, আর খারাপ test খারাপ কোডের মতোই বোঝা। দলের কাছে test-এর মূল্য একটাই: লাল হলে বিশ্বাস করা যায় যে কিছু সত্যিই ভেঙেছে, সবুজ হলে বিশ্বাস করা যায় যে আচরণটি ঠিক আছে। এই বিশ্বাস তৈরি হয় কয়েকটি গুণ থেকে। জনপ্রিয় সংক্ষেপটি FIRST; এটি Brett Schuchert ও Tim Ottinger-এর নামে প্রচলিত, আর Robert C. Martin-এর Clean Code (২০০৮) বইয়ের test অধ্যায়ে ব্যাপক পরিচিতি পায় — অক্ষরগুলোর ব্যাখ্যা উৎসভেদে সামান্য আলাদা।
F · Fast — দ্রুত
হাজার unit test কয়েক সেকেন্ডে শেষ হওয়া উচিত। ধীর test মানুষ কম চালায়, আর না চালানো test কোনো কাজের নয়।
sleep, আসল network নয়I · Independent / Isolated — স্বাধীন
প্রতিটি test নিজের শুরুর অবস্থা নিজে তৈরি করে; যেকোনো ক্রমে, একা বা একসাথে চালালে একই ফল। এটাই test isolation.
ভাগ করা state নয়R · Repeatable — বারবার একই
আপনার ল্যাপটপে, CI-তে, রাত ২টায় বা ঈদের দিনে — একই ফল। সময়, এলোমেলো সংখ্যা, time zone বা ইন্টারনেট সংযোগের ওপর নির্ভর নয়।
deterministicS · Self-validating — নিজেই রায় দেয়
Pass বা fail — দুটির একটি, কোনো log পড়ে মানুষকে বিচার করতে হয় না। print করে চোখে মেলানো test নয়।
T · Timely — সময়মতো
কোডের সাথে সাথে (বা TDD-তে আগেই) লেখা। ছয় মাস পরে লেখা test সাধারণত পুরোনো কোডের বর্তমান আচরণই নকল করে, উচিত আচরণ নয়।
পর্ব ৪৮: TDDএক test-এ এক আচরণ
“এক test-এ একটি assertion” নিয়মটি প্রায়ই শোনা যায়, কিন্তু আক্ষরিকভাবে মানলে অপ্রয়োজনীয় কষ্ট। আসল নিয়মটি হলো এক test-এ একটি আচরণ — একটি কারণে ব্যর্থ হওয়া উচিত। “রসিদের মোট ঠিক” আর “রসিদে প্রতিটি আইটেম আছে” একই আচরণের দুটি দিক হলে এক test-এ থাকতে পারে; কিন্তু “মোট ঠিক” আর “ঋণাত্মক ওজনে error” দুটি ভিন্ন আচরণ — আলাদা test. দুটি এক test-এ থাকলে প্রথমটি ব্যর্থ হলে দ্বিতীয়টি চলেই না, আর নাম দিয়ে বোঝানো যায় না কী ভাঙল।
নাম — test-এর নামই প্রথম ব্যর্থতার বার্তা
CI-র লাল তালিকায় আপনি প্রথমে শুধু নামগুলো দেখবেন। test1 বা test_fee কিছুই বলে না। পর্ব ৪১-এর (Clean code) নামের নীতিগুলো test-এ আরও কড়াভাবে খাটে। প্রচলিত কয়েকটি ধরন:
| ধরন | উদাহরণ | কোথায় বেশি দেখা যায় |
|---|---|---|
test_<unit>_<scenario>_<expected> | test_fee_zero_weight_raises | Python (unittest, pytest) |
| “should …” বাক্য | it("should round part kg up") | Jest, Vitest, Mocha |
| Unit_State_Expected | Fee_ZeroWeight_Throws | .NET, Java; Roy Osherove-এর বইয়ে প্রচলিত বলে জানা যায় |
| Given/When/Then বাক্য | given_outside_zone_when_2_5kg_then_170 | BDD-ধাঁচের দল |
কোন ধরন বেছে নেবেন, তার চেয়ে জরুরি হলো পুরো project-এ একটি ধরন মানা, আর নামে আচরণ লেখা, method-এর নাম নয়। test_calculate নয়, test_fee_unknown_zone_raises.
Test-এ logic নয়
Test-এর ভেতরে if, for দিয়ে হিসাব, বা প্রত্যাশিত মান বের করতে কোডের সূত্রই আবার লেখা — এটি সবচেয়ে প্রচলিত আর সবচেয়ে বিপজ্জনক ভুলগুলোর একটি। নিচের test দেখুন (চালানোর জন্য নয়):
def test_fee_many_weights(self):
for w in [0.5, 1.2, 3, 7.9]:
if w <= 1:
expected = 60
else:
expected = 60 + 15 * (math.ceil(w) - 1) # the formula again!
self.assertEqual(self.calc.fee(w, "city"), expected)সমস্যা তিনটি। প্রথমত, প্রত্যাশা কোডের সূত্র থেকেই আসছে, তাই সূত্রে ভুল থাকলে test-ও সেই ভুলকে “ঠিক” বলবে — test আর কোড একই কথা দুবার বলছে। দ্বিতীয়ত, test-এর if নিজেই ভুল হতে পারে, আর test-এর test কেউ লেখে না। তৃতীয়ত, loop-এ প্রথম ব্যর্থতায় বাকি মানগুলো আর যাচাই হয় না। সমাধান: প্রত্যাশিত মান হাতে হিসাব করা সংখ্যা হিসেবে একটি টেবিলে লিখুন, আর টেবিলটি parameterized test দিয়ে চালান (অংশ ৫)। Test হওয়া উচিত মুদি দোকানের হিসাবের খাতার পাতার মতো — প্রতিটি লাইনে “এই মাল, এই দাম”।
DRY test-এ কতটুকু? Production কোডে পুনরাবৃত্তি কমানো (DRY, পর্ব ২৬) গুণ। কিন্তু test-এ অতিরিক্ত helper আর abstraction দিলে প্রতিটি test পড়তে পাঁচটি ফাইল ঘুরতে হয়। অনেক দল তাই test-এর জন্য “DAMP” কথাটি বলে — Descriptive And Meaningful Phrases: একটু পুনরাবৃত্তি থাকুক, কিন্তু প্রতিটি test একা পড়ে বোঝা যাক। শুরুর অবস্থার একঘেয়ে অংশ fixture বা builder-এ সরান; প্রত্যাশিত মান আর যা পরীক্ষা হচ্ছে, তা test-এর ভেতরেই চোখের সামনে রাখুন।
৪ · Assertion — কোন যাচাইয়ে কোন হাতিয়ার
Test-এর assertion হলো রায় দেওয়ার মুহূর্ত। প্রতিটি framework অনেকগুলো assertion দেয়, আর ঠিকটি বেছে নিলে ব্যর্থতার বার্তা অনেক স্পষ্ট হয়। যেমন assertTrue(a == b) ব্যর্থ হলে শুধু বলে False is not true; কিন্তু assertEqual(a, b) বলে 170 != 160, আর list হলে কোন ঘরে পার্থক্য তাও দেখায়।
দুই রকম “assert”: পর্ব ৩১-এ (Interface আর contract) আপনি production কোডের ভেতরের assertion দেখেছেন — programmer-এর ভুল ধরার জন্য, যা Python-এ -O দিলে বাদ পড়ে, আর তাই ব্যবহারকারীর input যাচাইয়ে (validation) ব্যবহার করা যায় না। Test-এর assertion আলাদা জিনিস: এটি test কোডে থাকে, সবসময় চলে, আর পরীক্ষাধীন কোডের আচরণ নিয়ে রায় দেয়।
| যাচাই | Python unittest | pytest | Jest / Vitest |
|---|---|---|---|
| সমান | assertEqual(a, b) | assert a == b | expect(a).toBe(b) (একই মান/object), toEqual (গঠনে সমান) |
| সত্য/মিথ্যা | assertTrue(x), assertFalse(x) | assert x | toBe(true), toBeTruthy() |
| None / null | assertIsNone(x) | assert x is None | toBeNull(), toBeUndefined() |
| Exception | with assertRaises(ValueError):, assertRaisesRegex | with pytest.raises(ValueError, match=...) | expect(() => f()).toThrow("...") |
| দশমিক সংখ্যা | assertAlmostEqual(a, b, places=7) | assert a == pytest.approx(b) | toBeCloseTo(b, digits=2) |
| Collection | assertIn, assertCountEqual (ক্রম ছাড়া), assertDictEqual | assert x in xs, assert sorted(a) == sorted(b) | toContain, toHaveLength, toEqual([...]) |
Exception যাচাই — ভুল input-এ ঠিকভাবে ব্যর্থ হওয়া
একটি function-এর চুক্তির অর্ধেকটা হলো “ভুল input-এ কী হবে।” শূন্য ওজনে fee চাইলে ValueError ওঠা উচিত — এটাও একটি আচরণ, তাই এর জন্যও test লাগে। দুটি সতর্কতা: (১) শুধু exception-এর ধরন নয়, দরকার হলে বার্তাটিও মেলান (assertRaisesRegex, pytest-এর match=), নইলে অন্য কোনো কারণে ওঠা একই ধরনের exception-ও test পাশ করিয়ে দেবে। (২) with block-এর ভেতরে শুধু সেই একটি লাইন রাখুন যেটি exception তোলার কথা; Arrange অংশ ভেতরে রাখলে সেখানে ওঠা exception-ও “সফল” ধরা হবে। Jest-এ toThrow-কে function দিতে হয় (() => f()), ফল নয় — expect(f()).toThrow() লিখলে exception test-এর বাইরেই উঠে যায়।
Floating point — 0.1 + 0.2 কেন 0.3 নয়
কম্পিউটার দশমিক সংখ্যা সাধারণত IEEE 754 floating-point আকারে রাখে, যেখানে ভিত্তি ২. দশমিকের ১/১০ ভগ্নাংশটি ২-ভিত্তিতে ঠিক যেমন ১/৩ দশমিকে — অসীম পুনরাবৃত্তি (০.৩৩৩…)। তাই 0.1 আসলে 0.1-এর খুব কাছের একটি সংখ্যা, আর যোগফলে ক্ষুদ্র ভুল জমে: 0.1 + 0.2 হয় 0.30000000000000004. এর মানে test-এ দশমিক ফল হুবহু সমতা দিয়ে মেলানো যায় না; একটি সহনসীমা (tolerance) লাগে:
assertAlmostEqual(a, b, places=7):round(a - b, 7) == 0হলে পাশ (default ৭ ঘর)। চাইলেdelta=0.01দিয়ে সরাসরি সীমা দেওয়া যায়।pytest.approx(b): default-এ আপেক্ষিক সহনসীমা প্রায় 1e-6 (pytest-এর নথিতে বিস্তারিত; সংস্করণভেদে দেখে নিন)।- Jest-এর
toBeCloseTo(b, digits):|b − a| < 10^(−digits) / 2হলে পাশ, defaultdigits = 2.
টাকার হিসাবে কিন্তু সহনসীমা দিয়ে লুকানো সমাধান নয়: সেখানে পয়সায় পূর্ণসংখ্যা বা Python-এর decimal.Decimal ব্যবহার করুন, যাতে হিসাবটাই হুবহু হয়। Tolerance দরকার বৈজ্ঞানিক বা পরিমাপের হিসাবে, যেখানে ক্ষুদ্র ভুল স্বাভাবিক। নিচের ছোট harness-টি (৩২ লাইন) Jest-এর মতো test, test.each আর expect নিজেই বানায়, তারপর একটি কাল্পনিক বাজার app-এর বিল ভাগ করার function পরীক্ষা করে:
// A tiny Jest-style harness: test, test.each, expect().toBe / toBeCloseTo / toThrow
function test(name, fn) {
try { fn(); console.log("PASS " + name); }
catch (e) { console.log("FAIL " + name + " -> " + e.message); }
}
test.each = (rows) => (title, fn) => rows.forEach((row) => {
let i = 0;
test(title.replace(/%s/g, () => String(row[i++])), () => fn(...row));
});
function expect(actual) {
return {
toBe(exp) { if (!Object.is(actual, exp)) throw new Error(`expected ${exp}, got ${actual}`); },
toBeCloseTo(exp, digits = 2) { // same rule as Jest: |exp - actual| < 10^-digits / 2
if (!(Math.abs(exp - actual) < 10 ** -digits / 2)) throw new Error(`expected ~${exp}, got ${actual}`);
},
toThrow(part) {
try { actual(); } catch (e) { if (e.message.includes(part)) return; throw new Error("wrong error: " + e.message); }
throw new Error("expected a throw, nothing was thrown");
},
};
}
// Code under test (fictional bazaar app): split a bill between friends
function splitBill(total, people) {
if (people < 1) throw new Error("people must be at least 1");
return total / people;
}
test("0.1 + 0.2 toBe 0.3", () => expect(0.1 + 0.2).toBe(0.3));
test("0.1 + 0.2 toBeCloseTo 0.3", () => expect(0.1 + 0.2).toBeCloseTo(0.3));
test.each([[100, 4, 25], [100, 3, 33.33], [0.3, 3, 0.1]])(
"splitBill(%s, %s) is about %s", (total, people, share) => expect(splitBill(total, people)).toBeCloseTo(share));
test("splitBill(100, 3) toBe 33.33", () => expect(splitBill(100, 3)).toBe(33.33));
test("splitBill with 0 people throws", () => expect(() => splitBill(500, 0)).toThrow("at least 1"));আসল আউটপুট:
FAIL 0.1 + 0.2 toBe 0.3 -> expected 0.3, got 0.30000000000000004
PASS 0.1 + 0.2 toBeCloseTo 0.3
PASS splitBill(100, 4) is about 25
PASS splitBill(100, 3) is about 33.33
PASS splitBill(0.3, 3) is about 0.1
FAIL splitBill(100, 3) toBe 33.33 -> expected 33.33, got 33.333333333333336
PASS splitBill with 0 people throws
প্রথম লাইনটিই ফাঁদ: হুবহু সমতায় 0.1 + 0.2 ব্যর্থ, কারণ আসল মান 0.30000000000000004. একই মান toBeCloseTo-এ পাশ, কারণ পার্থক্য প্রায় ৫.৬ × ১০−১৭, যা সীমা ০.০০৫-এর অনেক নিচে। test.each একটি টেবিলের প্রতিটি সারি থেকে আলাদা নামসহ আলাদা test বানিয়েছে — নামে %s-এর জায়গায় সারির মান বসেছে, তাই কোন সারি ব্যর্থ হলো তা নাম দেখেই বোঝা যায়। splitBill(100, 3) হলো ৩৩.৩৩৩…, তাই toBeCloseTo(33.33) পাশ (পার্থক্য ০.০০৩৩ < ০.০০৫), কিন্তু toBe(33.33) ব্যর্থ। শেষ test-এ function-টি () => splitBill(500, 0) আকারে দেওয়া হয়েছে, তাই exception-টি toThrow-এর ভেতরে ধরা পড়েছে, আর বার্তায় “at least 1” মিলেছে।
Collection — ক্রম জরুরি কি না, আগে ঠিক করুন
একটি function যদি “এই এলাকার সব rider-এর নাম” ফেরত দেয়, আর ক্রম চুক্তির অংশ না হয়, তাহলে assertEqual(result, ["Jamal", "Karim"]) লিখলে test অকারণে ক্রমের ওপর নির্ভর করবে। এমন ক্ষেত্রে assertCountEqual (নাম বিভ্রান্তিকর, কিন্তু কাজ হলো “ক্রম বাদে একই উপাদান, একই সংখ্যায়”) বা সাজিয়ে তুলনা করুন। উল্টোভাবে, যদি ক্রম চুক্তির অংশ হয় (যেমন “সবচেয়ে কাছের rider আগে”), তাহলে ঠিক ক্রমটিই যাচাই করুন। JavaScript-এ array বা object-এর গঠন মেলাতে toBe নয়, toEqual লাগে।
৫ · Fixture আর parameterized test — শুরুর অবস্থা আর অনেক input
প্রতিটি test-এর Arrange অংশে একই কয়েকটি লাইন বারবার এলে সেগুলো এক জায়গায় সরানো যায়। Test শুরুর আগের এই নির্দিষ্ট অবস্থার নাম test fixture — যেমন ক্রিকেট ম্যাচের আগে প্রতিবার পিচ রোল করা আর দাগ টানা। xUnit-এ fixture তৈরির জায়গা হলো কয়েকটি বিশেষ method:
setUp()— প্রতিটি test-এর ঠিক আগে চলে;tearDown()— প্রতিটি test-এর পরে, test ব্যর্থ হলেও (তবেsetUpনিজেই ব্যর্থ হলেtearDownচলে না)।setUpClass()/tearDownClass()— পুরো class-এর জন্য একবার; ব্যয়বহুল কিন্তু শুধু-পড়ার (read-only) জিনিসের জন্য, যেমন বড় একটি rate table পড়া। এখানে বদলানো যায় এমন object রাখলে test-গুলো একে অপরকে প্রভাবিত করবে (অংশ ৬)।addCleanup(fn)— যেখানে কিছু তৈরি হলো, সেখানেই তার পরিষ্কারের কাজ নথিভুক্ত করা; ক্রমটি উল্টো (শেষে যোগ করা আগে চলে)।
Fixture-এর জীবনচক্র — দুটি test-এর একটি class
setUpClass একবার; প্রতিটি test-এর জন্য নতুন করে setUp আর tearDown
নিচের stepper-এ একই class লাইন ধরে চালিয়ে দেখুন — কোন লাইন কখন চলে, আর অস্থায়ী ফোল্ডারে কী থাকে। unittest একটি class-এর test method-গুলো default-এ নামের বর্ণানুক্রমে চালায়, তাই test_a… আগে, test_b… পরে।
1class ReceiptTest(unittest.TestCase):2 @classmethod3 def setUpClass(cls):4 cls.rates = load_rates() # once5 def setUp(self):6 self.tmp = tempfile.TemporaryDirectory()7 self.calc = FeeCalculator(self.rates)8 def tearDown(self):9 self.tmp.cleanup()10 def test_a_writes_receipt(self):11 save_receipt(self.tmp.name, self.calc.fee(1, "city"))12 self.assertEqual(len(os.listdir(self.tmp.name)), 1)13 def test_b_folder_starts_empty(self):14 self.assertEqual(os.listdir(self.tmp.name), [])15 @classmethod16 def tearDownClass(cls):17 del cls.rates
| চলছে | |
| cls.rates | |
| self.tmp | |
| self.calc |
setUpClass— একবার। Rate table পড়া হলোcls.rates-এ। এটি শুধু পড়া হবে, কেউ বদলাবে না — তাই ভাগ করা নিরাপদ। এখনো কোনো test শুরু হয়নি, কোনো অস্থায়ী ফোল্ডারও নেই।setUp— test_a-এর আগে। নতুন অস্থায়ী ফোল্ডার tmpA (খালি) আর নতুনFeeCalculator(#১) তৈরি হলো।tempfileপ্রতিবার অনন্য নামের ফোল্ডার বানায়।test_a_writes_receipt. রসিদ ফাইল লেখা হলো tmpA-তে; এখন সেখানে ১টি ফাইল, assertion পাশ।tearDown. tmpA পুরোটা মুছে গেল, calc #১-ও আর ব্যবহার হবে না। Test ব্যর্থ হলেও এই ধাপ চলত, তাই অস্থায়ী ফাইল জমে পরের test বা পরের দিনের run নষ্ট করে না।setUp— test_b-এর আগে, আবার নতুন করে। tmpB (খালি) আর calc #২. আগের test-এর কোনো চিহ্ন এখানে পৌঁছায় না — এটাই isolation. শুধুcls.ratesএকই আছে।test_b_folder_starts_empty. ফোল্ডার খালি, পাশ। যদি দুটি test একটি ফোল্ডার ভাগ করত, test_a আগে চললে এই test ব্যর্থ হতো, আর একা চালালে পাশ — ক্রমনির্ভর test-এর আদর্শ উদাহরণ (অংশ ৬)।tearDown. tmpB মুছে গেল। যেন প্রতিটি রোগীর আগে ও পরে ডাক্তারের হাত ধোয়া।tearDownClass— একবার, সবার শেষে। ভাগ করা resource ছেড়ে দেওয়া হলো। Runner জানায়Ran 2 testsআরOK. মোট: setUpClass ১ বার, setUp ২ বার, tearDown ২ বার, tearDownClass ১ বার।
pytest fixture আর tmp_path
pytest-এ fixture হলো একটি সাধারণ function, যার ওপর @pytest.fixture লেখা; test শুধু parameter-এর নামে সেটি চায়, আর pytest নাম মিলিয়ে দিয়ে দেয় — একধরনের dependency injection. yield-এর পরের অংশটি tearDown-এর কাজ করে। tmp_path হলো pytest-এর নিজস্ব একটি fixture, যা প্রতিটি test-কে একটি নতুন অস্থায়ী ফোল্ডার (pathlib.Path) দেয় — pytest-এর নতুন সংস্করণগুলোতে এটিই সুপারিশকৃত; পুরোনো tmpdir-ও আছে। (চালানোর জন্য নয়, আকার দেখাতে:)
import pytest
@pytest.fixture
def calc():
c = FeeCalculator()
yield c # the test runs here
c.close() # teardown part
def test_receipt_file_written(calc, tmp_path):
save_receipt(tmp_path, calc.fee(1, "city"))
assert (tmp_path / "receipt.txt").read_text() == "fee=60\n"Test data builder — অপ্রাসঙ্গিক খুঁটিনাটি লুকানো
একটি অর্ডার object-এ যদি বারোটি field থাকে (গ্রাহক, ঠিকানা, ফোন, আইটেম, zone, কুপন…), আর একটি test শুধু zone নিয়ে চিন্তিত, তাহলে বারোটি field-ই test-এ লেখা পাঠকের চোখ সরিয়ে দেয়। Test data builder — Nat Pryce-এর লেখার মাধ্যমে প্রায় ২০০৭ সাল থেকে পরিচিত, আর পর্ব ৩৩-এর builder pattern-এর ছোট আত্মীয় — যুক্তিসংগত default দিয়ে একটি বৈধ object বানায়, আর test শুধু যা প্রাসঙ্গিক তা বদলায়:
def an_order(**changes):
order = dict(customer="Test Customer", zone="city", weight_kg=1, coupon=None)
order.update(changes)
return order
def test_outside_zone_costs_more(self):
city = self.calc.fee_for(an_order())
outside = self.calc.fee_for(an_order(zone="outside"))
self.assertGreater(outside, city)Parameterized test — এক কাঠামো, অনেক সারি
অংশ ৩-এর loop-ওয়ালা test-এর সঠিক রূপ হলো parameterized test: input আর প্রত্যাশিত ফলের একটি টেবিল, আর প্রতিটি সারি আলাদা test হিসেবে রিপোর্ট হয়। unittest-এ এর জন্য আছে self.subTest(...) (Python 3.4 থেকে) — একটি সারি ব্যর্থ হলেও বাকি সারিগুলো চলে, আর রিপোর্টে কোন সারি ব্যর্থ তা দেখায়। pytest-এ @pytest.mark.parametrize("weight,zone,expected", [...]), আর Jest/Vitest-এ test.each (Jest-এ সংস্করণ ২৩ থেকে আছে বলে জানা যায়)। কোন সারিগুলো বেছে নেবেন — যেমন সীমার ঠিক আগের, ঠিক ওপরের আর ঠিক পরের মান — সেটি পর্ব ৪৭-এর (Test case নকশা) মূল বিষয়: equivalence partitioning আর boundary value.
এবার সব একসাথে: AAA, setUp, subTest দিয়ে টেবিল আর assertRaises. Suite চালানো হয়েছে TextTestRunner(stream=sys.stdout, verbosity=2) দিয়ে, যাতে প্রতিটি test-এর নাম আর ফল দেখা যায়।
import math, sys, unittest
class FeeCalculator: # fictional courier: delivery fee in Taka
RATES = {"city": (60, 15), "outside": (120, 25)} # (first kg, each extra kg)
def fee(self, weight_kg, zone):
if weight_kg <= 0:
raise ValueError("weight must be positive")
if zone not in self.RATES:
raise ValueError(f"unknown zone: {zone}")
first, extra = self.RATES[zone]
return first + extra * (math.ceil(weight_kg) - 1)
class FeeCalculatorTest(unittest.TestCase):
def setUp(self): # runs before EVERY test: fresh object
self.calc = FeeCalculator()
def test_fee_city_one_kg_is_base_rate(self):
weight, zone = 1, "city" # Arrange
result = self.calc.fee(weight, zone) # Act
self.assertEqual(result, 60) # Assert
def test_fee_rounds_part_kg_up(self):
cases = [(0.5, "city", 60), (1.2, "city", 75),
(3, "outside", 170), (2.01, "outside", 170)]
for weight, zone, expected in cases:
with self.subTest(weight=weight, zone=zone):
self.assertEqual(self.calc.fee(weight, zone), expected)
def test_fee_zero_weight_raises(self):
with self.assertRaises(ValueError):
self.calc.fee(0, "city")
def test_fee_unknown_zone_raises_with_message(self):
with self.assertRaisesRegex(ValueError, "unknown zone"):
self.calc.fee(1, "moon")
suite = unittest.defaultTestLoader.loadTestsFromTestCase(FeeCalculatorTest)
unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)আসল আউটপুট (শেষ লাইনের সময়টি যন্ত্রভেদে সামান্য আলাদা হতে পারে):
test_fee_city_one_kg_is_base_rate (__main__.FeeCalculatorTest.test_fee_city_one_kg_is_base_rate) ... ok
test_fee_rounds_part_kg_up (__main__.FeeCalculatorTest.test_fee_rounds_part_kg_up) ... ok
test_fee_unknown_zone_raises_with_message (__main__.FeeCalculatorTest.test_fee_unknown_zone_raises_with_message) ... ok
test_fee_zero_weight_raises (__main__.FeeCalculatorTest.test_fee_zero_weight_raises) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
চারটি test-ই পাশ, আর খেয়াল করুন ক্রমটি: unknown_zone এসেছে zero_weight-এর আগে, যদিও কোডে পরে লেখা — কারণ unittest নামের বর্ণানুক্রমে চালায় (“u” আগে, “z” পরে)। এ কারণেই test কখনো লেখার ক্রমের ওপর নির্ভর করা উচিত নয়। টেবিলের হিসাবগুলো হাতে মেলানো: ০.৫ কেজি ওপরের দিকে ১ কেজি, তাই ৬০; ১.২ কেজি → ২ কেজি → ৬০ + ১৫ = ৭৫; ৩ কেজি বাইরে → ১২০ + ২৫ × ২ = ১৭০; আর ২.০১ কেজি → ৩ কেজি → আবারও ১৭০. যদি কোনো সারি ভুল হতো, subTest সেই সারির weight আর zone-সহ আলাদা একটি FAIL দেখাত, আর বাকি সারিগুলো তবুও চলত। setUp প্রতিটি test-এর আগে নতুন FeeCalculator দিয়েছে; Exception-এর দুটি test আচরণের অর্ধেক চুক্তি ঢেকেছে, আর দ্বিতীয়টি বার্তাও মিলিয়েছে।
৬ · Deterministic test — সময়, এলোমেলোতা আর ক্রম
সবচেয়ে সহজে test করা যায় pure function: একই input-এ সবসময় একই output, বাইরের কিছু পড়েও না, বদলায়ও না। fee(2.5, "outside") চিরকাল ১৭০. সমস্যা শুরু হয় যখন কোড চুপচাপ বাইরের জগৎ থেকে কিছু পড়ে — আজকের তারিখ, এলোমেলো সংখ্যা, environment variable, ফাইল, network — বা বাইরে কিছু লেখে (side effect). এগুলো function-এর signature-এ দেখা যায় না, অর্থাৎ hidden dependency. ফলে test আর deterministic থাকে না: কোড না বদলালেও আজ পাশ, কাল ফেল। এমন test-এর নাম flaky test.
রেলের সময়সূচির একটি পরীক্ষা ভাবুন: “আন্তঃনগর ট্রেন কি দেরিতে চলছে?” যদি পরীক্ষক প্রতিবার দেয়ালের ঘড়ি দেখে উত্তর মেলান, তাহলে সকালে এক উত্তর, বিকেলে আরেক। কিন্তু যদি প্রশ্নটি হয় “ট্রেন ৪টায় ছাড়ার কথা, এখন যদি ৪:২০ হয়, তাহলে কি দেরি?” — উত্তর সবসময় একই। ঘড়িটিকে প্রশ্নের অংশ বানিয়ে দেওয়াই সমাধান।
কৌশলটি পর্ব ৩৭-এর (Dependency injection আর composition) সরাসরি প্রয়োগ: সময় বা এলোমেলোতা function-এর ভেতরে না পড়ে বাইরে থেকে দিন — মান হিসেবে (today), একটি clock object হিসেবে, বা একটি function হিসেবে (choose). Production-এ আসল ঘড়ি বসবে, test-এ একটি নির্দিষ্ট তারিখ। এলোমেলোতার ক্ষেত্রে আরেকটি পথ হলো নির্দিষ্ট seed-সহ একটি PRNG (random.Random(42)) দেওয়া; তবে seed-নির্ভর নির্দিষ্ট মান assert করলে test PRNG-এর ভেতরের algorithm-এ বাঁধা পড়ে, তাই সম্ভব হলে আচরণের ধর্ম যাচাই করুন (“ফল তালিকার ভেতরেই আছে”)।
নিচের কোডে প্রথম দুটি test লুকানো ঘড়ি আর লুকানো এলোমেলোতা ব্যবহার করে; পরের তিনটি একই আচরণ deterministic উপায়ে যাচাই করে। প্রতিটি test ৩০ বার চালানো হয়েছে, যেন ৩০টি CI run.
import datetime as dt, random, unittest
RIDERS = ["Karim", "Jamal", "Rafiq"]
def is_late_v1(due): # hidden clock: reads today's real date
return dt.date.today() > due
def pick_rider_v1(riders): # hidden randomness
return random.choice(riders)
def is_late(due, today): # the date comes in as a value
return today > due
def pick_rider(riders, choose): # the "random" choice comes in as a function
return choose(riders)
class FlakyTests(unittest.TestCase):
def test_not_late_v1(self): # written in 2025, when it passed
self.assertFalse(is_late_v1(dt.date(2025, 12, 31)))
def test_pick_rider_v1(self):
self.assertEqual(pick_rider_v1(RIDERS), "Karim")
class DeterministicTests(unittest.TestCase):
def test_not_late_on_due_date(self):
self.assertFalse(is_late(dt.date(2025, 12, 31), today=dt.date(2025, 12, 31)))
def test_late_one_day_after(self):
self.assertTrue(is_late(dt.date(2025, 12, 31), today=dt.date(2026, 1, 1)))
def test_pick_rider_uses_choice(self):
self.assertEqual(pick_rider(RIDERS, choose=lambda r: r[-1]), "Rafiq")
def run(cls, times):
for name in unittest.defaultTestLoader.getTestCaseNames(cls):
passed = 0
for _ in range(times):
result = unittest.TestResult()
cls(name).run(result)
passed += result.wasSuccessful()
print(f"{name:28} passed {passed}/{times}")
random.seed(46) # fixed only so this page shows the same counts
run(FlakyTests, 30)
run(DeterministicTests, 30)আসল আউটপুট:
test_not_late_v1 passed 0/30
test_pick_rider_v1 passed 12/30
test_late_one_day_after passed 30/30
test_not_late_on_due_date passed 30/30
test_pick_rider_uses_choice passed 30/30
test_not_late_v1 একটি “time bomb”: ২০২৫ সালে লেখার সময় আজকের তারিখ ৩১ ডিসেম্বর ২০২৫-এর আগে ছিল, তাই পাশ করত। ২০২৬ সাল থেকে প্রতিটি run-এ ব্যর্থ (০/৩০) — কোডের এক অক্ষরও না বদলে। test_pick_rider_v1 পাশ করেছে ৩০ বারে মাত্র ১২ বার — তিনজন rider থেকে “Karim” আসার সম্ভাবনা ১/৩, আর ৩০-এর ১/৩ হলো ১০-এর কাছাকাছি। পৃষ্ঠায় প্রতিবার একই সংখ্যা দেখাতে শুধু এখানে random.seed(46) দেওয়া হয়েছে; আসল CI-তে seed না থাকায় প্রতিটি run নতুন ভাগ্য। নিচের তিনটি test-এ তারিখ আর “বাছাই” বাইরে থেকে এসেছে, তাই ৩০/৩০ — এবং প্রথম দুটি সীমানাও (ঠিক due date-এর দিন আর তার পরদিন) স্পষ্ট করে ধরেছে। এখানে choose=lambda r: r[-1] একটি অতি সরল stub-এর কাজ করছে; বাছাইয়ের আসল নিয়ম (যেমন সবচেয়ে কাছের rider) থাকলে সেটির জন্য আলাদা deterministic test লিখতেন।
ক্রমনির্ভর test — ভাগ করা mutable state
তৃতীয় প্রচলিত উৎস হলো test-গুলোর মধ্যে ভাগ করা, বদলানো যায় এমন অবস্থা: একটি class attribute, module-স্তরের list, global singleton, বা একটি ডেটাবেস টেবিল যা কেউ পরিষ্কার করে না। একটি test তাতে কিছু রেখে যায়, পরেরটি সেটি দেখে অবাক হয়। এর নাম test interdependence, আর এর বৈশিষ্ট্য হলো “একা চালালে পাশ, সবার সাথে চালালে ফেল” (বা উল্টো)। নিচে একই দুটি test দুই ক্রমে চালানো হয়েছে — প্রথমে একটি ভাগ করা cart-এ, তারপর setUp-এ প্রতিবার নতুন cart দিয়ে:
import unittest
class Cart: # fictional grocery app
def __init__(self): self.items = []
def add(self, name, price): self.items.append((name, price))
def total(self): return sum(price for _, price in self.items)
class SharedCartTest(unittest.TestCase):
cart = Cart() # ONE cart shared by every test
def test_add_item(self):
self.cart.add("rice", 80)
self.assertEqual(self.cart.total(), 80)
def test_new_cart_is_empty(self):
self.assertEqual(self.cart.total(), 0)
class FreshCartTest(SharedCartTest): # same two tests ...
def setUp(self):
self.cart = Cart() # ... but a new cart before EACH test
def run(cls, order):
SharedCartTest.cart = Cart() # start every session clean
result = unittest.TestResult()
unittest.TestSuite(cls(name) for name in order).run(result)
failed = [test._testMethodName for test, _ in result.failures]
print(f"{cls.__name__:14} {' -> '.join(order)}: failed {failed or 'none'}")
A = ["test_new_cart_is_empty", "test_add_item"]
B = ["test_add_item", "test_new_cart_is_empty"]
run(SharedCartTest, A)
run(SharedCartTest, B)
run(FreshCartTest, A)
run(FreshCartTest, B)আসল আউটপুট:
SharedCartTest test_new_cart_is_empty -> test_add_item: failed none
SharedCartTest test_add_item -> test_new_cart_is_empty: failed ['test_new_cart_is_empty']
FreshCartTest test_new_cart_is_empty -> test_add_item: failed none
FreshCartTest test_add_item -> test_new_cart_is_empty: failed none
একই দুটি test, একই কোড — শুধু ক্রম বদলেছে, আর ফল বদলে গেছে। SharedCartTest-এ cart একটি class attribute, তাই দুটি test একই object ভাগ করে। খালি-যাচাই আগে চললে (ক্রম A) দুটিই পাশ। কিন্তু test_add_item আগে চললে (ক্রম B) সে নিজে পাশ করে, অথচ cart-এ ৮০ টাকার চাল রেখে যায়; পরে test_new_cart_is_empty total ৮০ পেয়ে ব্যর্থ — যদিও Cart-এর কোডে কোনো ভুল নেই। ভুলটা test-এর নকশায়। FreshCartTest একই দুটি test উত্তরাধিকারসূত্রে পায়, শুধু setUp-এ প্রতিবার নতুন Cart বানিয়ে self.cart-এ রাখে (instance attribute class attribute-কে ঢেকে দেয়), তাই দুই ক্রমেই কোনো ব্যর্থতা নেই। বাস্তবে এই bug প্রায়ই দেখা দেয় নতুন test যোগ করলে বা CI test-গুলো সমান্তরালে চালাতে শুরু করলে।
৭ · Test smell — যে লক্ষণ দেখে test সন্দেহ করবেন
পর্ব ৪২-এ (Code smell আর refactoring) আপনি production কোডের code smell দেখেছেন। Test কোডেরও নিজস্ব smell আছে — test smell. শব্দটির প্রথম তালিকা Arie van Deursen ও সহকর্মীদের ২০০১ সালের “Refactoring Test Code” গবেষণাপত্রে পাওয়া যায় বলে সাধারণত উল্লেখ করা হয়; পরে Gerard Meszaros-এর xUnit Test Patterns (২০০৭) বইয়ে বিস্তৃত ক্যাটালগ আসে। নিচের ছয়টি সবচেয়ে প্রচলিত:
| Smell | লক্ষণ | ক্ষতি | সারানোর উপায় |
|---|---|---|---|
| Assertion roulette | এক test-এ বার্তা ছাড়া অনেক assertion, প্রায়ই ভিন্ন আচরণের | লাল হলে কোনটি ভাঙল, খুঁজতে সময় যায় | এক আচরণে এক test; দরকারে msg= বা subTest |
| Mystery guest | Test বাইরের ফাইল বা shared ডেটা পড়ে (fixtures/data.json), যা test-এ দেখা যায় না | কেন ফলটি প্রত্যাশিত, বোঝা যায় না; কেউ ফাইল বদলালে অজানা test ভাঙে | প্রাসঙ্গিক ডেটা test-এর ভেতরে বা builder-এ |
| Fragile test | আচরণ ঠিক থাকলেও বিন্যাস, ভেতরের গঠন বা ডাকার ক্রম বদলালে ভাঙে | Refactoring-এ ভয়; দল test “ঠিক করতে করতে” ক্লান্ত | প্রকাশ্য আচরণ যাচাই, খুঁটিনাটি নয় |
| Slow test | sleep, আসল network বা database, বিশাল fixture | মানুষ test চালানো বন্ধ করে; feedback দেরিতে | সময় inject, test double, ছোট fixture |
| Test interdependence | ভাগ করা mutable state, ক্রমনির্ভরতা | একা পাশ, সবাই মিলে ফেল; ক্রম বদলালে অন্য ফল | প্রতি test-এ নতুন fixture; এলোমেলো ক্রমে চালিয়ে দেখা |
| Over-specified test | Private field, ঠিক কতবার কোন method ডাকা হলো, পুরো লেখার প্রতিটি অক্ষর | Fragile test-এর প্রধান কারণ; বাস্তবায়নের আয়না | শুধু চুক্তির অংশটুকু assert |
Fragile test সবচেয়ে ক্ষতিকর, কারণ এটি refactoring-এর মূল প্রতিশ্রুতি ভাঙে — “বাইরের আচরণ না বদলালে test সবুজ থাকবে” (পর্ব ৪২)। নিচের stepper-এ একটি fragile test ধাপে ধাপে সারানো হলো। কাল্পনিক বাজার app-এর receipt(order) একটি রসিদের লেখা বানায়।
| Assertion | |
| আজকের ঘটনা | |
| ফল | |
| রায় |
- মূল test.
assertEqual(receipt(order), "Bazaar Receipt …")— পুরো লেখা হুবহু, তারিখসহ।receiptনিজেdate.today()ডেকে তারিখ বসায়। লেখার দিন পাশ, তাই কারো সন্দেহ হয়নি। - পরদিনই লাল। কোড এক অক্ষরও বদলায়নি, কিন্তু তারিখ বদলেছে। এটি অংশ ৬-এর লুকানো ঘড়ি — test flaky, আর দল শিখছে “লাল মানে কিছু না”।
- আবার লাল, অপ্রাসঙ্গিক কারণে। Designer শেষে একটি ধন্যবাদের লাইন দিলেন। Fee, total, আইটেম সব ঠিক; তবুও test ভাঙল, কারণ এটি এমন খুঁটিনাটি বেঁধে রেখেছে যা আচরণের অংশ নয় — over-specified.
- প্রথম সারাই: ঘড়ি বাইরে থেকে।
receipt(order, today=date(2026, 3, 1))— পর্ব ৩৭-এর মতো dependency signature-এ এল। তারিখের কারণে আর ভাঙবে না, কিন্তু অক্ষর-মেলানো রয়ে গেছে, তাই “Thank you!” লাইনের জন্য এখনো লাল। - দ্বিতীয় সারাই: আচরণ যাচাই। হিসাব আর বিন্যাস আলাদা করা হলো:
receipt_data(order)দেয় total আর আইটেমের তালিকা, আর test শুধুtotal == 160ও("rice", 2, 160)আছে কি না দেখে। বিন্যাসের জন্য, দরকার হলে, একটিমাত্র আলাদা “golden” test রাখা যায়, যা জেনেশুনে হালনাগাদ করা হয়। এখন designer যা খুশি বদলান — test সবুজ। - যখন সত্যিই ভাঙা উচিত, তখনই ভাঙে। কেউ ভুলে discount দুবার কাটলেন; total ১৪০. Test লাল হলো, বার্তায় স্পষ্ট “140, প্রত্যাশা 160”. ভালো test-এর পরিচয় এটাই: শুধু আসল কারণে লাল হয়, আর লাল হলে কারণ বলে দেয়।
আরও গভীরে: Gerard Meszaros-এর xUnit Test Patterns: Refactoring Test Code (২০০৭) test smell, fixture-এর ধরন (fresh, shared, persistent) আর test double-এর নামকরণের সবচেয়ে বিস্তারিত সূত্র; “dummy, stub, spy, mock, fake” পরিভাষাও মূলত এখান থেকে জনপ্রিয়। Kent Beck-এর Test-Driven Development: By Example (২০০২) xUnit-এর ভেতরের নকশা দেখায় — বইয়ের একটি অংশে Python-এ একটি ছোট xUnit framework শূন্য থেকে বানানো হয়েছে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- Unit test কী, আর integration test থেকে পার্থক্য? — ছোট একটি আচরণ বিচ্ছিন্নভাবে, দ্রুত যাচাই; integration test একাধিক অংশ বা বাইরের ব্যবস্থা (database, network) একসাথে যাচাই করে।
- Arrange-Act-Assert ব্যাখ্যা করুন। — শুরুর অবস্থা সাজানো, যাচাইয়ের কাজটি একবার ডাকা, ফল প্রত্যাশার সাথে মেলানো; Given–When–Then একই কাঠামো।
- FIRST কী? — Fast, Independent, Repeatable, Self-validating, Timely — ভালো unit test-এর পাঁচ গুণ।
- setUp আর setUpClass-এর পার্থক্য? — setUp প্রতিটি test-এর আগে (নতুন fixture); setUpClass পুরো class-এ একবার, শুধু ব্যয়বহুল read-only জিনিসের জন্য।
- Flaky test কেন হয়, কীভাবে সারাবেন? — লুকানো সময়, এলোমেলোতা, ক্রম বা বাইরের ব্যবস্থা; dependency inject করে, প্রতি test-এ নতুন fixture দিয়ে আর বাইরের ব্যবস্থা test double দিয়ে বদলে।
৮ · Test সাজানো, ফাইলের নাম আর চালানো
Test runner হাজার ফাইলের ভেতর থেকে test খুঁজে নেয় নামের নিয়ম দেখে — একে বলে test discovery. তাই ফাইল আর function-এর নাম শুধু রুচির বিষয় নয়; ভুল নাম দিলে test চুপচাপ বাদ পড়ে, আর “সব পাশ” দেখে আপনি ভাববেন সব ঠিক।
| Tool | ফাইলের নাম (default) | Test-এর নাম | চালানোর কমান্ড |
|---|---|---|---|
Python unittest | test*.py | TestCase-এর ভেতরে test দিয়ে শুরু method | python -m unittest discover |
| pytest | test_*.py বা *_test.py | test দিয়ে শুরু function; Test দিয়ে শুরু class | pytest |
| Jest / Vitest | *.test.js, *.spec.js (বা __tests__ ফোল্ডার) | test(...) বা it(...) | npx jest, npx vitest |
এগুলো default; প্রতিটি tool-এর configuration-এ বদলানো যায়, আর সংস্করণভেদে খুঁটিনাটি আলাদা হতে পারে — নিজের project-এর নথি দেখুন। ফোল্ডার সাজানোর দুটি প্রচলিত ধরন: (১) কোডের পাশেই test (fee.js আর fee.test.js একই ফোল্ডারে), যা JavaScript-এ সাধারণ; (২) আলাদা tests/ ফোল্ডার, যার ভেতরে কোডের ফোল্ডার-কাঠামোর আয়না (src/courier/fee.py ↔ tests/courier/test_fee.py), যা Python-এ সাধারণ। যেটিই নিন, নিয়মটি হলো: যেকোনো কোড ফাইল দেখে তার test কোথায়, তা অনুমান করা যায়।
প্রতিদিনের কাজে পুরো suite নয়, শুধু প্রাসঙ্গিক অংশ চালানো যায়। নিচের কমান্ডগুলো উদাহরণ (আউটপুট দেখানো হয়নি):
# one test method with unittest
python -m unittest tests.test_fee.FeeCalculatorTest.test_fee_zero_weight_raises
# pytest: only tests whose name matches "zone", stop at first failure
pytest -k zone -x
# Jest: only tests whose name matches a pattern
npx jest -t "splitBill"আর দলের কাজে test চালানোর আসল জায়গা হলো continuous integration: প্রতিটি push বা pull request-এ একটি pipeline স্বয়ংক্রিয়ভাবে পুরো suite চালায়, আর লাল হলে merge আটকায়। কীভাবে pipeline সাজাবেন, দ্রুত feedback-এর জন্য কোন test আগে চালাবেন — সেটি পর্ব ৫৮-এর (Continuous integration) বিষয়। আর কত ভাগ কোড আপনার test চালায়, সেটি মাপার উপায় (coverage) আর তার সীমাবদ্ধতা পর্ব ৫০-এ (Coverage আর complexity).
এলোমেলো ক্রমে চালিয়ে দেখুন। ক্রমনির্ভর test খুঁজে বের করার সহজ উপায় হলো test-গুলো ইচ্ছা করে এলোমেলো ক্রমে চালানো। pytest-এর জন্য তৃতীয় পক্ষের plugin (যেমন pytest-randomly) এটি করে; Jest-এ একটি --randomize option আছে বলে জানা যায় (সংস্করণ দেখে নিন)। লাল হলে seed লিখে রাখুন, যাতে একই ক্রম আবার চালানো যায়।
৯ · AI-এর যুগে unit test
AI-কে “এই function-এর জন্য unit test লিখে দাও” বললে কয়েক সেকেন্ডে ত্রিশটি test চলে আসে, সবগুলো সবুজ। এটাই বিপদ। সবুজ test মানে শুধু এই যে test আর কোড একে অপরের সাথে মিলছে — কোড ঠিক কি না, তা নয়। AI-এর লেখা test-এ প্রায়ই যা দেখা যায়:
| AI-এর ভুল | কেন বিপজ্জনক | কীভাবে ধরবেন |
|---|---|---|
| প্রত্যাশিত মান কোডের সূত্র থেকেই হিসাব করে, বা কোড চালিয়ে যা এল তা-ই লিখে দেয় | Test বাস্তবায়নের আয়না; কোডের ভুলকেও “ঠিক” বলে | প্রতিটি প্রত্যাশিত মান হাতে বা requirement থেকে মেলান |
Assertion নেই, বা শুধু assertIsNotNone(result) | Test চলে, কিছুই যাচাই করে না | কোডে ইচ্ছা করে একটি ভুল ঢুকিয়ে দেখুন test লাল হয় কি না (mutation testing, পর্ব ৫০) |
| সব collaborator mock করে, তারপর mock-কেই যাচাই করে | আসল আচরণ কখনো চলেই না | Mock-এর সংখ্যা গুনুন; আসল কোড কতটুকু চলল দেখুন (পর্ব ৪৯) |
datetime.now(), random, sleep সরাসরি ব্যবহার | Flaky আর ধীর test | সময় আর এলোমেলোতা inject হয়েছে কি না দেখুন |
নেই এমন assertion বা option বানিয়ে বলে, বা বাদ পড়া পুরোনো নাম দেয় (যেমন assertAlmostEquals, যা Python 3.12-এ সরিয়ে দেওয়া হয়েছে) | Error, বা পুরোনো সংস্করণে আলাদা আচরণ | Framework-এর অফিসিয়াল নথি দেখুন; test একবার নিজে চালান |
| শুধু “সুখী পথ” — ভুল input, সীমা, exception বাদ | যেখানে bug থাকে, সেখানেই test নেই | পর্ব ৪৭-এর boundary value আর partition দিয়ে তালিকা মেলান |
AI-কে test লিখতে বললে requirement দিন, কোড নয় — নইলে সে কোডের আচরণই নকল করবে। আর প্রতিটি প্রত্যাশিত মানের পেছনের হিসাব চেয়ে নিন, যাতে আপনি মিলিয়ে দেখতে পারেন:
AI-এর লেখা test হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Unit” আসলে কতটুকু — London আর Detroit ধারার বিতর্ক কেন দুই দশক পরেও শেষ হয়নি?
কারণ দুটি ধারা দুটি ভিন্ন প্রশ্নের উত্তর খুঁজছে। Detroit বা classicist ধারার শিকড় Kent Beck-এর মূল TDD চর্চায়: test হলো আচরণের নিশ্চয়তা, আর “unit” হলো একটি আচরণ — দরকার হলে কয়েকটি class মিলে। Collaborator আসলটিই চলে, শুধু ধীর বা অনিয়ন্ত্রিত জিনিস (database, network, ঘড়ি) বদলানো হয়। এর বড় লাভ: ভেতরের গঠন বদলালে, class ভাঙলে বা জোড়া লাগালে test টেকে, তাই refactoring নিরাপদ।
London বা mockist ধারা, যার বিস্তারিত রূপ Steve Freeman ও Nat Pryce-এর ২০০৯ সালের বইয়ে, test-কে নকশার হাতিয়ারও মনে করে। Class-এর প্রতিটি collaborator-কে mock দিয়ে বদলে test লিখতে গেলে আপনি বাধ্য হন collaborator-এর interface আগে ঠিক করতে — “ওপর থেকে নিচে” নকশা হয়, আর object-গুলোর মধ্যে বার্তা আদান-প্রদান (role) স্পষ্ট হয়। ব্যর্থ হলে ঠিক কোন class দোষী, তা নিশ্চিত, কারণ বাকিরা নকল।
দুই পক্ষের সমালোচনাও বাস্তব। Mockist test বাস্তবায়নে বাঁধা পড়ে: method-এর নাম বা ডাকার ক্রম বদলালেই ভাঙে, যদিও ফল একই — fragile test. আর mock যদি আসল collaborator-এর আচরণ ভুলভাবে নকল করে, তাহলে সব unit test সবুজ থেকেও সিস্টেম ভাঙা থাকতে পারে; এজন্য London ধারার চর্চায় integration বা contract test আরও জরুরি (পর্ব ৫১)। উল্টোদিকে classicist test-এ একটি গভীর class-এর একটি ভুল পঞ্চাশটি test লাল করে দিতে পারে, আর কারণ খুঁজতে হয়।
Martin Fowler ২০০৭ সালের “Mocks Aren't Stubs” প্রবন্ধে নিজেকে classicist বলেছিলেন, কিন্তু অন্য ধারার যুক্তিও সম্মানের সাথে ব্যাখ্যা করেছিলেন — সেই প্রবন্ধ আজও এই বিতর্কের সবচেয়ে উদ্ধৃত সূত্রগুলোর একটি। বাস্তবে বেশিরভাগ দল মিশ্রণ ব্যবহার করে: domain logic-এ sociable test (আসল value object, আসল হিসাবের class), আর সিস্টেমের সীমানায় — payment gateway, email, ঘড়ি — test double. পর্ব ৪০-এর hexagonal architecture-এ এই সীমানাগুলোই port.
মূল উপলব্ধি: “Unit” কোনো প্রাকৃতিক মাপ নয়, একটি সিদ্ধান্ত। প্রশ্নটি হওয়া উচিত “কোন বদলে এই test ভাঙা উচিত, আর কোন বদলে নয়?” — উত্তর যা বলে, unit ততটুকু।
প্র ০২SUnit থেকে JUnit — কয়েকশো লাইনের একটি framework কীভাবে পুরো শিল্পের অভ্যাস বদলে দিল?
১৯৯০-এর দশকের শুরুতে অনেক প্রতিষ্ঠানে testing মানে ছিল আলাদা একটি দল, ছাপানো test plan আর হাতে চালানো পরীক্ষা — release-এর ঠিক আগে। Programmer-রা নিজেদের কোড যাচাই করতেন print দিয়ে, চোখে দেখে, আর সেই যাচাই পরের বার আবার করা যেত না। Kent Beck Smalltalk-এ যে ছোট framework লেখেন (পরে SUnit), তার মূল চিন্তা ছিল খুব সরল: programmer নিজেই test লিখবেন, কোডের ভাষাতেই; test নিজেই রায় দেবে; আর সব test এক চাপে আবার চালানো যাবে। এখানেই চারটি ধারণা — test case, fixture, assertion, runner — প্রথম একসাথে আসে, আর “fixture” শব্দটির ব্যবহারও সেই লেখায় পাওয়া যায় বলে বলা হয়।
১৯৯৭ সালে Beck আর Erich Gamma যখন Java-র জন্য JUnit লেখেন, Java তখন দ্রুত জনপ্রিয় হচ্ছে, আর ইন্টারনেটের মাধ্যমে বিনামূল্যে বিতরণ সহজ। JUnit ছোট ছিল, বোঝা সহজ, আর IDE-গুলো দ্রুত এর জন্য সবুজ-লাল বার দেখানো শুরু করে। “Green bar” সংস্কৃতির জন্ম হয় — প্রতিটি ছোট বদলের পর test চালানো। Martin Fowler JUnit সম্পর্কে একটি বিখ্যাত মন্তব্য করেছিলেন বলে প্রচলিত: সফটওয়্যারের ইতিহাসে এত কম লাইনের কোডের কাছে এত মানুষ এত ঋণী আর কখনো হয়নি।
তারপর আসে ধারাবাহিক অনুকরণ: NUnit (.NET), CppUnit, PHPUnit, আর Python-এ PyUnit, যা ২০০১ সালে standard library-তে unittest নামে ঢোকে — এজন্যই unittest-এর method-এর নাম (setUp, assertEqual) Python-এর স্বাভাবিক snake_case নয়, Java-র camelCase.
এই প্রভাবের পেছনে প্রযুক্তির চেয়ে সংস্কৃতি বেশি। XP (১৯৯৯ সালের Beck-এর বই) আর পরে Agile আন্দোলন developer testing-কে পেশার অংশ বানায়; TDD (Beck, ২০০২) test-কে নকশার আগে নিয়ে আসে; CI (পর্ব ৫৮) প্রতিটি বদলে test চালানোকে স্বয়ংক্রিয় করে। আজ একটি open-source project-এ test ছাড়া pull request দিলে review-তে প্রথম প্রশ্নই হয় “test কোথায়?”
মূল উপলব্ধি: সবচেয়ে প্রভাবশালী হাতিয়ার প্রায়ই সবচেয়ে ছোট — যদি সেটি একটি ভালো অভ্যাসকে সস্তা আর আনন্দদায়ক করে দেয়। Test লেখার খরচ কমেছিল বলেই মানুষ test লিখতে শুরু করে।
প্র ০৩Flaky test কেন একটি দলের জন্য “কোনো test না থাকার” চেয়েও খারাপ হতে পারে?
একটি test suite-এর মূল্য একটি সংকেতে: লাল মানে কিছু ভেঙেছে। Flaky test এই সংকেতকে শব্দে (noise) পরিণত করে। প্রথমবার লাল হলে দল খোঁজে; দশমবার, যখন দেখা গেছে প্রতিবার “আবার চালালে সবুজ”, তখন কেউ আর খোঁজে না — শুধু “re-run” চাপে। তারপর একদিন একটি আসল bug সেই একই test বা পাশের test লাল করে, আর অভ্যাসবশে re-run, কপালগুণে সবুজ, merge. রাখালের “বাঘ এসেছে” গল্পের মতো।
খরচও প্রত্যক্ষ। CI-তে একটি flaky test পুরো pipeline আবার চালাতে বাধ্য করে — বড় দলে প্রতিদিন শত শত মিনিট। Google-এর testing blog-এ ২০১৬ সালের একটি লেখায় জানানো হয়েছিল যে তাদের বিশাল test সংগ্রহের একটি উল্লেখযোগ্য অংশে (লেখায় প্রায় ১৬% বলা হয়েছিল) কোনো না কোনো মাত্রার flakiness দেখা গেছে, আর এটি সামলাতে আলাদা ব্যবস্থা রাখতে হয়েছিল — সংখ্যাগুলো একটি প্রতিষ্ঠানের, কিন্তু সমস্যাটি সর্বজনীন।
কারণগুলো সাধারণত পরিচিত: লুকানো ঘড়ি (time zone, মাসের শেষ, daylight saving), seed ছাড়া random, ভাগ করা state আর test-এর ক্রম, অ্যাসিঙ্ক্রোনাস কোডে “একটু অপেক্ষা” (sleep(1)) যা ধীর CI যন্ত্রে যথেষ্ট নয়, আসল network বা তৃতীয় পক্ষের service, আর set বা dictionary-র ক্রমের ওপর নির্ভরতা। Unit স্তরে প্রায় সবগুলোরই সমাধান এই পর্বের কৌশল: inject করুন, প্রতি test-এ নতুন fixture দিন, আর বাইরের ব্যবস্থা test double দিয়ে বদলান। Integration আর end-to-end স্তরে flaky test সামলানো কঠিন; সেটি পর্ব ৫১-এ।
দলের নীতি হিসেবে অনেক প্রতিষ্ঠান flaky test-কে “quarantine” করে: suite থেকে সাময়িকভাবে আলাদা করে, একটি ticket খুলে, নির্দিষ্ট সময়ের মধ্যে সারানো বা মুছে ফেলা। কৌশলটি তখনই কাজ করে যখন quarantine সত্যিই সাময়িক থাকে; নইলে সেটি test-এর কবরস্থান হয়ে যায় — test debt (পর্ব ৪৩)।
মূল উপলব্ধি: Test-এর কাজ বিশ্বাসযোগ্য সংকেত দেওয়া। যে test-কে বিশ্বাস করা যায় না, সেটি শুধু খরচ নয় — এটি অন্য সব test-এর বিশ্বাসযোগ্যতাও খেয়ে ফেলে।
প্র ০৪Test-এর ভেতরে logic কেন নিষেধ, অথচ production কোডে logic-ই আসল কাজ — এই দ্বৈত মানদণ্ড কি যুক্তিসংগত?
যুক্তিসংগত, কারণ test আর production কোডের ভূমিকা আলাদা। Production কোডের কাজ হলো সাধারণ নিয়ম প্রকাশ করা — “যেকোনো ওজনের জন্য fee কত।” Test-এর কাজ হলো সেই নিয়মের নির্দিষ্ট উদাহরণ দিয়ে তাকে যাচাই করা — “২.৫ কেজি বাইরে, ১৭০ টাকা।” উদাহরণ আর নিয়ম দুটি স্বাধীন উৎস থেকে এলে তবেই তাদের মিল কিছু প্রমাণ করে। Test-এ নিয়মটি আবার লিখলে (expected = 120 + 25 * (ceil(w) - 1)) দুটি উৎস এক হয়ে যায়; প্রোগ্রামার নিয়ম ভুল বুঝে থাকলে দুই জায়গাতেই একই ভুল, আর test সবুজ।
দ্বিতীয় কারণ: test-কে কেউ test করে না। Test-এর একটি if-এর ভুল শাখা নীরবে কিছু যাচাই বাদ দিতে পারে। একটি loop শূন্যবার চললে (খালি তালিকা) test কোনো assertion ছাড়াই সবুজ। তাই test যত সোজা — ওপর থেকে নিচে, কোনো শাখা ছাড়া — তত কম জায়গা ভুল লুকানোর। Cyclomatic complexity-র ভাষায় (পর্ব ৫০), একটি test function-এর complexity আদর্শভাবে ১.
তৃতীয় কারণ পাঠক। Test একটি executable documentation: নতুন সদস্য কোডের আচরণ জানতে প্রথমে test পড়েন। “weight 2.5, outside, 170” পড়ে নিয়মটি সঙ্গে সঙ্গে বোঝা যায়; একটি সূত্র পড়ে আবার মনে মনে হিসাব করতে হয়।
এর মানে এই নয় যে test কোডে কোনো কোডই থাকবে না। Test data builder, ছোট helper (যেমন an_order(zone="outside")), বা parameterized test-এর টেবিল — এগুলো logic নয়, বিন্যাস। আর একটি ব্যতিক্রম আছে: property-based testing (Python-এ Hypothesis library, Haskell-এ QuickCheck থেকে আসা ধারণা), যেখানে হাজার এলোমেলো input-এ একটি ধর্ম যাচাই হয় — যেমন “fee কখনো কমে না যখন ওজন বাড়ে।” এখানে প্রত্যাশিত মান নয়, একটি সাধারণ সম্পর্ক যাচাই হয়, যা সূত্রের পুনরাবৃত্তি নয়।
মূল উপলব্ধি: Production কোড বলে “কীভাবে”; test বলে “কী হওয়া উচিত”, হাতে করা উদাহরণে। দুটি এক উৎস থেকে এলে test শুধু প্রতিধ্বনি।
প্র ০৫সব test সবুজ, coverage ৯৫% — তবুও bug থাকে কীভাবে? একটি test suite-এর মান আসলে কী দিয়ে মাপবেন?
Coverage মাপে কোন লাইন test চলার সময় চলেছে, কিন্তু চলার পর কিছু যাচাই হয়েছে কি না, তা নয়। একটি test যদি fee(2.5, "outside") ডেকে ফল ফেলে দেয়, বা শুধু assertIsNotNone করে, তাহলে fee function-এর প্রতিটি লাইন “covered”, অথচ ১৭০-এর জায়গায় ১৪৫ এলেও test সবুজ। AI-এর লেখা test-এ এই ধরন প্রায়ই দেখা যায়, কারণ “coverage বাড়াও” নির্দেশ এভাবেই সবচেয়ে সহজে পূরণ হয়।
দ্বিতীয় ফাঁক হলো অনুপস্থিত test: যে আচরণের কথা কেউ ভাবেনি, তার জন্য কোনো লাইনও নেই, তাই coverage-এ ফাঁকও দেখা যায় না। যেমন requirement-এ ছিল “২০ কেজির বেশি হলে পার্সেল নেওয়া হবে না”, কিন্তু কোডে সেই শর্তই নেই — সব লাইন covered, সব test সবুজ, আর আচরণটি অনুপস্থিত। Test-এর কাজ requirement থেকে আসে, কোড থেকে নয়; পর্ব ৪৭-এর partition আর boundary কৌশল এই ফাঁক কমায়।
তাহলে মান মাপবেন কী দিয়ে? সবচেয়ে সরাসরি প্রশ্ন: “কোডে একটি ভুল ঢোকালে কোনো test কি লাল হয়?” এটাই mutation testing-এর ধারণা — একটি tool কোডে ছোট ছোট বদল (mutant) আনে, যেমন >-কে >=, ceil-কে floor, আর দেখে test suite কয়টি mutant “মারতে” পারল। বেঁচে থাকা mutant মানে test-এর একটি অন্ধ জায়গা। পর্ব ৫০-এ এর হিসাব দেখবেন। হাতে করা সংস্করণটি সহজ: review-এর সময় একটি লাইন বদলে test চালান।
আরও কিছু ব্যবহারিক মাপ: test-গুলো কতবার আসল bug ধরেছে (incident-এর পর “কোন test ধরতে পারত?”), flaky test-এর হার, suite চলতে কত সময় লাগে, আর নতুন সদস্য test পড়ে আচরণ বুঝতে পারেন কি না। কোনো একক সংখ্যা যথেষ্ট নয় — Goodhart-এর সূত্রের কথা মনে রাখুন: কোনো মাপকে লক্ষ্য বানালে সেটি আর ভালো মাপ থাকে না।
মূল উপলব্ধি: Test suite-এর মান তার ভুল ধরার ক্ষমতায়, তার আকারে বা সবুজ রঙে নয়। প্রতিটি test সম্পর্কে জিজ্ঞেস করুন: “কোন ভুলে এটি লাল হবে?” — উত্তর না থাকলে test-টি অলংকার।
অনুশীলন
প্রথমে নিজে চেষ্টা করুন, তারপর উত্তর মিলিয়ে নিন।
-
এই function-এর জন্য test লিখুন: একটি কাল্পনিক লাইব্রেরির বই ফেরতে দেরির জরিমানা — দেরি ০ দিন হলে ০ টাকা; ১ থেকে ৭ দিন হলে প্রতিদিন ৫ টাকা; ৭ দিনের বেশি হলে প্রথম ৭ দিনের ৩৫ টাকা আর তার পরের প্রতিদিন ১০ টাকা; ঋণাত্মক দিন দিলে
ValueError.কোন কোন input বেছে নেবেন, প্রত্যাশিত মান হাতে হিসাব করুন, আরdef late_fine(days_late): if days_late < 0: raise ValueError("days_late cannot be negative") if days_late <= 7: return 5 * days_late return 35 + 10 * (days_late - 7)unittest-এ AAA,subTestওassertRaisesRegexদিয়ে test লিখুন।Input বাছাই: প্রতিটি নিয়মের ভেতর থেকে একটি আর প্রতিটি সীমার দুপাশ — ০ (জরিমানা নেই), ১ (প্রথম দিন), ৭ (প্রথম ধাপের শেষ), ৮ (দ্বিতীয় ধাপের প্রথম দিন), ১০ (দ্বিতীয় ধাপের ভেতরে), আর −১ (ভুল input). হাতে হিসাব: ০ → ০; ১ → ৫ × ১ = ৫; ৭ → ৫ × ৭ = ৩৫; ৮ → ৩৫ + ১০ × ১ = ৪৫; ১০ → ৩৫ + ১০ × ৩ = ৬৫.
class LateFineTest(unittest.TestCase): def test_late_fine_boundaries(self): cases = [(0, 0), (1, 5), (7, 35), (8, 45), (10, 65)] for days, expected in cases: with self.subTest(days=days): self.assertEqual(late_fine(days), expected) def test_late_fine_negative_days_raises(self): with self.assertRaisesRegex(ValueError, "negative"): late_fine(-1)এই দুটি test চালালে দুটিই
ok(আমরা চালিয়ে মিলিয়েছি)। প্রত্যাশিত মানগুলো সংখ্যা হিসেবে লেখা, সূত্র নয়। প্রতিটি সারির একটি কাজ আছে: ১-এর সারি প্রথম ধাপের দর (৫) ধরে, ৮-এর সারি দ্বিতীয় ধাপের দর (১০) আর ৩৫ টাকার ভিত্তি ধরে, ১০-এর সারি নিশ্চিত করে যে দর প্রতিদিন যোগ হয়। একটি মজার পর্যবেক্ষণ: কেউ<= 7-কে< 7করলে ৭ দিন দ্বিতীয় সূত্রে যায় — ৩৫ + ১০ × ০ = ৩৫, একই ফল। এমন বদল আচরণই বদলায় না, তাই কোনো test এটি ধরতে পারে না, আর ধরার দরকারও নেই; mutation testing-এ একে “equivalent mutant” বলে (পর্ব ৫০)। সীমার মান বাছাইয়ের পদ্ধতিগত উপায় পর্ব ৪৭-এ। -
ফল অনুমান করুন — কোন test ব্যর্থ হবে?
unittestদিয়ে নিচের class চালানো হলো (TextTestRunner, verbosity 2). কোন ক্রমে test চলবে, আর কোনগুলো FAIL হবে?class Counter: def __init__(self): self.n = 0 class CounterTest(unittest.TestCase): hits = [] def setUp(self): self.c = Counter() def test_b_second(self): self.hits.append("b") self.assertEqual(len(self.hits), 1) def test_a_first(self): self.hits.append("a") self.assertEqual(len(self.hits), 1) def test_c_float_equal(self): self.assertEqual(0.1 * 3, 0.3) def test_d_float_close(self): self.assertAlmostEqual(0.1 * 3, 0.3) def test_e_counter_fresh(self): self.c.n += 1 self.assertEqual(self.c.n, 1)আমরা চালিয়ে যা পেয়েছি (traceback বাদ দিয়ে):
test_a_first (__main__.CounterTest.test_a_first) ... ok test_b_second (__main__.CounterTest.test_b_second) ... FAIL test_c_float_equal (__main__.CounterTest.test_c_float_equal) ... FAIL test_d_float_close (__main__.CounterTest.test_d_float_close) ... ok test_e_counter_fresh (__main__.CounterTest.test_e_counter_fresh) ... ok … AssertionError: 2 != 1 … AssertionError: 0.30000000000000004 != 0.3 … FAILED (failures=2)- ক্রম: নামের বর্ণানুক্রমে — a, b, c, d, e; কোডে লেখার ক্রম (b আগে) গুরুত্বহীন।
- test_b ব্যর্থ:
hitsএকটি class attribute — সব test একই list ভাগ করে।setUpশুধুself.cনতুন করে,hitsনয়। test_a আগে "a" রেখে গেছে, তাই test_b-এ দৈর্ঘ্য ২. শুধু test_b একা চালালে পাশ করত — ক্রমনির্ভর test. - test_c ব্যর্থ:
0.1 * 3হলো0.30000000000000004; হুবহু সমতা floating point-এ চলে না। - test_d পাশ:
assertAlmostEqualdefault-এ পার্থক্য ৭ দশমিক ঘরে গোল করে শূন্য কি না দেখে; পার্থক্য প্রায় ৫.৬ × ১০−১৭. - test_e পাশ:
self.cপ্রতিটি test-এর আগে নতুনCounter, তাইnসবসময় ০ থেকে শুরু। একই class-এ দুই রকম fixture পাশাপাশি — একটি নিরাপদ, একটি ফাঁদ।
-
এই test-এর সমালোচনা করুন: নিচের test-এ কতগুলো test smell খুঁজে পান? প্রতিটির নাম দিন, কেন ক্ষতিকর বলুন, আর একটি ভালো রূপ প্রস্তাব করুন।
def test1(self): data = json.load(open("fixtures/data.json")) time.sleep(2) # wait for the cache for c in data: if c["zone"] == "city": exp = 60 + 15 * (math.ceil(c["kg"]) - 1) else: exp = 120 + 25 * (math.ceil(c["kg"]) - 1) self.assertEqual(self.calc.fee(c["kg"], c["zone"]), exp) self.assertTrue(self.calc._cache) self.assertEqual(len(self.calc._cache), 7) self.assertTrue(datetime.now().hour < 18)- অর্থহীন নাম (
test1): লাল হলে কিছুই বলে না। →test_fee_…_…ধরনের আচরণ-বর্ণনা। - Mystery guest (
fixtures/data.json): কোন ওজন আর zone পরীক্ষা হচ্ছে, test পড়ে জানা যায় না; কেউ ফাইল বদলালে অজানা কারণে ভাঙবে। ফাইলটি বন্ধও করা হয়নি (openছাড়াwith), যা Python-এResourceWarningদিতে পারে। → ডেটা test-এর ভেতরে একটি টেবিলে। - Slow test (
sleep(2)): প্রতিটি run-এ ২ সেকেন্ড নষ্ট, আর ধীর CI যন্ত্রে ২ সেকেন্ড যথেষ্ট না-ও হতে পারে (flaky). → Cache-এর আচরণ আলাদা test-এ, সময় inject করে। - Test-এ logic (
ifআর সূত্র): প্রত্যাশা কোডের সূত্রের আয়না। → হাতে হিসাব করা সংখ্যা। - Assertion roulette: loop-এর ভেতরে বার্তা ছাড়া assertion, তার ওপর তিনটি ভিন্ন আচরণ (fee, cache, সময়) এক test-এ; প্রথম ব্যর্থতায় বাকিগুলো চলে না। → এক আচরণে এক test, সারির জন্য
subTest. - Over-specified / fragile (
_cacheআর তার দৈর্ঘ্য ৭): private বাস্তবায়নের খুঁটিনাটি; cache-এর কৌশল বদলালেই ভাঙবে। → প্রকাশ্য আচরণ যাচাই, যেমন একই input-এ দুবার ডাকলে একই ফল। - লুকানো ঘড়ি / time bomb (
datetime.now().hour < 18): সন্ধ্যা ৬টার পর CI চললে লাল, কোড না বদলেও। এই লাইনটি fee-র আচরণের সাথে সম্পর্কহীন — মুছে ফেলুন, বা সময়নির্ভর আচরণ থাকলে সময় inject করে আলাদা test.
ভালো রূপের একটি অংশ:
def test_fee_rounds_part_kg_up(self): cases = [(0.5, "city", 60), (1.2, "city", 75), (3, "outside", 170)] for weight, zone, expected in cases: with self.subTest(weight=weight, zone=zone): self.assertEqual(self.calc.fee(weight, zone), expected) def test_fee_same_input_gives_same_result_twice(self): self.assertEqual(self.calc.fee(2.5, "outside"), self.calc.fee(2.5, "outside")) - অর্থহীন নাম (
-
হিসাব করে বলুন — পাশ না ফেল? (ক)
assertAlmostEqual(33.333333, 33.33, places=2)(খ) Jest-এexpect(33.333333).toBeCloseTo(33.33, 3)(গ) Jest-এexpect(33.333333).toBeCloseTo(33.33)(default digits) (ঘ)assertAlmostEqual(1.005, 1.0, places=2). নিয়ম মনে করুন:assertAlmostEqualপাশ করে যদিround(a − b, places) == 0;toBeCloseToপাশ করে যদি|b − a| < 10^(−digits) / 2.পার্থক্য প্রথম তিনটিতে |৩৩.৩৩৩৩৩৩ − ৩৩.৩৩| ≈ ০.০০৩৩৩৩.
- (ক) পাশ: ০.০০৩৩৩৩-কে ২ ঘরে গোল করলে ০.০০, অর্থাৎ শূন্য।
- (খ) ফেল: সীমা ১০−৩ / ২ = ০.০০০৫; ০.০০৩৩৩৩ এর চেয়ে বড়।
- (গ) পাশ: default digits = ২, সীমা ০.০০৫; ০.০০৩৩৩৩ < ০.০০৫.
- (ঘ) পাশ — ফাঁদ: মনে হয় ০.০০৫ গোল হয়ে ০.০১ হবে, কিন্তু floating point-এ
1.005 - 1.0আসলে0.004999999999999893, যা ২ ঘরে গোল করলে0.0. আমরা Python-এ চালিয়ে মিলিয়েছি:round(1.005 - 1.0, 2) == 0সত্য। শিক্ষা: সীমার একেবারে কিনারায় tolerance-এর ওপর ভরসা করবেন না; টাকার হিসাব পূর্ণসংখ্যা পয়সায় বাDecimal-এ করুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমদুটি test-ওয়ালা একটি unittest class-এ প্রথম test পর্যন্ত আর একেবারে শেষের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
setUpClass— পুরো class-এর জন্য একবারsetUp— প্রথম test-এর ঠিক আগে- প্রথম test method
tearDown— প্রথম test-এর পরেtearDownClass— সব test শেষ হলে একবার
Arrange-Act-Assert কাঠামোয় “Act” অংশে সাধারণত কী থাকে?
- পরীক্ষাধীন object আর input তৈরি
- যে আচরণ যাচাই হচ্ছে, সেটি একবার ডাকা
- ফল প্রত্যাশার সাথে মেলানো
- প্রত্যাশিত মান বের করতে কোডের সূত্র আবার হিসাব করা
Python unittest-এ self.assertEqual(0.1 + 0.2, 0.3) ব্যর্থ হয়। দশমিক ফল মেলাতে কোনটি ঠিক?
self.assertTrue(0.1 + 0.2 == 0.3)self.assertEqual(str(0.1 + 0.2), "0.3")self.assertAlmostEqual(0.1 + 0.2, 0.3)self.assertIs(0.1 + 0.2, 0.3)
একটি unittest class-এ cart = Cart() class attribute হিসেবে আছে (কোনো setUp নেই)। test_add_item cart-এ ৮০ টাকার চাল যোগ করে total ৮০ কি না দেখে; test_new_cart_is_empty দেখে total ০ কি না। test_add_item আগে চললে কী হবে?
- দুটিই পাশ, কারণ প্রতিটি test নিজের cart পায়
- দুটিই ব্যর্থ
- শুধু test_add_item ব্যর্থ
- test_add_item পাশ, test_new_cart_is_empty ব্যর্থ
ভালো unit test-এর গুণের সংক্ষেপ FIRST-এ “R” কী বোঝায়?
- Readable — পড়তে সহজ
- Repeatable — যেকোনো সময়ে, যেকোনো জায়গায় একই ফল
- Refactorable — বদলানো সহজ
- Regression — পুরোনো bug আবার ধরা
OrderService-এর একটি test আসল PriceCalculator আর আসল Cart ব্যবহার করে, শুধু payment gateway-র জায়গায় একটি fake বসায়। এই test-কে কী বলা হয়?
- Sociable unit test
- Solitary unit test
- End-to-end test
- Flaky test
একটি test load("fixtures/customers_v3.csv") পড়ে, তারপর assertEqual(report.total, 48250) করে; ফাইলে কী আছে, test পড়ে বোঝা যায় না। এটি কোন test smell?
- Assertion roulette
- Slow test
- Mystery guest
- Test interdependence
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Unit test: একটি ছোট আচরণের দ্রুত, বিচ্ছিন্ন, স্বয়ংক্রিয় যাচাই। “Unit” কতটুকু — solitary (সহযোগী নকল, London) না sociable (আসল সহযোগী, Detroit) — একটি সিদ্ধান্ত; বাইরের ব্যবস্থা দুই ধারাতেই বদলানো হয়।
- Arrange → Act → Assert (Given–When–Then): সাজানো, একবার ডাকা, মেলানো। প্রত্যাশিত মান হাতে হিসাব করা সংখ্যা; test-এ if, loop বা সূত্র নয়।
- FIRST আর এক test-এ এক আচরণ; নামে unit, পরিস্থিতি আর প্রত্যাশা। ঠিক assertion বাছুন: exception-এ ধরন ও বার্তা, দশমিকে tolerance (
assertAlmostEqual,pytest.approx,toBeCloseTo), collection-এ ক্রম জরুরি কি না আগে ঠিক করা। - Fixture:
setUp/tearDownপ্রতিটি test-এ,setUpClassএকবার শুধু read-only জিনিসের জন্য; pytest fixture আরtmp_path; test data builder; parameterized test (subTest,parametrize,test.each). - Deterministic আর smell-মুক্ত: সময় আর এলোমেলোতা inject করুন, প্রতি test-এ নতুন state দিন, এলোমেলো ক্রমে চালিয়ে দেখুন। চিনে রাখুন assertion roulette, mystery guest, fragile, slow, interdependent আর over-specified test — আর AI-এর লেখা test-এ একটি ভুল ঢুকিয়ে দেখুন সেটি লাল হয় কি না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৭ · Test case নকশা — equivalence partitioning, boundary value আর decision tableপরবর্তী পর্বকোন input দিয়ে test লিখবেন, তার পদ্ধতি।
- পর্ব ৪৫ · Testing-এর মৌলিক — verification-validation, test-এর স্তর আর black-box বনাম white-boxআগের পর্বTest-এর স্তর আর ধরন — unit test কোথায় বসে।
- পর্ব ৪৯ · Test double — dummy, stub, fake, spy, mock আর কখন mock নয়একই মডিউলSolitary test-এর হাতিয়ার আর তার সীমা।
- পর্ব ৩৭ · Dependency injection আর composition — inversion of control আর inheritance-এর বদলে compositionভিত্তিঘড়ি, random আর বাইরের ব্যবস্থা inject করে test-যোগ্য কোড।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Python প্রোগ্রামিং · টেস্টিং — pytestপাশের ট্র্যাকpytest-এর fixture, parametrize আর বাস্তব project-এ test চালানো।
- JavaScript প্রোগ্রামিং · টেস্টিং — Vitest আর Playwrightপাশের ট্র্যাকJavaScript-এ unit test থেকে browser test পর্যন্ত।