পাঠ ৫৩ · ৫৭-এর মধ্যে · মডিউল ১২

বিহেভিয়ার-ড্রিভেন ডেভেলপমেন্ট (BDD)

Behavior-driven development
৮ মিনিট পড়া উচ্চ-মধ্যবর্তী · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Given-When-Then কাঠামো কী এবং কেন এটি ব্যবসায়িক ভাষায় লেখা হয়
  • BDD, TDD (M12/L52)-এর সাথে কীভাবে সম্পর্কিত অথচ ভিন্ন — কার জন্য লেখা হয় সেই দৃষ্টিকোণ থেকে
  • Cucumber/Gherkin বাস্তব ইন্ডাস্ট্রিতে কী ভূমিকা রাখে, এবং এই কোর্স কেন তা সরাসরি চালায় না
  • একটি সরল, stdlib-only Given-When-Then কাঠামো বানিয়ে একটি বাস্তব সিনারিও যাচাই করা

১ · Given-When-Then — ব্যবসায়িক ভাষায় আচরণ

বিহেভিয়ার-ড্রিভেন ডেভেলপমেন্টBehavior-Driven Development (BDD)একটি পদ্ধতি যেখানে সফটওয়্যারের প্রত্যাশিত আচরণ কোড লেখার আগেই ব্যবসায়িক, প্রায়-স্বাভাবিক ভাষায় Given-When-Then কাঠামোয় লিখে ফেলা হয়, যাতে অ-টেকনিক্যাল স্টেকহোল্ডারও তা পড়ে বুঝতে ও একমত হতে পারেন। প্রতিটি সিনারিও তিনটি অংশে ভাগ করা হয়:

Given
প্রাথমিক অবস্থা/প্রেক্ষাপট — সিনারিও শুরু হওয়ার আগে কী সত্যি বলে ধরে নেওয়া হচ্ছে।
When
একটি নির্দিষ্ট কাজ/ঘটনা — ব্যবহারকারী বা সিস্টেম কী করে।
Then
প্রত্যাশিত ফলাফল — সেই কাজের পর সিস্টেমের অবস্থা কেমন হওয়া উচিত।

উদাহরণ, প্রায়-স্বাভাবিক ভাষায় (এখনো কোনো নির্দিষ্ট টুলের সিনট্যাক্স নয়): "Given একজন ব্যবহারকারীর কার্টে ১০০০ টাকার পণ্য আছে এবং তিনি একটি বৈধ কুপন প্রয়োগ করেছেন, When তিনি চেকআউট সম্পন্ন করেন, Then ফাইনাল টোটাল কুপনের ছাড় বিয়োগ করে হিসাব হওয়া উচিত।" এই বাক্যটি একজন ডেভেলপার, একজন টেস্টার, আর একজন নন-টেকনিক্যাল প্রোডাক্ট ম্যানেজার — তিনজনেই পড়ে বুঝতে পারেন, কোনো কোড না জেনেও।

২ · বাস্তব ইন্ডাস্ট্রিতে: Cucumber ও Gherkin

বাস্তব ইন্ডাস্ট্রিতে এই ধরনের সিনারিও প্রায়ই Gherkin নামের একটি নির্দিষ্ট, আধা-কাঠামোগত সিনট্যাক্সে লেখা হয় (Feature:, Scenario:, Given, When, Then কীওয়ার্ডসহ একটি .feature ফাইলে), আর Cucumber-এর মতো একটি টুল সেই ফাইলটি পড়ে প্রতিটি ধাপকে (একটি "step definition" ফাইলে লেখা) প্রকৃত টেস্ট কোডের সাথে জুড়ে দেয় এবং চালায়। এই পদ্ধতিতে অ-টেকনিক্যাল স্টেকহোল্ডাররাও সরাসরি .feature ফাইল পড়ে/লিখে সিনারিওতে অবদান রাখতে পারেন।

গুরুত্বপূর্ণ সীমাবদ্ধতা

এই কোর্সের Pyodide স্যান্ডবক্সে শুধু Python-এর স্ট্যান্ডার্ড লাইব্রেরি চলে — আসল Cucumber বা Gherkin পার্সার এখানে ইনস্টল করা বা চালানো সম্ভব নয়। তাই নিচের কোড সেলটি আসল Gherkin সিনট্যাক্স বা Cucumber-এর API নয় — বরং Given-When-Then ধারণাটির মূল যান্ত্রিকতা (প্রেক্ষাপট সেট করা, একটি কাজ ঘটানো, ফলাফল যাচাই করা) বোঝানোর জন্য stdlib দিয়ে বানানো একটি সরলীকৃত, নিজস্ব কাঠামো।

৩ · একটি সরল, সত্যিকারে-চালানো Given-When-Then কাঠামো

নিচে একটি ছোট্ট Scenario ক্লাস আছে — এর given(), when(), then() মেথডগুলো একটি শেয়ার্ড context ডিকশনারিতে কাজ করে এবং প্রতিটি ধাপ লগ করে রাখে। সিনারিও: "বৈধ কুপনসহ একজন ব্যবহারকারীর চেকআউট" — এটি genuinely এক্সিকিউট হয় এবং "Then" ধাপে real computed final_total-এর বিরুদ্ধে একটি real assert চলে।

Python
class Scenario:
    """একটি সরল, stdlib-only Given-When-Then কাঠামো -- আসল Cucumber/Gherkin নয়,
    শুধু ধারণাটি real, চলমান Python দিয়ে বোঝানোর জন্য একটি ছোট সহায়ক ক্লাস।"""
    def __init__(self, title):
        self.title = title
        self.context = {}
        self.log = []

    def given(self, description, fn):
        fn(self.context)
        self.log.append(f"Given {description}")
        return self

    def when(self, description, fn):
        fn(self.context)
        self.log.append(f"When {description}")
        return self

    def then(self, description, assertion_fn):
        assertion_fn(self.context)
        self.log.append(f"Then {description}  [OK]")
        return self

    def print_log(self):
        print(f"Scenario: {self.title}")
        for line in self.log:
            print(f"  {line}")


COUPONS = {"SAVE10": 0.10}  # কুপন কোড -> ছাড়ের হার

def apply_coupon(context):
    discount_rate = COUPONS.get(context["coupon_code"], 0.0)
    context["discount_rate"] = discount_rate

def checkout(context):
    total = context["cart_total"]
    discount = total * context["discount_rate"]
    context["final_total"] = total - discount

def assert_final_total_is_900(context):
    actual = context["final_total"]
    assert actual == 900, f"প্রত্যাশিত final_total=900, পাওয়া গেছে {actual}"

scenario = Scenario("বৈধ কুপনসহ একজন ব্যবহারকারীর চেকআউট")
try:
    (scenario
        .given("কার্টে ১০০০ টাকার পণ্য আছে", lambda ctx: ctx.update(cart_total=1000))
        .given("ব্যবহারকারী 'SAVE10' কুপন কোড প্রয়োগ করেছেন", lambda ctx: ctx.update(coupon_code="SAVE10"))
        .when("কুপনটি যাচাই ও প্রয়োগ করা হয়", apply_coupon)
        .when("ব্যবহারকারী চেকআউট সম্পন্ন করেন", checkout)
        .then("ফাইনাল টোটাল ৯০০ টাকা হওয়া উচিত (১০% ছাড় প্রয়োগের পর)", assert_final_total_is_900)
    )
    scenario.print_log()
    print(f"\nবাস্তব computed context: cart_total={scenario.context['cart_total']}, "
          f"discount_rate={scenario.context['discount_rate']}, final_total={scenario.context['final_total']}")
    print("ফলাফল: PASS -- সিনারিওটি প্রত্যাশিতভাবে সম্পন্ন হয়েছে")
except AssertionError as e:
    scenario.print_log()
    print(f"\nফলাফল: FAIL -- {e}")

    
প্রতিটি given()/when() কল সত্যিই context ডিকশনারিতে লেখে — apply_coupon genuinely COUPONS ডিকশনারিতে "SAVE10" লুকআপ করে discount_rate = 0.10 সেট করে, আর checkout genuinely 1000 - (1000 * 0.10) = 900 গণনা করে। "Then" ধাপে assert_final_total_is_900 এই real computed 900-এর বিরুদ্ধেই আসল assert চালায় — যদি COUPONS-এ ভুল ছাড়ের হার থাকত, বা checkout-এর গণনা ভুল হতো, এই assertion genuinely AssertionError রেইজ করত এবং except ব্লকে ধরা পড়ত।
BDD বনাম TDD (M12/L52)

TDD ও BDD দুটোই "কোড লেখার আগে প্রত্যাশা লিখে ফেলা" নীতিতে বিশ্বাস করে, কিন্তু ভিন্ন দর্শকের জন্য। M12/L52-এর TDD উদাহরণে টেস্টগুলো ডেভেলপারের জন্য লেখা — টেকনিক্যাল ভাষায় (assertEqual), একটি ফাংশনের নির্ভুলতা যাচাই করতে। এখানের BDD সিনারিও লেখা হয়েছে এমনভাবে যাতে একজন প্রোডাক্ট ম্যানেজার বা ক্লায়েন্টও Given/When/Then লাইনগুলো পড়ে বুঝতে পারেন সিস্টেমটি কী করবে — এটি স্টেকহোল্ডারদের মধ্যে একমত হওয়ার একটি যোগাযোগ-টুল, শুধু একটি ডেভেলপার-টেস্টিং টেকনিক নয়।

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

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

প্র ০১ কেন BDD সিনারিও ইচ্ছাকৃতভাবে "প্রায়-স্বাভাবিক ভাষায়" লেখা হয়, সরাসরি কোড হিসেবে নয়?

যাতে অ-টেকনিক্যাল স্টেকহোল্ডার (প্রোডাক্ট ম্যানেজার, ক্লায়েন্ট, বিজনেস অ্যানালিস্ট) কোনো প্রোগ্রামিং জ্ঞান ছাড়াই সিনারিওটি পড়ে যাচাই করতে পারেন এটি আসলে তাদের প্রত্যাশিত আচরণ কি না — কোড লেখা শুরু হওয়ার আগেই। এটি ভুল বোঝাবুঝি (একজন ডেভেলপার একরকম বুঝেছেন, ক্লায়েন্ট আরেকরকম চেয়েছেন) কোডিং শুরুর আগেই ধরার একটি উপায়, যা M12/L51-এর শিফট-লেফট নীতির সাথে সামঞ্জস্যপূর্ণ।

প্র ০২ উপরের কোড সেলে যদি COUPONS ডিকশনারিতে "SAVE10": 0.10-এর বদলে ভুলবশত "SAVE10": 0.15 লেখা থাকত, তাহলে কী হতো?

apply_coupon তখন discount_rate = 0.15 সেট করত, আর checkout গণনা করত 1000 - 150 = 850। "Then" ধাপে assert_final_total_is_900 তখন 850 == 900 চেক করে genuinely AssertionError রেইজ করত, এবং except ব্লকে ধরা পড়ে "ফলাফল: FAIL" প্রিন্ট হতো — এই সিনারিওটি একটি real, bug-catching-capable টেস্টের মতোই কাজ করে, শুধু একটি বর্ণনামূলক নথি নয়।

প্র ০৩ আসল Cucumber/Gherkin ব্যবহার করলে এই একই সিনারিও লেখার ক্ষেত্রে ঠিক কী আলাদা হতো, এই পাঠের কোড সেলের তুলনায়?

Gherkin-এ সিনারিওটি একটি আলাদা .feature টেক্সট ফাইলে Given, When, Then কীওয়ার্ড দিয়ে বিশুদ্ধ ব্যবসায়িক ভাষায় লেখা হতো (কোনো Python কোড ছাড়াই), আর Cucumber প্রতিটি লাইনকে regex/প্যাটার্ন ম্যাচিং দিয়ে একটি আলাদা "step definitions" ফাইলের প্রকৃত টেস্ট কোডের সাথে জুড়ে দিত। এই পাঠের Scenario ক্লাসে সেই বিভাজন নেই — এখানে বর্ণনা ও এক্সিকিউশন কোড একই জায়গায়, একটি একক সরলীকৃত ইলাস্ট্রেশন হিসেবে, আসল টুলের প্রতিস্থাপন হিসেবে নয়।

অনুশীলন

  1. চিন্তা করুন: "একজন ব্যবহারকারী ভুল পাসওয়ার্ড দিয়ে লগইন করার চেষ্টা করছেন" — এই পরিস্থিতির জন্য নিজে একটি Given-When-Then বাক্য (প্রায়-স্বাভাবিক ভাষায়, কোড নয়) লিখুন।

    একটি সম্ভাব্য উত্তর: "Given একজন নিবন্ধিত ব্যবহারকারী আছেন, When তিনি সঠিক ইউজারনেম কিন্তু ভুল পাসওয়ার্ড দিয়ে লগইন করার চেষ্টা করেন, Then সিস্টেম তাকে প্রবেশ করতে না দিয়ে একটি "ভুল পাসওয়ার্ড" এরর মেসেজ দেখাবে।" লক্ষ্য করুন এই বাক্যে কোনো কোড নেই, তবু ঠিক কী পরীক্ষা করতে হবে তা স্পষ্ট।

  2. পরীক্ষা করুন: উপরের কোড সেলে given() চেইনে একটি নতুন লাইন যোগ করুন — .given("কুপন কোডটি আসলে 'EXPIRED99'", lambda ctx: ctx.update(coupon_code="EXPIRED99")) — (এটি শেষের given-এর পরিবর্তে বসিয়ে) এবং Run চেপে দেখুন সিনারিওটি PASS নাকি FAIL দেখায়।

    FAIL দেখাবে — কারণ "EXPIRED99" কোডটি COUPONS ডিকশনারিতে নেই, তাই COUPONS.get("EXPIRED99", 0.0) ডিফল্ট মান 0.0 রিটার্ন করবে, অর্থাৎ কোনো ছাড় প্রয়োগ হবে না এবং final_total হবে 1000, ৯০০ নয়। "Then" ধাপের assertion তখন genuinely ব্যর্থ হবে — এটি প্রমাণ করে সিনারিওটি সত্যিই কুপন কোডের উপর নির্ভর করে ফলাফল পরিবর্তন করে, হার্ডকোড করা "সবসময় ৯০০" নয়।

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

আগের পাঠ
টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD)