সরাসরি পর্বে যান
পর্ব ৪৮ · ৭০-এর মধ্যে · মডিউল ৮

TDD আর BDD — red-green-refactor আর Given-When-Then

TDD & BDD — Red-Green-Refactor and Given-When-Then

Taught in Bangla, this lesson covers test-driven development: the red-green-refactor cycle, test lists and baby steps, fake it, triangulation and obvious implementation, a complete bus-fare TDD kata step by step, outside-in versus inside-out TDD, ATDD and BDD as the outer loop, Gherkin Given-When-Then scenarios wired to step definitions, and when TDD does not fit.

এই পর্বে যা শিখবেন

  • Red-green-refactor চক্রের প্রতিটি ধাপে কী করবেন আর কী করবেন না, Beck-এর নিয়ম আর “তিন সূত্র” দিয়ে ব্যাখ্যা করা
  • Test list, baby steps আর fake it, triangulation, obvious implementation — কোন অবস্থায় কোন কৌশল, তা বাছাই করা
  • একটি পূর্ণ TDD kata চক্রে চক্রে অনুসরণ করা: প্রতিটি লাল বার্তা পড়া, ন্যূনতম কোড আর আলাদা refactor commit
  • Outside-in (London) আর inside-out (classic) TDD-র পার্থক্য, আর ATDD-BDD-র বাইরের চক্র কীভাবে ভেতরের TDD-কে চালায় তা বোঝা
  • Gherkin scenario থেকে step definition হয়ে কোড পর্যন্ত পথ ধরা, আর TDD কখন খাটে, কখন খাটে না, কোন ভুল সবচেয়ে সাধারণ তা বিচার করা

১ · TDD কী — কোডের আগে test কেন

পর্ব ৪৫-এ দেখেছেন test কাকে বলে আর কোন স্তরে কী যাচাই হয়; পর্ব ৪৬-এ শিখেছেন একটি unit test-এর গঠন — Arrange-Act-Assert, assertion আর fixture. আর পর্ব ৪৭-এ দেখেছেন কোন কোন input বেছে নিলে কম test-এ বেশি ভুল ধরা যায়। সেখানে ধরে নেওয়া হয়েছিল, কোড আগে আছে, test পরে আসে। Test-driven development (TDD) এই ক্রম উল্টে দেয়: আগে এমন একটি test লিখুন যা এখন ফেল করে, তারপর ঠিক ততটুকু কোড লিখুন যাতে সেটি পাস করে, তারপর কোড গোছান। তারপর পরের test.

পাড়ার দর্জির দোকানের কথা ভাবুন। ভালো দর্জি কাপড়ে কাঁচি চালানোর আগে খাতায় মাপ লিখে নেন — বুক ৩৮, হাতা ২৪, ঝুল ৪২। কাটা আর সেলাইয়ের পরে ফিতা ধরে প্রতিটি মাপ মেলান। খাতার মাপ এখানে test: কাজ শুরুর আগেই লেখা, আর কাজ “শেষ” হলো কি না তার মাপকাঠি। TDD-র বিশেষত্ব হলো দর্জি সব মাপ একসাথে কাটেন না — একটি মাপ লেখেন, সেই অংশটুকু সেলাই করেন, গায়ে মিলিয়ে দেখেন, তারপর পরের মাপ। আর প্রতিবার মাপ মেলার পরে সেলাইয়ের ভাঁজ একটু পরিপাটি করে নেন — সেটি refactor.

পদ্ধতিটির নাম আর রূপ দাঁড় করান Kent Beck. ১৯৯০-এর দশকের শেষে Extreme Programming (XP)-এর (পর্ব ৫) একটি মূল অনুশীলন ছিল “test-first programming”. পরে Beck তাঁর Test-Driven Development: By Example বইয়ে (Addison-Wesley; ২০০২ সালের শেষ দিকে প্রকাশিত, বইয়ে কপিরাইট সাল ২০০৩ — তাই দুটি সালই উদ্ধৃত হতে দেখবেন) পুরো পদ্ধতিটি উদাহরণসহ লেখেন। বইয়ের প্রথম ভাগে বহু-মুদ্রার একটি Money উদাহরণ ছোট ছোট চক্রে গড়ে ওঠে, আর দ্বিতীয় ভাগে Beck Python-এ একটি xUnit ধরনের test framework নিজেই TDD দিয়ে বানান। Beck নিজে বলেছেন তিনি পদ্ধতিটি “পুনরাবিষ্কার” করেছেন — পুরোনো programming বইয়ে প্রত্যাশিত আউটপুট আগে লিখে রাখার কথা পড়েছিলেন।

Beck-এর দুটি নিয়ম আর “তিন সূত্র”

বইয়ের ভূমিকায় Beck পুরো পদ্ধতিকে দুটি নিয়মে বাঁধেন:

  1. নতুন কোড লিখুন কেবল তখনই, যখন একটি স্বয়ংক্রিয় test আগে ফেল করেছে।
  2. Duplication দূর করুন। — test আর কোডের মধ্যে, কোডের ভেতরে, যেখানেই হোক।

পরে Robert C. Martin (যাঁকে অনেকে “Uncle Bob” নামে চেনেন) একই কথাকে আরও কড়া করে তিনটি “সূত্রে” লেখেন, যা তাঁর ২০০০-এর দশকের লেখা আর Clean Code (২০০৮) বইয়ের test-বিষয়ক অধ্যায়ে পাওয়া যায় — ভাষা জায়গাভেদে একটু আলাদা। এগুলোকে বলা হয় three laws of TDD:

  1. একটি ব্যর্থ unit test না লিখে কোনো production কোড লেখা যাবে না।
  2. ফেল করার জন্য যতটুকু দরকার, তার বেশি test লেখা যাবে না — আর compile না হওয়াও ফেল।
  3. বর্তমান ব্যর্থ test-টি পাস করানোর জন্য যতটুকু দরকার, তার বেশি production কোড লেখা যাবে না।

তিন সূত্র মানলে চক্র হয় খুব ছোট — কয়েক সেকেন্ড থেকে কয়েক মিনিট। Beck নিজে ধাপের মাপ অবস্থা বুঝে ছোট-বড় করার কথা বলেন (অংশ ২), তবে অভ্যাস গড়ার সময়ে কড়া রূপটি কাজের।

ধারাক্রমTest কখন লাল দেখা যায়?নকশার ওপর প্রভাব
Test-afterপুরো কোড, তারপর testপ্রায় কখনো না — test লেখা হয় চলমান কোডের জন্যকম; কঠিন-to-test কোড পরে টের পাওয়া যায়
TDDএকটি test → ন্যূনতম কোড → refactor, বারবারপ্রতিটি চক্রের শুরুতে, একটিমাত্রবেশি; প্রতিটি নতুন আচরণ ব্যবহারকারীর চোখে আগে ভাবা হয়
মূল ধারণা — TDD testing-এর চেয়ে বেশি নকশার পদ্ধতি

আগে test লেখা মানে আগে ভাবা: “এই function-কে কেউ কীভাবে ডাকবেন, কী দেবেন, কী ফেরত চাইবেন?” এর উত্তরেই নাম, parameter আর দায়িত্ব ঠিক হয়। থেকে যাওয়া test-গুলো regression test-এর নিরাপত্তার জাল আর কোডের ব্যবহারের জীবন্ত উদাহরণ — দুটিই উপজাত ফল।

২ · Red → green → refactor — চক্রটি খুঁটিয়ে

TDD-র হৃৎস্পন্দন তিন রঙের একটি চক্র, যার নাম red-green-refactor. রং দুটি এসেছে test runner-এর পুরোনো progress bar থেকে — ফেল করলে লাল, সব পাস করলে সবুজ।

Red-green-refactor চক্র

প্রতিটি চক্র test list-এর একটি item নেয়। Refactor শেষে আবার red — পরের item দিয়ে।

Red-green-refactor চক্র RED লাল GREEN সবুজ REFACTOR গোছানো ১. একটি ছোট ব্যর্থ test লিখুন; ঠিক কারণে লাল? ২. পাস করানোর ন্যূনতম কোড ৩. test সবুজ রেখে duplication, নাম Test list থেকে পরের item প্রতি চক্র: কয়েক মিনিট

তিনটি ধাপ, প্রতিটির নিজের নিয়ম

  • Red — একটি ছোট ব্যর্থ test: test list থেকে একটি item নিয়ে test লিখুন, চালান, আর লাল দেখুন। লাল দেখা প্রমাণ করে test-টি আদৌ ফেল করতে পারে। আরও দেখুন কেন লাল — import-এর গোলমালে নয়, প্রত্যাশিত মান না মেলায়।
  • Green — পাস করানোর ন্যূনতম কোড: এখন একমাত্র লক্ষ্য সবুজ। Beck-এর ভাষায়, এই মুহূর্তে যত “পাপ” দরকার করে ফেলুন — constant ফেরত দেওয়া, কোড কপি করা। সুন্দর কোডের চিন্তা পরের ধাপের। নতুন কোনো আচরণ এই ধাপে যোগ হয় না, যা test চায়নি।
  • Refactor — test সবুজ রেখে গোছানো: green ধাপের পাপগুলো এখন শোধ করুন: test আর কোডের মধ্যে duplication (যেমন test-এর ১৩ আর কোডের ১৩), খারাপ নাম, লম্বা function. প্রতিটি ছোট বদলের পর test চালান। এটি পর্ব ৪২-এর refactoring — আচরণ একই, “দুই টুপি”-র refactoring-টুপি।

Test list আর baby steps

চক্র শুরুর আগে Beck একটি কাগজে লেখেন: এই কাজে কী কী test লাগবে বলে এখন মনে হচ্ছে। একে বলে test list. তালিকাটি সম্পূর্ণ হওয়ার দরকার নেই; কাজ চলতে চলতে নতুন ভাবনা (“শূন্য দূরত্ব হলে কী হবে?”) এলে তালিকায় যোগ করুন, কিন্তু সেই মুহূর্তে কোডে নয় — চলতি চক্র আগে শেষ। প্রতিটি সবুজের পর একটি item কেটে দিন। Beck ২০২৩ সালের “Canon TDD” লেখায় এই তালিকাকেই প্রথম ধাপ বলেছেন বলে আমাদের জানা।

লোডশেডিং-এর রাতে অন্ধকার সিঁড়ি দিয়ে নামার কথা ভাবুন। আপনি এক এক ধাপ নামেন, প্রতিবার পা দিয়ে নিচের ধাপ ছুঁয়ে নিশ্চিত হন। আলো ফিরলে দুই ধাপ একসাথে নামেন — কারণ এখন দেখতে পাচ্ছেন। আবার অন্ধকার হলে আবার এক ধাপ। TDD-র baby steps ঠিক এমন: পরিচিত, সহজ জায়গায় বড় ধাপ চলে; অচেনা বা জটিল জায়গায়, বিশেষ করে যখন অপ্রত্যাশিত লাল আসছে, ধাপ ছোট করুন। ভুল ধাপের খরচ তখন মাত্র কয়েক মিনিট — শেষ সবুজ অবস্থায় ফিরে গেলেই হয়।

নিচের stepper-এ একটি পুরো চক্র ধীরে ধীরে দেখুন। প্রেক্ষাপট একটি কাল্পনিক শহরের বাস ভাড়ার হিসাব, যার পূর্ণ kata অংশ ৪-এ। প্রথম চক্রে fare(1)-এর জন্য ১০ টাকা ফেরত দিতে fare শুধু return 10 লিখেছে। এবার পরের item.

  1. Test list4 km → 13পরের item
  2. Red10 != 13ঠিক কারণে ফেল
  3. GreenOKন্যূনতম কোড
  4. RefactorOKduplication বাদ
  5. Commitgit logপরের চক্র
Test list + fare.pyচক্রের শুরু: সব সবুজ
✔ 1 km   → 10 টাকা (ন্যূনতম)
▶ 4 km   → 13 টাকা
☐ 3 km   → 10 টাকা (সীমা)
☐ 3.2 km → 13 টাকা (শুরু করা কিমি = পুরো কিমি)
☐ ছাত্র, 10 km → 16 টাকা
☐ 0 km   → ValueError

# fare.py — আগের চক্রের পরে
def fare(km):
    return 10
test_fare.py + টার্মিনালএকটি নতুন test, লাল
def test_four_km_adds_one_extra_km(self):
    self.assertEqual(fare(4), 13)

$ python -m unittest
F.
FAIL: test_four_km_adds_one_extra_km (…)
…
AssertionError: 10 != 13
Ran 2 tests in 0.001s
FAILED (failures=1)
fare.pyTriangulation: দুই উদাহরণ, তাই নিয়ম
def fare(km):
    if km <= 3:
        return 10
    return 10 + (km - 3) * 3

$ python -m unittest
..
Ran 2 tests in 0.000s
OK
$ git commit -am "feat: add 3 taka per km after 3 km (triangulated)"
fare.pyআচরণ একই, কাঠামো পরিষ্কার
BASE_FARE, BASE_KM, PER_KM = 10, 3, 3

def fare(km):
    return BASE_FARE + max(0, km - BASE_KM) * PER_KM

$ python -m unittest
..
OK
টার্মিনালপ্রতিটি সবুজ = একটি ছোট commit
$ git commit -am "refactor: name fare constants, drop the if"
$ git log --oneline -3
e254efc refactor: name fare constants, drop the if
36c8721 feat: add 3 taka per km after 3 km (triangulated)
8050ffd feat: charge minimum fare of 10 taka (fake it)

✔ 4 km → 13 টাকা   (তালিকা থেকে কাটা)
চক্রের হিসাব
Test (সবুজ/মোট)
কোন টুপি
  1. Test list থেকে পরের item. সব test সবুজ। পরের সহজতম item: ৪ কিমি গেলে ১৩ টাকা (৩ কিমি পর্যন্ত ১০, তারপর প্রতি কিমি ৩)। কোড এখনো আগের চক্রের fake return 10.
  2. Red: একটি test, একটি কারণ। নতুন test চালাতেই পুরোনোটি পাস (.), নতুনটি ফেল (F). বার্তা AssertionError: 10 != 13 — ঠিক যে কারণে ফেল করার কথা: কোড এখনো সবসময় ১০ দেয়।
  3. Green: ন্যূনতম কোড, তারপর commit. এখন দুটি উদাহরণ (১ কিমি → ১০, ৪ কিমি → ১৩), তাই constant আর চলে না; দুটি মিলিয়ে নিয়ম বের হলো (triangulation, অংশ ৩)। দুটি test সবুজ — একটি feat: commit.
  4. Refactor: duplication আর magic number. 10 আর 3 দুবার করে, অর্থ নামহীন। নাম দেওয়া constant বসল, আর max(0, …) দিয়ে if-টিও বাদ গেল। আচরণ একই, তাই test-এ হাত পড়েনি — আবার চালিয়ে সবুজ।
  5. আলাদা refactor commit, item কাটা। কাঠামোর বদল আলাদা refactor: commit-এ (পর্ব ১৮)। তালিকা থেকে ৪ কিমি কাটা হলো; পরের চক্র শুরু হবে আবার red দিয়ে।

Refactor ধাপ বাদ দেওয়া সবচেয়ে সাধারণ ভুল। Red-green করে পরের test-এ চলে গেলে green ধাপের “পাপ” জমে থাকে — কপি করা কোড, নামহীন সংখ্যা। কয়েক দিনে test-ভরা কিন্তু জট-পাকানো কোড তৈরি হয়। Refactor ধাপে নতুন test নয়, নতুন আচরণ নয় — শুধু গোছানো।

৩ · সবুজে পৌঁছানোর তিন কৌশল — fake it, triangulation, obvious implementation

Red থেকে green-এ যাওয়ার জন্য Beck তিনটি “green bar pattern” দেন। কোনটি নেবেন, তা নির্ভর করে সমাধানটি আপনি কতটা নিশ্চিতভাবে জানেন তার ওপর।

🎭 Fake it (till you make it)

Fake it: test যে মান চায়, সেটিই হুবহু ফেরত দিন — return 10. পরে ধাপে ধাপে constant-কে আসল হিসাবে বদলান। লাভ: দ্রুত সবুজ, আর প্রমাণ যে test আর runner ঠিকমতো জোড়া।

📐 Triangulation

Triangulation: একটি উদাহরণে constant যথেষ্ট; দ্বিতীয়, ভিন্ন উদাহরণ এলে তবেই সাধারণ নিয়ম লিখুন — যেমন জরিপে দুই বিন্দু থেকে তৃতীয় বিন্দুর অবস্থান। Beck বলেন, তিনি এটি নেন যখন সঠিক abstraction নিয়ে নিশ্চিত নন।

✅ Obvious implementation

Obvious implementation: সমাধান স্পষ্ট হলে সরাসরি আসল কোড লিখুন। শর্ত একটিই — অপ্রত্যাশিত লাল এলে অহংকার নয়, গিয়ার নামিয়ে আবার fake it বা triangulation-এর ছোট ধাপে ফিরুন।

নিচের Python উদাহরণে একটি কাল্পনিক মোবাইল wallet-এর নিয়ম: প্রতি পূর্ণ ১০০ টাকা top-up-এ ১টি loyalty point. প্রথম test (৫০০ টাকা → ৫ point) একটি fake দিয়েই পাস করে। দ্বিতীয় test (১২৫০ → ১২) fake-কে ধরে ফেলে, আর তখন সাধারণ নিয়ম লিখতে হয়। একই দুটি test দুই সংস্করণে চালানো হয়েছে।

import unittest

# Fictional wallet rule: 1 loyalty point for every full 100 taka of a top-up.
def points_fake(amount):           # "fake it": a constant that passes test 1
    return 5

def points_general(amount):        # triangulated: two examples force the rule
    return amount // 100

def make_tests(points):
    class PointsTest(unittest.TestCase):
        def test_1_500_taka_gives_5(self):
            self.assertEqual(points(500), 5)
        def test_2_1250_taka_gives_12(self):
            self.assertEqual(points(1250), 12)
    return PointsTest

def run(label, points, upto):
    loader = unittest.defaultTestLoader
    tests = loader.loadTestsFromTestCase(make_tests(points))
    chosen = unittest.TestSuite(list(tests)[:upto])
    result = unittest.TestResult()
    chosen.run(result)
    print(f"{label}: ran {result.testsRun}, failed {len(result.failures)}")
    for test, tb in result.failures:
        print("   ", test.id().split(".")[-1], "->", tb.strip().splitlines()[-1])

run("fake, test 1 only   ", points_fake, 1)
run("fake, tests 1 and 2 ", points_fake, 2)
run("general, tests 1, 2 ", points_general, 2)
print("extra check: 99 ->", points_general(99), "| 100 ->", points_general(100))

আউটপুট:

fake, test 1 only   : ran 1, failed 0
fake, tests 1 and 2 : ran 2, failed 1
    test_2_1250_taka_gives_12 -> AssertionError: 5 != 12
general, tests 1, 2 : ran 2, failed 0
extra check: 99 -> 0 | 100 -> 1

প্রথম লাইন: শুধু প্রথম test চালালে fake (return 5) নিখুঁতভাবে পাস — একটি উদাহরণ থেকে fake আর আসল নিয়মের পার্থক্য বোঝার উপায় নেই। দ্বিতীয় লাইন: দ্বিতীয় উদাহরণ যোগ হতেই fake ধরা পড়ল, AssertionError: 5 != 12. দুই বিন্দু এখন একটি সরলরেখা ঠিক করে দেয়: ৫০০ → ৫ আর ১২৫০ → ১২ মেলে amount // 100-এ (পূর্ণসংখ্যার ভাগ, ভগ্নাংশ বাদ)। তৃতীয় লাইনে সাধারণ সংস্করণ দুটিই পাস। শেষ লাইন সীমার দুটি মান দেখায় — ৯৯ টাকায় ০, ১০০ টাকায় ১ — যা পর্ব ৪৭-এর boundary value কৌশলে test list-এর পরের item হওয়ার যোগ্য। (unittest.TestResult-এ চালানো, যাতে সময়ের লাইন না থাকে আর আউটপুট প্রতিবার একই হয়।)

Fake চিরকালের জন্য নয়। Fake it একটি অস্থায়ী ধাপ — হয় একই চক্রের refactor-এ duplication সরিয়ে আসল হিসাব বসবে, নয়তো test list-এ এমন item থাকতে হবে যা fake-কে ধরবে। শুধু একটি উদাহরণের test রেখে fake ship করা মানে test সবুজ, কোড ভুল।

৪ · একটি পূর্ণ TDD kata — কাল্পনিক শহরের বাস ভাড়া, চক্রে চক্রে

Code kata হলো বারবার করার মতো ছোট অনুশীলন — কারাতের kata যেমন একই নড়াচড়া বারবার অনুশীলন করা। নামটি Dave Thomas জনপ্রিয় করেন বলে সাধারণত বলা হয়; প্রচলিত kata-র মধ্যে আছে Roy Osherove-র String Calculator আর Robert C. Martin-এর Bowling Game. আমাদের kata-র নিয়মগুলো একটি সম্পূর্ণ কাল্পনিক শহরের বাসের:

  • প্রথম ৩ কিমি পর্যন্ত ন্যূনতম ভাড়া ১০ টাকা।
  • তারপর প্রতি কিমি ৩ টাকা; শুরু করা কিমি পুরো কিমি ধরা হয় (৩.২ কিমি মানে বাড়তি ১ কিমি)।
  • ছাত্রছাত্রী অর্ধেক ভাড়া দেন; ভগ্নাংশ হলে ওপরের পূর্ণ টাকায় (৩১ টাকার অর্ধেক ১৫.৫ → ১৬)।
  • দূরত্ব ০ বা ঋণাত্মক হলে error.

শুরুর test list ছয়টি item-এর, সহজ থেকে কঠিন ক্রমে সাজানো। নিচের stepper প্রতিটি চক্র দেখায় — নতুন test, লাল অবস্থায় runner-এর আসল বার্তা, সবুজে যাওয়ার কৌশল, আর commit. বার্তাগুলো প্রতিটি অবস্থার কোড Python ৩.১৪-এ চালিয়ে সংগ্রহ করা।

Kata-র অবস্থা
চক্র
নতুন test
লাল অবস্থার বার্তা
সবুজের কৌশল
Refactor
Test (সবুজ/মোট)
Commit
  1. শুরু: শুধু test list. ছয়টি item, কোনো কোড নেই; সরল আচরণ আগে, error-এর নিয়ম শেষে।
  2. চক্র ১ — fake it. fare নামের function-ই নেই, তাই প্রথম লাল NameError. সবচেয়ে ছোট সবুজ: def fare(km): return 10.
  3. চক্র ২ — triangulation. ৪ কিমিতে fake ধরা পড়ে: 10 != 13. দুই উদাহরণ থেকে নিয়ম লেখা হলো, তারপর আলাদা refactor commit-এ constant (অংশ ২-এর stepper-এর চক্র)।
  4. চক্র ৩ — যে test প্রথমবারেই সবুজ। ৩ কিমির সীমা আগের কোডেই ঠিক ছিল (max(0, 0) * 3 = 0). নতুন কোড লাগল না। Test-টি তবু রাখা হলো সীমার দলিল হিসেবে, আলাদা test: commit-এ। প্রথমবারেই সবুজ হলে কোড একটু ভেঙে দেখে নিন test ফেল করতে পারে কি না।
  5. চক্র ৪ — floating point-এর বার্তা। ৩.২ কিমিতে বাড়তি ০.২ কিমি × ৩ = ০.৬, তাই 10.600000000000001 — শেষের অংশ floating point-এর স্বাভাবিক আচরণ। নিয়ম “শুরু করা কিমি পুরো”, তাই সমাধান স্পষ্ট: math.ceil.
  6. চক্র ৫ — নতুন parameter. student=True দিয়ে ডাকতেই TypeError: function এই keyword চেনে না। Default False, যাতে পুরোনো চারটি ডাক অক্ষত থাকে। ৩১-এর অর্ধেক ১৫.৫, ceil-এ ১৬।
  7. চক্র ৬ — error-এর নিয়ম। ০ কিমিতে আগের কোড ১০ টাকা দিয়ে দিচ্ছিল, তাই ValueError not raised. শুরুতে একটি guard যোগ হলো, আর refactor-এ বাড়তি কিমির হিসাব extra_km নামে আলাদা লাইনে। ছয়টি test সবুজ, তালিকা খালি।

এবার kata-র শেষ অবস্থা — production কোড আর তার পুরো test suite. চালিয়ে দেখুন সব সবুজ কি না।

import math, unittest

BASE_FARE, BASE_KM, PER_KM = 10, 3, 3       # fictional city-bus fare rules

def fare(km, student=False):
    if km <= 0:
        raise ValueError("distance must be positive")
    extra_km = math.ceil(max(0, km - BASE_KM))   # a started km counts as a full km
    full = BASE_FARE + extra_km * PER_KM
    return math.ceil(full / 2) if student else full

class FareTest(unittest.TestCase):
    def test_one_km_costs_minimum(self):
        self.assertEqual(fare(1), 10)
    def test_four_km_adds_one_extra_km(self):
        self.assertEqual(fare(4), 13)
    def test_three_km_is_still_minimum(self):
        self.assertEqual(fare(3), 10)
    def test_partial_km_counts_as_full_km(self):
        self.assertEqual(fare(3.2), 13)
    def test_student_pays_half_rounded_up(self):
        self.assertEqual(fare(10, student=True), 16)
    def test_zero_km_is_rejected(self):
        with self.assertRaises(ValueError):
            fare(0)

suite = unittest.defaultTestLoader.loadTestsFromTestCase(FareTest)
names = [t.id().split(".")[-1] for t in suite]       # loader sorts names A-Z
result = unittest.TestResult()
suite.run(result)
bad = {t.id().split(".")[-1] for t, _ in result.failures + result.errors}
for n in names:
    print(("FAIL  " if n in bad else "ok    ") + n)
print(f"{result.testsRun} tests, {len(bad)} failed")

আউটপুট:

ok    test_four_km_adds_one_extra_km
ok    test_one_km_costs_minimum
ok    test_partial_km_counts_as_full_km
ok    test_student_pays_half_rounded_up
ok    test_three_km_is_still_minimum
ok    test_zero_km_is_rejected
6 tests, 0 failed

ছয়টি test, শূন্য ব্যর্থ। নামগুলো বর্ণানুক্রমে ছাপা, কারণ unittest-এর loader test method-এর নাম A-Z সাজায় — লেখার ক্রমে নয়। এজন্য ভালো unit test একে অপরের ওপর নির্ভর করে না (পর্ব ৪৬)। কিন্তু শেষ সবুজ অবস্থা পুরো গল্প নয়। লেখার সময় প্রতিটি মধ্যবর্তী অবস্থা চালিয়ে যে লাল-সবুজের ইতিহাস ধরা হয়েছিল, তা এই:

C1 red         0 ok, ERROR NameError: name 'fare' is not defined
C1 green       1 ok
C2 red         1 ok, FAIL AssertionError: 10 != 13
C2 green       2 ok
C2 refactor    2 ok
C3 (new test)  3 ok
C4 red         3 ok, FAIL AssertionError: 10.600000000000001 != 13
C4 green       4 ok
C5 red         4 ok, ERROR TypeError: fare() got an unexpected keyword argument 'student'
C5 green       5 ok
C6 red         5 ok, FAIL AssertionError: ValueError not raised
C6 green       6 ok

এখানে C1 মানে চক্র ১, C2 চক্র ২, এভাবে। প্রতিটি “red” সারিতে ঠিক একটি test ফেল বা error করেছে, বাকিগুলো সবুজ — এটিই baby steps-এর চিহ্ন। চক্র ৩-এ কোনো লাল নেই: সেই test নতুন আচরণ চায়নি, শুধু সীমা দলিল করেছে। FAIL মানে assertion মেলেনি; ERROR মানে test চলতে গিয়েই exception — যেমন অজানা নাম বা অজানা keyword.

Git-এ kata: প্রতিটি সবুজে একটি commit

নিচের transcript একটি আসল scratch repository থেকে (Git ২.৫৩-এ, লেখক Rahim, তারিখ নির্দিষ্ট করে দেওয়া, তাই hash পুনরুৎপাদনযোগ্য)। প্রতিটি commit-এর আগে পুরো suite চালিয়ে সবুজ নিশ্চিত করা হয়েছিল।

$ git log --oneline --reverse
8050ffd feat: charge minimum fare of 10 taka (fake it)
36c8721 feat: add 3 taka per km after 3 km (triangulated)
e254efc refactor: name fare constants, drop the if
f39c6ad test: pin 3 km boundary at minimum fare
6e3091f feat: count a started km as a full km
c489225 feat: half fare for students, rounded up
6afd7c5 feat: reject zero or negative distance

$ git log --format="%h %ad %s" --date=format:%H:%M -3
6afd7c5 10:24 feat: reject zero or negative distance
c489225 10:20 feat: half fare for students, rounded up
6e3091f 10:16 feat: count a started km as a full km

$ git show --stat --format="%h %s" e254efc
e254efc refactor: name fare constants, drop the if

 fare.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

$ git show --stat --format="%h %s" f39c6ad
f39c6ad test: pin 3 km boundary at minimum fare

 test_fare.py | 2 ++
 1 file changed, 2 insertions(+)

কী দেখা গেল:

  • git log --oneline --reverse: সাতটি commit পুরোনো থেকে নতুন ক্রমে — পাঁচটি feat:, একটি refactor:, একটি test:. পরের --format-এর সময় দেখায়, প্রতিটি চক্র প্রায় ৪ মিনিটের (তারিখ নির্দিষ্ট করে দেওয়া)।
  • e254efc (refactor): শুধু fare.py বদলেছে, test-এর file ছোঁয়া হয়নি — refactoring-এ ঠিক এটাই প্রত্যাশিত (পর্ব ৪২)।
  • f39c6ad (test): উল্টো ছবি — শুধু test_fare.py-তে দুই লাইন। চক্র ৩-এর সেই test, যা প্রথমবারেই সবুজ ছিল।
  • প্রতিটি commit সবুজ, তাই পরে কোনো bug খুঁজতে git bisect (পর্ব ৫৫) যেকোনো commit-এ থেমে test চালাতে পারে। অনেক দল অবশ্য push-এর আগে এমন ছোট commit interactive rebase-এ মিলিয়ে নেয় (পর্ব ১৫) — দলের নিয়মভেদে আলাদা।

৫ · TDD নকশায় চাপ দেয় — testable কোড, outside-in আর inside-out

TDD-তে কোডের প্রথম ব্যবহারকারী আপনি নিজেই — test-এর ভেতর থেকে। যে কোড test থেকে ডাকা কঠিন, TDD-তে সেই কোড লেখাই কঠিন। এই অস্বস্তিকেই বলা হয় TDD-র নকশার চাপ (design pressure). Steve Freeman আর Nat Pryce এর নাম দিয়েছেন “listening to the tests” — test লিখতে কষ্ট হলে সেটি নকশার একটি সংকেত, test-এর দোষ নয়।

একটি উদাহরণ নিন। কাল্পনিক বাস কোম্পানি ঠিক করল, ঈদের আগের তিন দিন ভাড়া ২০% বেশি, আর প্রতিটি টিকিটের পর যাত্রীকে SMS যাবে। TDD ছাড়া প্রথম খসড়া প্রায়ই এমন হয়:

import datetime, sms_gateway            # a real SMS service

def sell_ticket(km, phone):
    price = fare(km)
    if is_eid_week(datetime.date.today()):   # hidden dependency: the clock
        price = round(price * 1.2)
    sms_gateway.send(phone, f"Fare {price} taka")   # hidden side effect
    return price

এর test লিখতে গেলেই প্রশ্ন: ঈদের হিসাব যাচাই করতে কি ঈদ পর্যন্ত অপেক্ষা? প্রতিটি test কি সত্যিকারের SMS পাঠাবে? TDD-তে এই প্রশ্ন কোডের আগেই ওঠে, আর স্বাভাবিক উত্তর হয় dependency বাইরে থেকে দেওয়া:

def sell_ticket(km, phone, today, notifier):
    price = fare(km)
    if is_eid_week(today):                   # the date is an input now
        price = round(price * 1.2)
    notifier.send(phone, f"Fare {price} taka")  # any object with send()
    return price

# in a test:  sell_ticket(1, "01700000000", date(2026, 3, 18), FakeNotifier())

এখন তারিখ একটি সাধারণ input, আর SMS-এর জায়গায় test একটি নকল notifier দিতে পারে — পর্ব ৩৭-এর dependency injection, আর গভীরে পর্ব ২৮-এর dependency inversion principle. এই ধরনের নকশা testability বাড়ায়, আর সেই সাথে coupling কমায়: ব্যবসার নিয়ম আর SMS-এর প্রযুক্তি আলাদা হয়ে যায়, যা পর্ব ৪০-এর hexagonal architecture-এর port আর adapter-এর ছোট রূপ। নকল notifier কোন ধরনের test double — stub, fake, spy না mock — সেই পার্থক্য পর্ব ৪৯-এ।

দুই ধারা: inside-out (classic) আর outside-in (London)

কোথা থেকে শুরু করবেন — ভেতরের ছোট হিসাব থেকে, না বাইরের ব্যবহারকারীর দিক থেকে? এ নিয়ে দুটি প্রচলিত ধারা আছে। নামগুলো আনুষ্ঠানিক নয়; Martin Fowler ২০০৭ সালের “Mocks Aren't Stubs” লেখায় দুই দলকে “classicist” আর “mockist” বলেছেন, আর অনেকে জায়গার নামে “Detroit/Chicago school” আর “London school” বলেন।

দিকInside-out (classic)Outside-in (London)
শুরুDomain-এর ছোট, ভেতরের অংশ — যেমন fare()System-এর কিনারা — একটি acceptance test বা controller
Collaboratorসম্ভব হলে আসল object; নকল শুধু ধীর বা বাইরের জিনিসেএখনো না লেখা collaborator-এর জায়গায় mock
কী যাচাই হয়ফল আর অবস্থা (state): কী ফেরত এলআচরণ আর কথোপকথন (interaction): কাকে কী বলা হলো
নকশা কীভাবে আসেRefactor-এ, duplication সরাতে গিয়েTest লেখার সময়ে, “আমার কাকে দরকার” প্রশ্নে interface আবিষ্কার
ঝুঁকিশেষে টুকরোগুলো জোড়া না-ও লাগতে পারে; বাড়তি কোড যা কেউ ডাকে নাTest implementation-এ বাঁধা; refactor করলেই অনেক test ভাঙে
মূল বইBeck, TDD: By Example (২০০২/২০০৩)Freeman ও Pryce, Growing Object-Oriented Software, Guided by Tests (২০০৯)

বাস ভাড়ার kata ছিল inside-out: একটি বিশুদ্ধ হিসাব, কোনো collaborator নেই। টিকিট বিক্রির পুরো feature হলে outside-in শুরু হতো “টিকিট কিনলে ভাড়া দেখায় আর SMS যায়” — এই acceptance test থেকে, আর notifier-এর interface আবিষ্কার হতো mock দিয়ে। বাস্তবে বেশিরভাগ দল দুটি মেশায়।

৬ · ATDD আর BDD — বাইরের চক্র

TDD-র ছোট চক্র নিশ্চিত করে যে প্রতিটি টুকরো যা করার কথা তা করে। কিন্তু “কী করার কথা” — সেটি কে ঠিক করে? পর্ব ২২-এ দেখেছেন, user story-র acceptance criteria সেই উত্তর দেয়, আর Given-When-Then উদাহরণে লিখলে তা দ্ব্যর্থহীন হয়। Acceptance test-driven development (ATDD) এই উদাহরণগুলোকে কাজ শুরুর আগেই স্বয়ংক্রিয় acceptance test বানায়। সেগুলো শুরুতে ফেল করে; story শেষ ধরা হয় যখন সব পাস করে।

দুই চক্র: বাইরে ATDD/BDD, ভেতরে TDD

একটি acceptance test সবুজ করতে ভেতরের চক্র অনেকবার ঘোরে। বাইরের চক্র ঘণ্টা বা দিনের, ভেতরেরটি মিনিটের।

ATDD/BDD-র বাইরের চক্র আর TDD-র ভেতরের চক্র ১ · Scenario লিখুন acceptance test লাল ৩ · Acceptance test সবুজ ৪ · Refactor, পরের scenario ২ · ভেতরে TDD red → green → refactor বাইরে: ঘণ্টা-দিন · ভেতরে: মিনিট

Freeman ও Pryce-এর বইয়ে বাইরের আর ভেতরের feedback চক্রের এই ছবি আছে; অনেকে একে double-loop TDD বলেন। ATDD-র শিকড় আরও পুরোনো: Ward Cunningham-এর FIT (২০০২-এর দিকে) টেবিলে লেখা উদাহরণকে acceptance test হিসেবে চালাত, আর Gojko Adzic-এর specification by example একই ভাবনার বড় রূপ।

BDD — শব্দ বদলালে ভাবনা বদলায়

Behaviour-driven development (BDD) এসেছে TDD শেখানোর একটি সমস্যা থেকে। Dan North ২০০৬ সালের “Introducing BDD” লেখায় বলেন, TDD শিখতে আসা মানুষ বারবার একই প্রশ্ন করতেন: কোথা থেকে শুরু করব, কী test করব, কী নয়, test-এর নাম কী দেব, ফেল করলে বুঝব কীভাবে? তাঁর উত্তর ছিল শব্দ বদলানো: “test”-এর বদলে “behaviour”, আর test-এর নাম একটি বাক্য — should charge minimum fare for short trips. প্রশ্ন তখন হয়ে যায় “system-এর পরের কোন আচরণটি সবচেয়ে জরুরি?” North এই ভাবনায় JBehave নামে একটি Java framework লেখেন, আর Chris Matts-এর সাথে Given-When-Then ছাঁচে পৌঁছান বলে উল্লেখ করেন।

BDD-র তিনটি মূল উপাদান:

  • সবার এক ভাষা: Eric Evans-এর domain-driven design (২০০৩) থেকে নেওয়া ubiquitous language — ব্যবসার মানুষ যে শব্দ বলেন (“top-up”, “ব্যালান্স”, “loyalty point”), scenario আর কোডেও ঠিক সেই শব্দ।
  • উদাহরণই specification: Given-When-Then scenario মানুষ পড়তে পারে আবার tool চালাতেও পারে — একটি executable specification. নিয়মিত চালানো হয় বলে এটি living documentation: কোড বদলালে দলিল নিজেই লাল হয়ে জানায়, পুরোনো হয়ে পড়ে থাকে না।
  • আলাপ আগে, automation পরে: scenario লেখার আগে three amigos — ব্যবসার প্রতিনিধি (যেমন product owner), developer আর tester — একসাথে বসে নিয়ম আর উদাহরণ ঠিক করেন। নামটি জনপ্রিয় করার কৃতিত্ব সাধারণত George Dinwiddie-কে দেওয়া হয়। এমন আলোচনার একটি জনপ্রিয় কৌশল example mapping (Matt Wynne, ২০১৫-র দিকে): story হলুদ কার্ডে, নিয়ম নীলে, উদাহরণ সবুজে, অমীমাংসিত প্রশ্ন লালে।

বিয়েবাড়ির আয়োজনের আগে কনেপক্ষ, বাবুর্চি আর ডেকোরেটর একসাথে বসার কথা ভাবুন। “ভালো খাবার চাই” বললে কিছুই ঠিক হয় না। কিন্তু উদাহরণে বললে হয়: “অতিথি ৩০০ জন হলে বিরিয়ানি ৭৫ কেজি চালের; বৃষ্টি হলে প্যান্ডেল উঠোনে নয়, ছাদে।” এই উদাহরণ সবাই বোঝেন, বাবুর্চি হিসাব মেলাতে পারেন, আর বিয়ের দিন কনেপক্ষ মিলিয়ে দেখতে পারেন। Three amigos-এর আলাপ ঠিক এই — Gherkin শুধু সেই আলাপের লিখিত রূপ।

দিকTDDATDDBDD
মূল প্রশ্নএই টুকরো কোড ঠিক কাজ করে?এই story গ্রহণযোগ্য?System কোন আচরণ করবে, সবাই কি একমত?
কে লেখেনDeveloperদল, গ্রাহকের সাথেThree amigos মিলে; automation developer বা tester
ভাষাProgramming language-এ testটেবিল, Gherkin বা কোডব্যবসার ভাষায় Given-When-Then

ATDD আর BDD-র সীমানা ঝাপসা; অনেকে দুটিকে একই চর্চার দুই নাম বলেন — পরীক্ষায় এই মতভেদটুকুও উল্লেখ করুন।

৭ · Step definition — Gherkin-এর লেখা থেকে কোড পর্যন্ত

পর্ব ২২-এর কাল্পনিক রিচার্জ অ্যাপের Gherkin scenario মনে করুন: “Given my wallet balance is 500 taka, When I top up 100 taka…”। লেখাটি নিজে কিছু চালায় না। একটি BDD tool লেখাটিকে কোডের সাথে জোড়ে step definition দিয়ে: প্রতিটি ধাপের লেখার একটি ছাঁচ (regular expression, বা কিছু tool-এ {int} ধরনের সহজ expression) আর একটি function. মোটামুটি সব tool একই কাজ করে:

  1. Feature file পড়ে প্রতিটি scenario-র ধাপগুলো আলাদা করে।
  2. Given/When/Then/And/But keyword বাদ দিয়ে বাকি লেখা প্রতিটি step definition-এর ছাঁচের সাথে মেলায়।
  3. ছাঁচের বন্ধনীতে ধরা অংশ (যেমন 500) argument হয়ে function-এ যায়।
  4. প্রতিটি scenario একটি নতুন, খালি context বা “world” object পায়।
  5. একটি ধাপ ফেল করলে সেই scenario-র বাকি ধাপ চালানো হয় না (skipped); কোনো ছাঁচই না মিললে ধাপটি “undefined” বলে রিপোর্ট হয়।

প্রচলিত tool-এর মধ্যে আছে Cucumber (Ruby-তে শুরু, এখন JavaScript, Java-সহ অনেক ভাষায়), Python-এ behave আর pytest-bdd. ছাঁচের ব্যাকরণ আর রিপোর্টের ধরন tool আর সংস্করণভেদে আলাদা — লেখার সময়ের অবস্থা জানতে প্রতিটির documentation দেখুন। নিচের stepper একটি ধাপের পুরো যাত্রা দেখায়।

  1. Scenario.featureব্যবসার ভাষা
  2. মেলানোregexstep definition
  3. কোডtopUp()আসল নিয়ম
  4. রিপোর্টPASS/FAILপ্রতি ধাপে
topup.featureScenario: Not enough balance
Scenario: Not enough balance
  Given my wallet balance is 50 taka
  When I top up 100 taka
  Then my balance should be 50 taka
  And I should see "Not enough money"
Step definition-এর তালিকাkeyword বাদ দিয়ে মেলানো
লেখা:  "my balance should be 50 taka"

/^my wallet balance is (\d+) taka$/   ✗
/^I top up (\d+) taka$/              ✗
/^I should see "(.*)"$/              ✗
/^my balance should be (\d+) taka$/   ✓  ধরা অংশ: "50"
World + কোডআগের দুই ধাপ যা রেখে গেছে
Given → w.balance = 50
When  → w.msg = topUp(w, 100)

function topUp(wallet, amount) {
  wallet.balance -= amount;      // আগে কাটে …
  return wallet.balance < 0 ? "Not enough money" : …
}
w = { balance: -50, msg: "Not enough money" }
Step functionargument "50" দিয়ে ডাকা
(w, b) => {
  if (w.balance !== +b)            // -50 !== 50
    throw `balance is ${w.balance}`;
}
রিপোর্টব্যর্থতার পরে বাকিটা skipped
Scenario: Not enough balance
  PASS Given my wallet balance is 50 taka
  PASS When I top up 100 taka
  FAIL Then my balance should be 50 taka  <- balance is -50
  SKIP And I should see "Not enough money"
  1. ব্যবসার ভাষার scenario. ব্যালান্স ৫০, চাওয়া ১০০ — নিয়ম বলে রিচার্জ হবে না, টাকাও কাটবে না। চিহ্নিত ধাপটির যাত্রা দেখি।
  2. ছাঁচ মেলানো। “Then” বাদ দিয়ে লেখাটি চারটি step definition-এর ছাঁচের সাথে মেলানো হয়। চতুর্থটি মেলে, আর বন্ধনী (\d+) ধরে "50" — একটি string, সংখ্যা নয়।
  3. World-এ আগের ধাপের ফল। Given ধাপ world-এ ব্যালান্স ৫০ রেখেছিল; When ধাপ আসল topUp ডেকেছিল। কিন্তু topUp আগে টাকা কাটে, পরে দেখে — তাই ব্যালান্স এখন −৫০, যদিও বার্তা ঠিকই “Not enough money”.
  4. Step function চলে। +b string “50”-কে সংখ্যা ৫০ বানায়। −৫০ আর ৫০ মেলে না, তাই function একটি বার্তা ছুড়ে (throw) ধাপটিকে ব্যর্থ করে।
  5. রিপোর্ট: FAIL, তারপর SKIP. ব্যর্থ ধাপের পর scenario-র বাকি ধাপ আর চলে না। শুধু বার্তা দেখার scenario এই bug ধরত না।

এবার পুরো ব্যাপারটি চালিয়ে দেখুন — দুটি scenario, চারটি step definition, আর একটি কাল্পনিক wallet-এর topUp, যাতে ইচ্ছা করে একটি bug রাখা। এটি আসল BDD tool নয়: Background, Scenario Outline, টেবিল বা tag সামলায় না।

// A toy BDD runner: Gherkin lines -> regex step definitions -> code (fictional wallet)
const feature = `Scenario: Successful top-up
  Given my wallet balance is 500 taka
  When I top up 100 taka
  Then I should see "Top-up successful"
  And my balance should be 400 taka
Scenario: Not enough balance
  Given my wallet balance is 50 taka
  When I top up 100 taka
  Then my balance should be 50 taka
  And I should see "Not enough money"`;

function topUp(wallet, amount) {             // system under test (it has a bug)
  wallet.balance -= amount;                  // deducts BEFORE checking the balance
  return wallet.balance < 0 ? "Not enough money" : "Top-up successful";
}
const steps = [                              // step definitions: regex -> code
  [/^my wallet balance is (\d+) taka$/, (w, b) => { w.balance = Number(b); }],
  [/^I top up (\d+) taka$/, (w, a) => { w.msg = topUp(w, Number(a)); }],
  [/^I should see "(.*)"$/, (w, m) => { if (w.msg !== m) throw `saw "${w.msg}"`; }],
  [/^my balance should be (\d+) taka$/, (w, b) => { if (w.balance !== +b) throw `balance is ${w.balance}`; }],
];
let world, failed, pass = 0, fail = 0, skip = 0;
for (const line of feature.split("\n").map(l => l.trim())) {
  if (line.startsWith("Scenario:")) { world = {}; failed = false; console.log(line); continue; }
  const text = line.replace(/^(Given|When|Then|And|But) /, "");
  if (failed) { skip++; console.log("  SKIP " + line); continue; }
  const def = steps.find(([re]) => re.test(text));
  try {
    if (!def) throw "no matching step definition";
    def[1](world, ...text.match(def[0]).slice(1));
    pass++; console.log("  PASS " + line);
  } catch (e) { failed = true; fail++; console.log("  FAIL " + line + "  <- " + e); }
}
console.log(`steps: ${pass} passed, ${fail} failed, ${skip} skipped`);

আউটপুট:

Scenario: Successful top-up
  PASS Given my wallet balance is 500 taka
  PASS When I top up 100 taka
  PASS Then I should see "Top-up successful"
  PASS And my balance should be 400 taka
Scenario: Not enough balance
  PASS Given my wallet balance is 50 taka
  PASS When I top up 100 taka
  FAIL Then my balance should be 50 taka  <- balance is -50
  SKIP And I should see "Not enough money"
steps: 6 passed, 1 failed, 1 skipped

প্রথম scenario-র চারটি ধাপই পাস: ৫০০ থেকে ১০০ কাটলে ৪০০, বার্তা ঠিক। দ্বিতীয়টিতে Given আর When পাস (ধাপ দুটি শুধু অবস্থা তৈরি করে), কিন্তু ব্যালান্সের Then ফেল করে balance is -50 বার্তায়, আর শেষ ধাপ skipped. মোট ৬টি পাস, ১টি ফেল, ১টি skipped — মোট ৮টি ধাপ। এখন ভেতরের TDD চক্রে নামার পালা: topUp-এর জন্য একটি unit test লিখুন যা ব্যালান্স −৫০ হওয়া ধরে (লাল), তারপর ন্যূনতম সমাধান — আগে যাচাই, পরে কাটা: if (amount > wallet.balance) return "Not enough money"; তারপর wallet.balance -= amount; (সবুজ)। তখন দুটিই সবুজ — double loop-এর একটি পূর্ণ ঘূর্ণন।

বাস্তব tool-এ একই step definition দেখতে কেমন? নিচে behave-ধাঁচের Python (এখানে চালানো হয়নি; behave আলাদা package). ছাঁচগুলো regular expression নয়, {balance:d} ধরনের সহজ format — যা সংখ্যায় রূপান্তরও করে দেয়। ভাগ করা অবস্থা থাকে context object-এ।

# features/steps/topup_steps.py  (illustration, needs: pip install behave)
from behave import given, when, then
from wallet import Wallet, top_up

@given("my wallet balance is {balance:d} taka")
def step_balance(context, balance):
    context.wallet = Wallet(balance)

@when("I top up {amount:d} taka")
def step_top_up(context, amount):
    context.message = top_up(context.wallet, amount)

@then('I should see "{message}"')
def step_message(context, message):
    assert context.message == message

@then("my balance should be {balance:d} taka")
def step_check_balance(context, balance):
    assert context.wallet.balance == balance

Step definition পাতলা রাখুন। এগুলো শুধু লেখা আর কোডের মাঝের আঠা (glue): argument রূপান্তর, আসল কোড ডাকা, ফল যাচাই। ব্যবসার নিয়ম step-এ লিখলে test নিজেকেই test করে। আর সম্ভব হলে step থেকে UI নয়, application-এর ভেতরের স্তর (service বা use case) ডাকুন — দ্রুত আর কম ভঙ্গুর। কোন test কোন স্তরে, সেই ভারসাম্য পর্ব ৫১-এর test pyramid-এ।

৮ · প্রমাণ কী বলে, কখন TDD নয়, আর সাধারণ ভুল

গবেষণার প্রমাণ — মিশ্র

TDD নিয়ে সবচেয়ে বেশি উদ্ধৃত শিল্প-গবেষণাগুলোর একটি Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat আর Laurie Williams-এর ২০০৮ সালের লেখা (Empirical Software Engineering জার্নালে)। তাঁরা চারটি শিল্প-দলের — একটি IBM-এর, তিনটি Microsoft-এর — case study করেন। লেখকদের রিপোর্ট অনুযায়ী, TDD ব্যবহারকারী দলগুলোর release-এর আগের defect density তুলনীয় অন্য project-এর চেয়ে উল্লেখযোগ্যভাবে কম ছিল (প্রায় ৪০% থেকে ৯০% কম, আমাদের জানামতে), আর দলগুলোর ব্যবস্থাপকদের হিসাবে শুরুর development সময় লেগেছিল আনুমানিক ১৫–৩৫% বেশি। কিন্তু এটি case study, randomized পরীক্ষা নয়: দলগুলো নিজেরাই TDD বেছেছিল, আর তুলনার project হুবহু এক নয়।

নিয়ন্ত্রিত পরীক্ষা আর meta-analysis-এর ছবি অস্পষ্টতর। কয়েকটি meta-analysis (যেমন Rafique ও Mišić, ২০১৩) গুণগত মানে ছোট উন্নতি পেয়েছে, কিন্তু productivity-তে স্পষ্ট লাভ বা ক্ষতি নয়। Davide Fucci ও সহকর্মীদের একটি গবেষণা (IEEE TSE, ২০১৭) ইঙ্গিত দেয় যে লাভের বড় অংশ হয়তো আসে ছোট, নিয়মিত ধাপ থেকে — test আগে না পরে, সেই ক্রম থেকে ততটা নয়। সৎ সারাংশ: TDD নিয়ে কোনো সংখ্যাকে সর্বজনীন সত্য বলা যায় না; প্রসঙ্গ, দলের দক্ষতা আর কাজের ধরন ফল বদলায়।

কখন খাটে, কখন খাটে না

ভালো খাটেকম খাটে বা অন্যভাবে
নিয়ম আর হিসাব: ভাড়া, কর, ছাড়, GPA, parsingঅজানা প্রযুক্তি শেখা — আগে spike: সময় বেঁধে খেলনা কোড, শেখা হলে ফেলে দিয়ে TDD-তে নতুন করে
Bug fix: আগে bug-টি ধরে এমন লাল test, তারপর সমাধানUI-র চেহারা আর অনুভূতি খোঁজা — রং, বিন্যাস, animation; চোখে দেখে ঠিক করা হয়
Library আর API: ডাকার রূপ আগে ভাবা জরুরিPrototype বা demo, যা কয়েক দিনে ফেলে দেওয়া হবে
পুরোনো কোড বদলানো — আগে characterization test (পর্ব ৪৩), তারপর TDD-তে নতুন অংশData অনুসন্ধান বা গতি মাপা — সেখানে মাপই প্রধান যাচাই

সাধারণ ভুল

ভুললক্ষণপ্রতিকার
Implementation test করাPrivate function বা ভেতরের ডাকের ক্রম assert; rename করলেই test ভাঙেবাইরের আচরণ যাচাই — input দিয়ে ফল বা দেখা যায় এমন প্রভাব
বিশাল ধাপএক চক্রে ২০ মিনিট লাল, একসাথে পাঁচটি নিয়মTest list-এর item ভাঙা; ধাপ ছোট; ভুল হলে শেষ সবুজে ফেরা
Refactor বাদTest অনেক, কিন্তু কপি করা কোড আর নামহীন সংখ্যাপ্রতিটি সবুজের পরে এক মিনিট গোছানো, আলাদা commit
লাল না দেখাTest প্রথমবারেই সবুজ, কেউ খেয়াল করেনিনতুন test আগে চালিয়ে লাল দেখা; সবুজ হলে কোড ভেঙে যাচাই
সবকিছু mockTest-এ mock-এর setup কোডের চেয়ে লম্বাবাইরের জগতের সীমানায় নকল; ভেতরে আসল object (পর্ব ৪৯)
Coverage-ই লক্ষ্য১০০% লাইন চলে, assertion দুর্বলAssertion-এর মান যাচাই, mutation testing (পর্ব ৫০)

ধাপ কত বড় হচ্ছে, তা অনুভবে না রেখে মাপা যায়। নিচের মডেল একটি অনুশীলন-সেশনের লগ (সম্পূর্ণ কাল্পনিক) পড়ে: প্রতিটি লাইনে সময় আর ঘটনা — red মানে নতুন test লেখা শুরু, green মানে সব test সবুজ, refactor মানে গোছানো শুরু, end সেশন শেষ। ধরে নেওয়া হয়েছে: একটি চক্র শুরু হয় red-এ আর শেষ হয় পরের red বা end-এ; প্রতিটি অবস্থা চলে পরের ঘটনা পর্যন্ত; ১০ মিনিটের বেশি চক্র “giant step”. সীমাটি একটি নিয়মমাফিক পছন্দ, কোনো মানদণ্ড নয়।

// TDD session log -> cycles and time per phase (fictional log of one practice session)
// Assumptions: a cycle starts at "red" and ends at the next "red" or at "end";
// a phase lasts until the next event; a cycle over 10 min is flagged as a giant step.
const log = `00:00 red|01:30 green|02:10 refactor|03:00 red|04:20 green|05:30 refactor|
07:00 red|07:40 green|08:30 red|10:00 green|10:50 refactor|12:00 red|25:30 green|
26:00 red|27:10 green|28:00 refactor|29:30 end`;
const ev = log.replace(/\s+/g, " ").split("|").map(s => s.trim().split(" "))
  .map(([t, p]) => ({ sec: +t.slice(0, 2) * 60 + +t.slice(3), p }));
const mmss = s => `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
const cycles = [], total = { red: 0, green: 0, refactor: 0 };
for (let i = 0; i < ev.length - 1; i++) {
  const d = ev[i + 1].sec - ev[i].sec;
  if (ev[i].p === "red") cycles.push({ start: ev[i].sec, red: 0, green: 0, refactor: 0 });
  const c = cycles[cycles.length - 1];
  c[ev[i].p] += d; total[ev[i].p] += d;
}
cycles.forEach((c, k) => {
  const len = c.red + c.green + c.refactor;
  const notes = [len > 600 ? "GIANT STEP" : "", c.refactor === 0 ? "no refactor" : ""].filter(Boolean);
  console.log(`cycle ${k + 1} at ${mmss(c.start)}: red ${mmss(c.red)} green ${mmss(c.green)}` +
    ` refactor ${mmss(c.refactor)} = ${mmss(len)}` + (notes.length ? "  " + notes.join(", ") : ""));
});
const all = ev[ev.length - 1].sec;
for (const p of ["red", "green", "refactor"])
  console.log(`${p.padEnd(8)} ${mmss(total[p])} (${Math.round(100 * total[p] / all)}%)`);
console.log(`${cycles.length} cycles in ${mmss(all)}, mean ${mmss(Math.round(all / cycles.length))} per cycle`);

আউটপুট:

cycle 1 at 00:00: red 01:30 green 00:40 refactor 00:50 = 03:00
cycle 2 at 03:00: red 01:20 green 01:10 refactor 01:30 = 04:00
cycle 3 at 07:00: red 00:40 green 00:50 refactor 00:00 = 01:30  no refactor
cycle 4 at 08:30: red 01:30 green 00:50 refactor 01:10 = 03:30
cycle 5 at 12:00: red 13:30 green 00:30 refactor 00:00 = 14:00  GIANT STEP, no refactor
cycle 6 at 26:00: red 01:10 green 00:50 refactor 01:30 = 03:30
red      19:40 (67%)
green    04:50 (16%)
refactor 05:00 (17%)
6 cycles in 29:30, mean 04:55 per cycle

ছয়টি চক্রের পাঁচটি ১½ থেকে ৪ মিনিটের — স্বাস্থ্যকর ছন্দ। চক্র ৫ আলাদা: ১২:০০ থেকে ২৫:৩০ পর্যন্ত ১৩½ মিনিট লাল, তারপর refactor ছাড়াই পরের test — giant step আর refactor বাদ, দুটি ভুল একসাথে। মোট হিসাব: লাল ১৯:৪০ + সবুজ ৪:৫০ + refactor ৫:০০ = ২৯:৩০; লালের ৬৭% ভাগের বেশিরভাগই ওই এক চক্রের। গড় চক্র ১৭৭০ ÷ ৬ = ২৯৫ সেকেন্ড, অর্থাৎ ৪:৫৫ — কিন্তু একটি লম্বা চক্র গড়কে টেনে তোলে, তাই প্রতিটি চক্র আলাদা দেখাই বেশি কাজের। চক্র ৩-এর দেড় মিনিটে গোছানোর কিছু না থাকাও স্বাভাবিক।

আরও গভীরে: মূল উৎস Beck-এর Test-Driven Development: By Example (২০০২/২০০৩) আর Freeman ও Pryce-এর Growing Object-Oriented Software, Guided by Tests (২০০৯)। Mock object-এর ধারণা প্রথম আনুষ্ঠানিকভাবে আসে Tim Mackinnon, Steve Freeman ও Philip Craig-এর XP2000 সম্মেলনের “Endo-Testing: Unit Testing with Mock Objects” লেখায়। Robert C. Martin-এর “Transformation Priority Premise” (২০১৩-র দিকে) fake it থেকে সাধারণ কোডে যাওয়ার ছোট রূপান্তরগুলোকে পছন্দের ক্রমে সাজানোর একটি প্রস্তাব — বিতর্কিত, কিন্তু triangulation বুঝতে কাজের।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • TDD-র চক্র ব্যাখ্যা করুন। — Red: একটি ছোট ব্যর্থ test; green: পাস করানোর ন্যূনতম কোড; refactor: test সবুজ রেখে duplication আর নাম ঠিক করা; তারপর আবার।
  • নতুন test-কে আগে লাল দেখা কেন জরুরি? — প্রমাণ হয় test-টি ফেল করতে পারে আর ঠিক কারণে ফেল করছে; নইলে সবুজ মানেই নিশ্চয়তা নয়।
  • Fake it আর triangulation-এর পার্থক্য? — Fake it প্রত্যাশিত constant ফেরত দিয়ে দ্রুত সবুজ; triangulation দ্বিতীয় ভিন্ন উদাহরণ দিয়ে সাধারণ নিয়মে বাধ্য করে।
  • TDD আর BDD-র পার্থক্য? — TDD developer-এর কোড-স্তরের চক্র; BDD ব্যবসা, developer আর tester-এর একমত হওয়া আচরণ, Given-When-Then-এ লেখা, step definition দিয়ে স্বয়ংক্রিয়, ভেতরে TDD.
  • TDD কোথায় ব্যবহার করবেন না? — Spike বা অনুসন্ধানে, UI-র চেহারা খোঁজায়, ফেলে দেওয়ার মতো prototype-এ; শেখা হলে আসল কোড TDD-তে নতুন করে।

৯ · AI-এর যুগে TDD আর BDD

AI সহকারী কয়েক সেকেন্ডে test আর কোড দুটিই লিখে দেয়। এতে TDD-র মূল্য বাড়ে: AI-এর কোড যাচাইয়ের সবচেয়ে ভরসার উপায় আপনার লেখা, আগে লাল দেখা test. আর AI দিয়ে কোড থেকে test বানালে TDD-র উদ্দেশ্যই উল্টে যায়।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
বিদ্যমান কোড দেখে test লেখে — implementation-এর প্রতিচ্ছবি, bug-সহ “প্রত্যাশা” বেঁধে দেয়প্রত্যাশিত মান requirement থেকে নিজে হিসাব করুন; কোড ইচ্ছা করে ভেঙে দেখুন test লাল হয় কি না (mutation testing, পর্ব ৫০)
এমন test, যা কিছুই যাচাই করে না — assertion নেই, বা assert True, বা শুধু “exception ছোড়ে না”প্রতিটি test-এ অন্তত একটি অর্থপূর্ণ assertion; আগে লাল দেখা
একসাথে বিশ test আর পুরো কোড — চক্র নেই, refactor নেইএকবারে একটি test চান; প্রতিটি চক্র আলাদা commit
Gherkin-এ UI-ক্লিকের script, বা ব্যবসার নিয়ম step definition-এর ভেতরেScenario-তে ব্যবসার ভাষা, একটি When; step পাতলা (অংশ ৭)
ভুল তথ্য: “TDD ২০০৮-এ আবিষ্কার”, অস্তিত্বহীন tool-এর flag বা decorator, ভুল সংখ্যা “TDD ৫০% সময় বাঁচায়”মূল বই আর tool-এর official documentation; সংখ্যার উৎস চান, না পেলে বাদ
“পরিষ্কার” করতে git reset --hard বা git push --force, সতর্কতা ছাড়া; বা হুবহু কপি করা অজানা licence-এর kata-সমাধানThrowaway clone-এ আগে চালান, git help reset পড়ুন (পর্ব ১১, ১৫); লম্বা কোডের উৎস আর licence দেখুন

AI-কে TDD-র জুটি (pair) হিসেবে ব্যবহার করুন — আপনি চক্র চালান, AI পরামর্শ দেয়। কোড বা requirement দেওয়ার আগে গোপন key, password, গ্রাহকের তথ্য আর অভ্যন্তরীণ URL মুছে নিন:

আমি TDD-তে একটি কাল্পনিক বাস ভাড়ার function লিখছি (Python, unittest). নিয়মগুলো নিচে দিলাম, সাথে বর্তমান কোড আর test (গোপন তথ্য নেই)। ১. পুরো সমাধান লিখবে না। শুধু test list-এ পরের একটি item প্রস্তাব করো, আর কেন সেটি এখন সবচেয়ে ছোট ধাপ তা এক লাইনে বলো। ২. সেই item-এর জন্য একটিমাত্র unittest method লিখে দাও। প্রত্যাশিত মান নিয়ম থেকে হাতে হিসাব করে দেখাও; বর্তমান কোড দেখে প্রত্যাশা ঠিক করবে না। ৩. বর্তমান কোডে test-টি চালালে runner কী লাল বার্তা দেবে বলে মনে করো, তা লেখো — আমি নিজে চালিয়ে মিলিয়ে নেব। ৪. সবুজ করার ন্যূনতম বদল দাও, আর বলো কৌশলটি fake it, triangulation না obvious implementation. ৫. সবুজের পরে কোনো refactor দরকার কি না বলো; থাকলে আচরণ না বদলে এমন ছোট ধাপে দাও। ৬. Git command দিলে প্রতিটির ঝুঁকি বলো; reset --hard বা push --force দেবে না। কোনো tool-এর flag বা decorator নিয়ে নিশ্চিত না হলে "নিশ্চিত নই" লিখবে।

AI-এর test বা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Kent Beck নিজেই বলেছেন তিনি test-first “পুনরাবিষ্কার” করেছেন। ধারণাটি পুরোনো হলে TDD ১৯৯০-এর দশকের শেষ আর ২০০০-এর দশকে এসে কেন ছড়াল?

আগে প্রত্যাশিত ফল লিখে রেখে পরে মেলানো — এই ভাবনা punch card আর magnetic tape-এর যুগেও ছিল। কিন্তু তখন একবার program চালানোই ছিল দামি: কম্পিউটারের সময় ভাগ করে পাওয়া যেত, ফল আসত ঘণ্টা বা দিন পরে। প্রতি কয়েক মিনিটে test চালানোর ছন্দ সেই পরিবেশে অসম্ভব। TDD-র মূল শর্ত হলো feedback সস্তা আর তাৎক্ষণিক হওয়া, আর সেই শর্ত পূরণ হয় ১৯৯০-এর দশকে ব্যক্তিগত কম্পিউটার, দ্রুত compiler আর interpreter আসার পরে।

দ্বিতীয় উপাদান ছিল tool. Beck ১৯৯০-এর দশকের শুরুতে Smalltalk-এর জন্য SUnit লেখেন; পরে Erich Gamma-র সাথে Java-র জন্য JUnit — যা নিয়ে প্রচলিত গল্প হলো, দুজন একটি বিমানযাত্রায় এর প্রথম রূপ লিখেছিলেন। JUnit-এর নকশা প্রায় প্রতিটি ভাষায় কপি হলো — Python-এর unittest-ও সেই xUnit পরিবারের। Test চালানো আর লাল-সবুজ দেখা এক ক্লিকের কাজ হলো, আর IDE-র automated refactoring চক্রের তৃতীয় ধাপও সস্তা করল।

তৃতীয় উপাদান ছিল প্রক্রিয়ার বদল। Waterfall-এর যুগে নকশা আগে শেষ হতো, তাই “test দিয়ে নকশা চালানো” কথাটিই অর্থহীন শোনাত। Extreme Programming আর পরে Agile Manifesto (২০০১) ছোট ধাপ, ঘন release আর বদলকে স্বাগত জানানোর কথা বলল — আর ঘন ঘন বদলাতে হলে ভরসার regression test চাই। TDD সেই চাহিদার স্বাভাবিক উত্তর। Beck-এর বই ধাপগুলোর নাম দিল — fake it, triangulate, test list — আর নাম থাকলে শেখানো যায়।

মূল উপলব্ধি: একটি অনুশীলন ছড়ায় তখনই, যখন তার খরচ কমে আর প্রক্রিয়া তাকে চায় — দ্রুত কম্পিউটার, xUnit, refactoring tool আর Agile মিলে test-first-কে পুরোনো ভাবনা থেকে প্রতিদিনের অভ্যাসে পরিণত করে।

প্র ০২২০১৪ সালে একজন পরিচিত framework-নির্মাতা লিখলেন “TDD is dead. Long live testing.” — আর Kent Beck, Martin Fowler ও তিনি প্রকাশ্যে আলোচনায় বসলেন। এই বিতর্ক থেকে কী শেখার আছে?

লেখাটি David Heinemeier Hansson-এর (Ruby on Rails-এর নির্মাতা)। তাঁর অভিযোগ মূলত তিনটি। প্রথমত, TDD অনেক জায়গায় ধর্মীয় নিয়মের মতো চাপানো হচ্ছে — “test আগে না লিখলে আপনি পেশাদার নন।” দ্বিতীয়ত, শুধু দ্রুত unit test-এর জন্য কোডকে টুকরো টুকরো করা, প্রতিটি জিনিসের পেছনে interface আর mock বসানো, নকশাকে খারাপ করে — তাঁর ভাষায় “test-induced design damage”. তৃতীয়ত, web application-এর অনেক গুরুত্বপূর্ণ আচরণ database আর framework-এর সাথে মিলে ঘটে; সেগুলো পুরোপুরি আলাদা করে test করার চেষ্টা অনেক সময় বৃথা।

এর পরে “Is TDD Dead?” নামে কয়েক পর্বের প্রকাশ্য আলোচনা হয়। Beck-এর অবস্থান ছিল লক্ষণীয়ভাবে নরম: TDD তাঁর কাছে একটি কৌশল, যা আত্মবিশ্বাস বাড়ায়; যেখানে কাজ করে না, সেখানে না করাই ঠিক। তিনি নিজেও বলেন mock-এর অতিরিক্ত ব্যবহার তাঁর পছন্দ নয়। Fowler জোর দেন self-testing code-এর ওপর — কোড আর test একসাথে থাকা, যেকোনো ক্রমে লেখা হোক।

শিক্ষাটি পদ্ধতি নিয়ে নয়, পদ্ধতির সাথে সম্পর্ক নিয়ে। TDD-র দাবি “এভাবে কাজ করলে কোড ভালো হয়” — একটি অভিজ্ঞতাভিত্তিক দাবি, যা প্রসঙ্গ ভেদে সত্য বা মিথ্যা। নিয়মটিকে লক্ষ্য বানালে মানুষ শুধু নিয়ম রক্ষা করে: সবকিছু mock, test-এর সংখ্যা বাড়ে, আস্থা বাড়ে না। আবার “TDD মৃত” শুনে test লেখাই বাদ দেওয়াও ভুল শিক্ষা। বিতর্কের শেষে প্রায় সবাই একটি জায়গায় একমত: কোড এমনভাবে লিখুন যাতে তার আচরণ দ্রুত, স্বয়ংক্রিয়ভাবে যাচাই করা যায়; TDD সেখানে পৌঁছানোর একটি শক্তিশালী, কিন্তু একমাত্র নয়, পথ।

মূল উপলব্ধি: TDD একটি হাতিয়ার, পরিচয় নয় — কোথায় দ্রুত feedback আর ভালো নকশা দেয় সেখানে ব্যবহার করুন, কোথায় নকশাকে বিকৃত করছে সেটিও খেয়াল রাখুন।

প্র ০৩একটি শিল্প-গবেষণা TDD-তে অনেক কম defect দেখায়, অথচ কয়েকটি নিয়ন্ত্রিত পরীক্ষায় তেমন পার্থক্য মেলে না। দুটি একসাথে সত্য হয় কীভাবে, আর এমন প্রমাণ কীভাবে পড়বেন?

Nagappan ও সহকর্মীদের ২০০৮ সালের case study চারটি আসল দলকে দেখেছিল — একটি IBM-এ, তিনটি Microsoft-এ। লেখকদের রিপোর্টে TDD দলগুলোর release-পূর্ব defect density তুলনীয় project-এর চেয়ে অনেক কম, আর শুরুর সময় কিছুটা বেশি। এটি বাস্তব কাজের প্রমাণ — বড় codebase, পেশাদার developer, সত্যিকারের চাপ। কিন্তু case study-র দুর্বলতাও আছে: দল নিজে TDD বেছেছে, তাই হয়তো তারা আগে থেকেই বেশি যত্নশীল ছিল (selection bias); তুলনার project হুবহু এক নয়; আর যে দল TDD করছে বলে জানে, তারা অন্য দিকেও বেশি মনোযোগী হতে পারে।

নিয়ন্ত্রিত পরীক্ষায় অংশগ্রহণকারীদের এলোমেলোভাবে TDD আর test-last দলে ভাগ করা হয়, যা selection bias কমায়। কিন্তু সেখানে অন্য সমস্যা: অংশগ্রহণকারী প্রায়ই শিক্ষার্থী বা TDD-তে নতুন; কাজ কয়েক ঘণ্টার ছোট kata; আর TDD-র দাবি করা লাভ — নকশা, দীর্ঘমেয়াদি রক্ষণাবেক্ষণ — কয়েক ঘণ্টায় দেখা যায় না। ফলে দুটি ধরনের গবেষণা আসলে ভিন্ন প্রশ্নের উত্তর দেয়: “বাস্তব দলে কী ঘটেছে” আর “নতুন শিক্ষার্থীর ছোট কাজে test-first ক্রম কী বদলায়”।

Fucci ও সহকর্মীদের একটি বিশ্লেষণ আরও সূক্ষ্ম ইঙ্গিত দেয়: যা সবচেয়ে বেশি মেলে তা হয়তো ধাপের মাপ আর নিয়মিততা — ছোট, সমান ধাপে কাজ — test আগে না পরে, তার চেয়ে বেশি। এটি TDD-কে বাতিল করে না; বরং বলে TDD-র যে অংশটি সবচেয়ে মূল্যবান, তা হলো baby steps আর অবিরাম feedback. প্রমাণ পড়ার সময় তাই কয়েকটি প্রশ্ন করুন: কারা অংশ নিয়েছেন, কাজ কত বড়, কী মাপা হয়েছে (defect, সময়, coverage), তুলনা কার সাথে, আর লেখকেরা নিজেরা কোন সীমাবদ্ধতার কথা বলেছেন।

মূল উপলব্ধি: প্রমাণের ধরন প্রশ্নের ধরন ঠিক করে — case study বাস্তবতা দেখায় কিন্তু কারণ প্রমাণ করে না, পরীক্ষা কারণ যাচাই করে কিন্তু বাস্তবতা থেকে দূরে; তাই “TDD defect এত% কমায়” ধরনের একক সংখ্যায় আস্থা রাখবেন না।

প্র ০৪London school-এর outside-in TDD mock দিয়ে collaborator-এর interface আবিষ্কার করে; classic school আসল object দিয়ে ভেতর থেকে গড়ে। একটি নতুন feature-এ কোনটি বেছে নেবেন, আর কেন?

দুই ধারার শিকড় আলাদা অভিজ্ঞতায়। Beck-এর classic ধারা বড় হয়েছে domain-কেন্দ্রিক কাজে — টাকা, মুদ্রা, হিসাব — যেখানে object-গুলো মূলত মান নিয়ে কাজ করে, আর ফল যাচাই করাই স্বাভাবিক। London-এর একটি দল (Mackinnon, Freeman, Craig ও আরও অনেকে) ২০০০ সালের দিকে দেখলেন, অনেক object-এর কাজ নিজে কিছু ফেরত দেওয়া নয়, অন্যকে বার্তা পাঠানো — “order হলে warehouse-কে জানানো, গ্রাহককে SMS পাঠানো”। এমন object-এর আচরণ যাচাই করতে হলে দেখতে হয় সে কাকে কী বলল। Mock object সেই প্রশ্নের উত্তর, আর ২০০৯ সালের GOOS বই পুরো পদ্ধতি সাজায়।

Outside-in-এর বড় লাভ নকশা আবিষ্কারে। আপনি একটি acceptance test দিয়ে শুরু করেন, তারপর বাইরের object-এর test লিখতে গিয়ে প্রশ্ন করেন: “এই কাজ করতে আমার কোন সহযোগী দরকার, তাকে কী জিজ্ঞেস করব?” উত্তর থেকে interface জন্মায় — যা ঠিক ততটুকু, যতটুকু caller-এর দরকার (পর্ব ২৮-এর interface segregation). ঝুঁকি হলো test implementation-এর কথোপকথনে বাঁধা পড়ে: ভেতরের কাজ নতুনভাবে ভাগ করলেই mock-এর প্রত্যাশা ভাঙে, যদিও বাইরের আচরণ একই। Classic ধারায় test টেকসই, কারণ শুধু ফল দেখে; কিন্তু টুকরোগুলো শেষে জোড়া না-ও লাগতে পারে, আর ভেতরে এমন কোড তৈরি হতে পারে যা বাইরের কেউ ডাকে না।

বাস্তব সিদ্ধান্ত তাই feature-এর আকার আর ধরনের ওপর নির্ভর করে। হিসাব-প্রধান অংশে (ভাড়া, কর, ছাড়) classic ধারা স্বাভাবিক — collaborator নেই, শুধু মান। Workflow-প্রধান অংশে (অর্ডার নেওয়া, notification, payment gateway-কে ডাকা) outside-in বেশি কাজের, বিশেষ করে বাইরের সীমানায়। অনেক অভিজ্ঞ developer তাই মেশান: বাইরের দিক থেকে কাজ শুরু করে mock দিয়ে সীমানা ঠিক করেন, তারপর ভেতরের domain-এ আসল object দিয়ে classic TDD. Mock-এর নিয়মগুলো পর্ব ৪৯-এ।

মূল উপলব্ধি: প্রশ্নটি “কোন school ঠিক” নয়, “এই object-এর কাজ মান ফেরত দেওয়া না বার্তা পাঠানো” — প্রথমটিতে ফল যাচাই, দ্বিতীয়টিতে কথোপকথন যাচাই, আর দুটির সীমানা ঠিক রাখাই নকশার কাজ।

প্র ০৫অনেক দলে BDD মানে দাঁড়িয়েছে “tester একা বসে শত শত Gherkin scenario লেখেন, যা কোনো ব্যবসার মানুষ পড়েন না।” Dan North-এর মূল ভাবনা থেকে এটি কোথায় সরে গেল?

Dan North-এর ২০০৬ সালের লেখার কেন্দ্রে ছিল ভাষা আর যোগাযোগ, tool নয়। তিনি দেখেছিলেন “test” শব্দটিই মানুষকে ভুল দিকে ঠেলে: test মানে কাজ শেষের যাচাই, অথচ TDD-তে তা কাজ শুরুর নকশা। শব্দ বদলে “behaviour” আর “should” বললে প্রশ্ন বদলায় — “system-এর কী করা উচিত?” — যা ব্যবসার মানুষও বোঝেন। Given-When-Then সেই আলাপকে একটি ছাঁচ দিল, আর Cucumber-এর মতো tool সেই ছাঁচকে চালানো যায় এমন বানাল। চালানো যাওয়াটা বোনাস ছিল; মূল লক্ষ্য ছিল ব্যবসা, developer আর tester-এর এক বোঝাপড়া।

সমস্যা শুরু হয় যখন বোনাসটিই লক্ষ্য হয়ে যায়। Tool হাতে পেয়ে দল ভাবে Gherkin একটি test automation-এর ভাষা মাত্র। Tester একা scenario লেখেন, প্রায়ই UI-র ধাপে — “When I click the blue button, And I type 100” — কারণ তিনি screen দেখে test করেন। ব্যবসার মানুষ এগুলো পড়েন না, কারণ এগুলো তাঁদের ভাষায় নয়। Screen বদলালেই শ-খানেক scenario ভাঙে, আর দল বলে “BDD ধীর আর ভঙ্গুর”। আসলে এটি BDD নয়, Gherkin-এ লেখা UI script — পর্ব ২২-এ যে অপব্যবহারের কথা বলা হয়েছিল।

প্রতিকার মূল ভাবনায় ফেরা। প্রথমে আলাপ: three amigos মিলে example mapping — story, নিয়ম, উদাহরণ, প্রশ্ন। যে উদাহরণ নিয়ে মতভেদ ছিল বা যা ব্যবসার কাছে গুরুত্বপূর্ণ, শুধু সেগুলোই scenario হয়; বাকি খুঁটিনাটি নিচের স্তরের unit test-এ (পর্ব ৫১-এর pyramid). ভাষা declarative: “When I top up 100 taka”, বোতামের নাম নয়। Step definition পাতলা, আর সম্ভব হলে UI-র নিচের স্তর ডাকে। আর scenario নিয়মিত ব্যবসার মানুষ পড়েন — sprint review-তে বা story গ্রহণের সময়। তখনই এটি সত্যিকারের living documentation.

মূল উপলব্ধি: BDD-র মূল্য তৈরি হয় আলাপে, automation-এ নয় — যে scenario কোনো ব্যবসার মানুষ পড়েন না, সেটি শুধু একটি ধীর, ভঙ্গুর test, specification নয়।

অনুশীলন

পরের TDD চক্র, ব্যর্থ ধাপের অনুমান, একটি ভুয়া-TDD-র সমালোচনা আর সময়ের হিসাব।

  1. পরের TDD চক্রটি করুন: কাল্পনিক বাস কোম্পানি নতুন নিয়ম দিল — “যেকোনো যাত্রায় সর্বোচ্চ ভাড়া ৬০ টাকা।” বর্তমান fare হলো অংশ ৪-এর শেষ সংস্করণ (BASE_FARE, BASE_KM, PER_KM = 10, 3, 3; বাড়তি কিমি math.ceil-এ; ছাত্রভাড়া ceil(full / 2)).
    1. প্রথম test কোনটি লিখবেন? fare(25)-এর জন্য একটি unittest method লিখুন।
    2. বর্তমান কোডে চালালে কোন লাল বার্তা আসবে? হিসাব দেখান।
    3. সবুজ করার ন্যূনতম বদল আর refactor কী?
    4. সীমার কোন দুটি test যোগ করবেন, আর তাদের প্রত্যাশিত মান কত?
    5. কোন প্রশ্নটি code লেখার আগে ব্যবসার মানুষকে জিজ্ঞেস করবেন? commit বার্তা কী হবে?
    1. নিয়মটি স্পষ্টভাবে ভাঙে এমন একটি দূরের মান দিয়ে শুরু: def test_long_trip_is_capped_at_60(self): self.assertEqual(fare(25), 60).
    2. ২৫ কিমিতে বাড়তি ২২ কিমি × ৩ = ৬৬, যোগ ১০ = ৭৬. তাই বার্তা AssertionError: 76 != 60 (লেখার সময় চালিয়ে মেলানো)। এটিই ঠিক কারণে লাল।
    3. Obvious implementation: MAX_FARE = 60 নামের constant, আর full = min(BASE_FARE + extra_km * PER_KM, MAX_FARE). সব test সবুজ। Refactor-এ constant-গুলো এক জায়গায় রাখা যথেষ্ট; নতুন if লাগে না।
    4. সীমা পেরোনোর ঠিক আগে-পরে: ১৯ কিমি → ১০ + ১৬ × ৩ = ৫৮ (cap খাটে না); ২০ কিমি → ১০ + ১৭ × ৩ = ৬১ → ৬০ (cap খাটে)। ১৯.৫ কিমিও ৬১ → ৬০, কারণ শুরু করা কিমি পুরো।
    5. প্রশ্ন: ছাত্রভাড়া কি cap-এর পরে অর্ধেক (২৫ কিমিতে ৬০ → ৩০), নাকি অর্ধেক করার পরে cap? এই কোডে min আগে, তাই ৩০ — কিন্তু এটি ব্যবসার সিদ্ধান্ত, three amigos-এর প্রশ্ন; উত্তর একটি test হয়ে থাকবে। Commit: feat: cap any fare at 60 taka; সীমার test আলাদা হলে test: pin cap boundary at 19 and 20 km.
  2. আউটপুট অনুমান করুন — কোন ধাপ ফেল করবে? অংশ ৭-এর ছোট BDD runner-এ এখন topUp ঠিক করা হয়েছে (আগে যাচাই, পরে কাটা), আর step definition চারটি একই: /^my wallet balance is (\d+) taka$/, /^I top up (\d+) taka$/, /^I should see "(.*)"$/, /^my balance should be (\d+) taka$/. নিচের feature চালালে প্রতিটি ধাপে PASS, FAIL না SKIP — আর শেষ লাইনের হিসাব কী?
    Scenario: Exact balance
      Given my wallet balance is 100 taka
      When I top up 100 taka
      Then I should see "Top-up successful"
      And my balance should be 0 taka
    Scenario: Loyalty points
      Given my wallet balance is 300 taka
      When I top up 50 taka
      Then my balance should be 250 taka
      And I should get 0 loyalty points
    Scenario: Paisa amount
      Given my wallet balance is 20 taka
      When I top up 12.5 taka
      Then my balance should be 7.5 taka

    আসল আউটপুট (Node.js-এ চালিয়ে মেলানো):

    Scenario: Exact balance
      PASS Given my wallet balance is 100 taka
      PASS When I top up 100 taka
      PASS Then I should see "Top-up successful"
      PASS And my balance should be 0 taka
    Scenario: Loyalty points
      PASS Given my wallet balance is 300 taka
      PASS When I top up 50 taka
      PASS Then my balance should be 250 taka
      FAIL And I should get 0 loyalty points  <- no matching step definition
    Scenario: Paisa amount
      PASS Given my wallet balance is 20 taka
      FAIL When I top up 12.5 taka  <- no matching step definition
      SKIP Then my balance should be 7.5 taka
    steps: 8 passed, 2 failed, 1 skipped
    • Exact balance: ১০০ > ১০০ মিথ্যা, তাই রিচার্জ হয়, ব্যালান্স ০ — চারটিই PASS. সীমার মানে (> বনাম >=) এই scenario-ই ভুল ধরত।
    • Loyalty points: প্রথম তিনটি PASS. শেষ ধাপের লেখা কোনো ছাঁচে মেলে না — FAIL, কারণ “no matching step definition”. আসল tool এটিকে সাধারণত “undefined” বলে।
    • Paisa amount: Given PASS. When-এর (\d+) শুধু পূর্ণসংখ্যা ধরে; “12.5”-এর পরে “.5 taka” থাকায় $ পর্যন্ত মেলে না — FAIL, আর পরের Then SKIP. পয়সার নিয়ম চাইলে আগে three amigos-এ সিদ্ধান্ত, তারপর ছাঁচ আর কোড।
    • হিসাব: ৪ + ৩ + ১ = ৮ পাস, ২ ফেল, ১ skipped — মোট ১১টি ধাপ।
  3. একটি “TDD”-র সমালোচনা করুন: একটি কাল্পনিক অনলাইন বাজার অ্যাপের developer stand-up-এ বললেন, “ছাড়ের module TDD দিয়ে করেছি।” তাঁর আসল কাজের ধারা:
    1. দুই দিনে পুরো DiscountService class লিখলেন — ৪০০ লাইন।
    2. AI-কে class-টি দিয়ে বললেন “এর unit test লেখো”; ৩৫টি test এল, প্রথমবারেই সব সবুজ, line coverage ১০০%.
    3. Test-গুলো প্রতিটি private method আলাদা ডাকে, আর mock দিয়ে যাচাই করে ভেতরের helper কোন ক্রমে ডাকা হলো।
    4. একটি commit: “add discount module with tests (TDD)”.
    5. পরের সপ্তাহে একটি helper-এর নাম বদলাতেই ২২টি test ভাঙল; তিনি test মুছে দিলেন।
    কী কী সমস্যা, আর আসল TDD-তে ধারাটি কেমন হতো?
    • এটি test-after, TDD নয়: কোড আগে, test পরে; কোনো test কখনো লাল হয়নি। ফলে প্রমাণ নেই যে test-গুলো ভুল ধরতে পারে, আর নকশায় test-এর কোনো প্রভাব পড়েনি।
    • Implementation-এর প্রতিচ্ছবি: কোড থেকে বানানো test কোডের বর্তমান আচরণ — bug-সহ — “প্রত্যাশা” বলে বেঁধে দেয়। ১০০% coverage মানে শুধু প্রতিটি লাইন চলেছে, assertion অর্থপূর্ণ কি না নয় (পর্ব ৫০)।
    • ভেতরের কথোপকথনে বাঁধা: private method আর ডাকের ক্রম যাচাই করায় rename-এর মতো নিরীহ refactoring-এ ২২টি test ভাঙল — refactor ধাপ অসম্ভব হয়ে গেল, আর শেষে নিরাপত্তার জালটিই মুছে গেল।
    • এক বিশাল commit: review, revert বা bisect কোনোটিই সহজ নয়; বার্তায় “(TDD)” লেখা প্রক্রিয়ার ভুল বর্ণনা।
    • আসল ধারা: requirement থেকে test list (যেমন “৫০০ টাকার নিচে ছাড় নেই”, “ঈদ কুপনে ১০%”, “দুটি কুপন একসাথে নয়”); তারপর public method-এর একটি test, লাল দেখা, ন্যূনতম কোড, refactor, commit — প্রতিটি নিয়মে আলাদা চক্র। প্রত্যাশিত মান নিয়ম থেকে হাতে হিসাব। Mock শুধু বাইরের সীমানায় (যেমন কুপনের database). AI প্রস্তাব দিক, লাল দেখা আর প্রত্যাশা যাচাই আপনার।
  4. সময়ের হিসাব: একজন শিক্ষার্থীর kata-সেশনের লগ (কাল্পনিক): 00:00 red · 02:00 green · 02:30 refactor · 04:00 red · 16:00 green · 17:00 red · 18:00 green · 19:30 refactor · 21:00 end. অংশ ৮-এর নিয়মে (চক্র red থেকে পরের red বা end পর্যন্ত; ১০ মিনিটের বেশি giant step) হিসাব করুন: কয়টি চক্র, প্রতিটির red/green/refactor সময়, মোট সময়ের কত শতাংশ লাল, গড় চক্র, আর কোন চক্রে কী পরামর্শ দেবেন।

    অংশ ৮-এর মডেলে এই লগ বসিয়ে চালালে (মিলিয়ে দেখা):

    cycle 1 at 00:00: red 02:00 green 00:30 refactor 01:30 = 04:00
    cycle 2 at 04:00: red 12:00 green 01:00 refactor 00:00 = 13:00  GIANT STEP, no refactor
    cycle 3 at 17:00: red 01:00 green 01:30 refactor 01:30 = 04:00
    red      15:00 (71%)
    green    03:00 (14%)
    refactor 03:00 (14%)
    3 cycles in 21:00, mean 07:00 per cycle
    • চক্র: ১ — red ২ + green ½ + refactor ১½ = ৪ মিনিট; ২ — red ০৪:০০–১৬:০০ = ১২ + green ১ = ১৩ মিনিট, refactor নেই, giant step; ৩ — red ১ + green ১½ + refactor ১½ = ৪ মিনিট।
    • মোট: লাল ২ + ১২ + ১ = ১৫ মিনিট, ২১ মিনিটের ৯০০ ÷ ১২৬০ ≈ ৭১%; সবুজ ৩ মিনিট ≈ ১৪%; refactor ৩ মিনিট ≈ ১৪% (গোল করায় যোগফল ৯৯)। গড় ২১ ÷ ৩ = ৭ মিনিট।
    • পরামর্শ: চক্র ২-এর test সম্ভবত একসাথে কয়েকটি নিয়ম চেয়েছিল — test list-এর item ভেঙে দুই-তিনটি ছোট চক্র করা; ১০ মিনিট লাল থাকলে বদল ফেলে শেষ সবুজে ফিরে ছোট ধাপে আবার শুরু। সবুজের পরে refactor-এর এক মিনিটও বাদ না দেওয়া।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি TDD চক্রের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 Test list থেকে পরের সবচেয়ে ছোট item বাছা
  2. 🔴 সেই item-এর একটি test লিখে চালানো — ঠিক কারণে লাল
  3. 🟢 পাস করানোর ন্যূনতম কোড লিখে সব test সবুজ করা
  4. 🧹 Test সবুজ রেখে duplication আর নাম ঠিক করা
  5. 📌 Commit করে item-টি তালিকা থেকে কেটে দেওয়া

একটি test আছে: assertEqual(points(500), 5). Developer সবুজ করতে লিখলেন def points(amount): return 5. TDD-র ভাষায় কৌশলটির নাম আর পরের যুক্তিসংগত ধাপ কোনটি?

  • Fake it; এরপর ভিন্ন একটি উদাহরণের test (যেমন 1250 → 12) দিয়ে triangulation
  • Obvious implementation; কাজ শেষ, commit করে release
  • Refactoring; test মুছে দিয়ে আসল কোড লেখা
  • এটি TDD-র নিয়মভঙ্গ; fake কখনো লেখা যায় না

TDD করার সময় একটি নতুন test লিখে প্রথমবার চালাতেই সেটি সবুজ হলো, production কোডে কিছু না বদলেই। সবচেয়ে ভালো প্রতিক্রিয়া কোনটি?

  • Test-টি অর্থহীন, সঙ্গে সঙ্গে মুছে দেওয়া
  • কেন সবুজ বোঝা, কোড ইচ্ছা করে ভেঙে দেখা test ফেল করতে পারে কি না; ঠিক থাকলে দলিল হিসেবে রাখা
  • প্রত্যাশিত মান বদলে test-টিকে লাল বানানো
  • Test সবুজ, তাই এবার পরের কয়েকটি feature একসাথে লিখে ফেলা

Outside-in (London school) আর inside-out (classic school) TDD সম্পর্কে কোন বক্তব্যটি সঠিক?

  • Classic ধারা acceptance test থেকে শুরু করে আর সব collaborator mock করে
  • Outside-in-এ refactor ধাপ থাকে না
  • Outside-in শুধু ফেরত মান যাচাই করে, mock ব্যবহার করে না
  • Outside-in বাইরের acceptance test থেকে শুরু করে না লেখা collaborator mock দিয়ে বসায়; classic ভেতর থেকে আসল object দিয়ে গড়ে

একটি BDD tool-এ top-up ধাপের step definition একটিই, ছাঁচ /^I top up (\d+) taka$/. একটি scenario-র তিনটি ধাপ: Given my wallet balance is 20 taka (যার step definition আছে ও পাস করে), When I top up 12.5 taka, Then my balance should be 7.5 taka. কী ঘটবে?

  • When ধাপ 12 টাকা ধরে চলবে, Then ধাপ ফেল করবে
  • When ধাপ 12.5 ধরে চলবে, সব পাস
  • When ধাপের কোনো মেলানো step definition নেই, তাই সেটি undefined বা ব্যর্থ, আর Then ধাপ skipped
  • পুরো feature file parse হবে না, কোনো scenario চলবে না

Nagappan, Maximilien, Bhat ও Williams-এর ২০০৮ সালের TDD-বিষয়ক গবেষণা সম্পর্কে কোন বর্ণনাটি সবচেয়ে সঠিক?

  • শিক্ষার্থীদের এলোমেলো দলে ভাগ করা একটি নিয়ন্ত্রিত পরীক্ষা, যা প্রমাণ করে TDD সবসময় দ্রুততর
  • IBM ও Microsoft-এর চারটি শিল্প-দলের case study, যেখানে কম defect কিন্তু বেশি প্রাথমিক সময়ের কথা এসেছে — randomized নয়, তাই সাধারণীকরণে সাবধানতা দরকার
  • চারটি দলে TDD আর test-after-এর মধ্যে কোনো পার্থক্য পাওয়া যায়নি
  • Gherkin scenario লেখার সময় মাপার একটি পরীক্ষা

BDD-তে “three amigos” আলোচনার মূল উদ্দেশ্য কোনটি?

  • কাজ শুরুর আগে ব্যবসার প্রতিনিধি, developer আর tester মিলে নিয়ম আর উদাহরণে একমত হওয়া
  • কোড লেখা শেষে তিনজন reviewer-এর approval নেওয়া
  • Cucumber, behave না pytest-bdd — কোন tool নেবে তা ঠিক করা
  • Tester-কে একা সব Gherkin scenario লেখার দায়িত্ব দেওয়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. TDD = red → green → refactor, একবারে একটি ছোট test. Beck-এর দুই নিয়ম (২০০২/২০০৩): ব্যর্থ স্বয়ংক্রিয় test ছাড়া নতুন কোড নয়; duplication দূর করুন। তিন সূত্রের কড়া রূপ Robert C. Martin-এর।
  2. লাল আগে দেখুন, ঠিক কারণে: যে test কখনো লাল হয়নি তাকে বিশ্বাস করার কারণ নেই। প্রথমবারেই সবুজ হলে কোড ভেঙে যাচাই করুন। Test list হাতে রাখুন; অনিশ্চিত হলে ধাপ ছোট।
  3. সবুজের তিন পথ: fake it (constant), triangulation (দ্বিতীয় উদাহরণে সাধারণ নিয়ম), obvious implementation (জানা থাকলে সরাসরি)। Refactor বাদ দেবেন না — সেটিই green-এর পাপ শোধ।
  4. TDD নকশায় চাপ দেয়: ঘড়ি, network, SMS বাইরে থেকে দিন — testability আর কম coupling একসাথে। Inside-out (classic) ফল যাচাই করে; outside-in (London) mock দিয়ে কথোপকথন আর interface আবিষ্কার করে।
  5. বাইরের চক্র ATDD/BDD: three amigos-এর আলাপ → Given-When-Then scenario → step definition → ভেতরে TDD. প্রমাণ মিশ্র; spike আর UI অনুসন্ধানে আগে শেখা। সাধারণ ভুল: implementation test, বিশাল ধাপ, refactor বাদ।

পরের পর্বে (পর্ব ৪৯): এই পর্বে যে নকল notifier আর mock-এর কথা এসেছে, তাদের পুরো পরিবার — dummy, stub, fake, spy আর mock; কোনটি কখন, আর কখন mock একেবারেই নয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান