পাঠ ৪৫ · ৫৮-এর মধ্যে · মডিউল ১০

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

Test-driven development (TDD)
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • TDD কী এবং কেন "টেস্ট আগে" এই উল্টো ক্রমটি ইচ্ছাকৃত
  • Red-Green-Refactor সাইকেলের প্রতিটি ধাপ নির্দিষ্টভাবে কী দাবি করে
  • TDD-এর সৎ, বাস্তবিক সুবিধা — টেস্ট কভারেজ ও ডিজাইনের উপর প্রভাব
  • একটি সত্যিকারের, এক্সিকিউট করা দুই-রাউন্ডের Red-Green সাইকেল — শুধু বর্ণনা নয়, বাস্তবে চালিয়ে দেখানো

১ · TDD কী — ক্রমটি উল্টে দেওয়া

টেস্ট-ড্রিভেন ডেভেলপমেন্টTest-Driven Development (TDD)একটি ডেভেলপমেন্ট শৃঙ্খলা যেখানে একটি ফাংশনালিটির জন্য টেস্ট লেখা হয় তার ইমপ্লিমেন্টেশন কোড লেখার আগে। (TDD) স্বজ্ঞাত ক্রমটিকে সম্পূর্ণ উল্টে দেয়: সাধারণত আমরা প্রথমে কোড লিখি, তারপর (হয়তো) সেটি টেস্ট করি — TDD বলে প্রথমে টেস্ট লিখুন, তারপর সেই টেস্টটি পাস করানোর জন্য প্রয়োজনীয় ন্যূনতম কোড লিখুন। এই উল্টো ক্রমটি ইচ্ছাকৃত, এবং একটি নির্দিষ্ট, পুনরাবৃত্ত সাইকেলে পরিচালিত হয়।

২ · Red-Green-Refactor সাইকেল

TDD-এর প্রমিত কাঠামো তিনটি ধাপের একটি পুনরাবৃত্ত সাইকেল — প্রতিটি নতুন ছোট্ট ফাংশনালিটির জন্য এই সাইকেলটি আবার চালানো হয়।

RED টেস্ট লিখুন, ব্যর্থ হতে দেখুন GREEN ন্যূনতম কোডে পাস করান REFACTOR কোড পরিষ্কার করুন পরের ছোট্ট ফাংশনালিটির জন্য আবার RED থেকে শুরু
প্রতিটি নতুন ফাংশনালিটির জন্য এই তিন-ধাপের চক্র আবার শুরু হয় — একবারে একটি ছোট্ট, পরিচালনাযোগ্য অংশ।
RED
এখনো-ইমপ্লিমেন্ট-না-হওয়া ছোট্ট একটি ফাংশনালিটির জন্য টেস্ট লিখুন, চালান, নিশ্চিত করুন এটি প্রকৃতপক্ষে ব্যর্থ হয় — এটি প্রমাণ করে টেস্টটি সত্যিই কিছু যাচাই করছে, ভুলবশত পাস হয়ে যাচ্ছে না।
GREEN
শুধু ততটুকু কোড লিখুন যা টেস্টটি পাস করাতে প্রয়োজন — এর বেশি কিছু নয় (M4/L15-এর YAGNI নীতির সরাসরি প্রয়োগ, ভবিষ্যতের হাইপোথেটিক্যাল প্রয়োজনের জন্য আগে থেকে কিছু বানানো নয়)।
REFACTOR
পাস-হওয়া টেস্টের নিরাপত্তা-জাল সহ, কোডের অভ্যন্তরীণ গঠন পরিষ্কার করুন — বাহ্যিক আচরণ অপরিবর্তিত রেখে (M11/L50-এ বিস্তারিত), প্রতিটি পরিবর্তনের পর টেস্ট আবার চালিয়ে নিশ্চিত করুন কিছু ভাঙেনি।
সৎভাবে বলা সুবিধা — বাড়িয়ে না বলে

TDD-তে লেখা টেস্ট স্বাভাবিকভাবেই বিস্তৃত হয়ে ওঠে, কারণ প্রতিটি ফাংশনালিটির অস্তিত্বই একটি স্পষ্ট টেস্ট দিয়ে চালিত হয়েছিল। ডিজাইনও প্রায়ই স্বাভাবিকভাবে টেস্টযোগ্য (প্রায়ই কম-কাপল্ড, M4/L14-এর নীতি) কোডের দিকে ঝোঁকে, কারণ কোনো কিছু সহজে টেস্ট করতে না পারলে সেই কষ্টটি লেখকই প্রথমে অনুভব করেন। আর প্রতিটি ছোট্ট পরিবর্তনের পর দ্রুত একটি ফিডব্যাক লুপ থাকে — কোড এখনো কাজ করছে কিনা তা তাৎক্ষণিকভাবে নিশ্চিত হওয়া যায়।

৩ · একটি বাস্তব, এক্সিকিউট করা সাইকেল

নিচের কোড সেলে আমরা একটি ক্লাসিক fizzbuzz-স্টাইল ফাংশনের জন্য একটি সম্পূর্ণ, দুই-রাউন্ডের Red-Green সাইকেল বাস্তবে চালাব — শুধু বর্ণনা নয়, প্রতিটি ধাপ সত্যিই এক্সিকিউট হবে। প্রথম রাউন্ডে টেস্টটি fizzbuzz ফাংশনটি এখনো লেখাই হয়নি বলে সত্যিকারের NameError দিয়ে ব্যর্থ হবে — আমরা সেই এররটি ধরব এবং প্রিন্ট করব, তারপর ন্যূনতম ইমপ্লিমেন্টেশন লিখে সেটি পাস করাব। দ্বিতীয় রাউন্ডে একটি নতুন টেস্ট যোগ হবে, যা বর্তমান ন্যূনতম ইমপ্লিমেন্টেশনের বিরুদ্ধে ব্যর্থ হবে (একটি ভুল মান নিয়ে), তারপর ইমপ্লিমেন্টেশন সম্প্রসারণ করে উভয় টেস্ট একসাথে পাস করানো হবে।

Python
# একটি সত্যিকারের, দুই-রাউন্ডের Red-Green-Refactor সাইকেল -- fizzbuzz-স্টাইল ফাংশনের জন্য।

print("রাউন্ড ১ (RED) -- fizzbuzz() এখনো লেখা হয়নি\n")

def test_multiple_of_3():
    assert fizzbuzz(3) == "Fizz"

try:
    test_multiple_of_3()
    print("অপ্রত্যাশিত PASS")
except NameError as e:
    print(f"RED -- প্রত্যাশিতভাবে ব্যর্থ: {e}")

print("\nরাউন্ড ১ (GREEN) -- ন্যূনতম ইমপ্লিমেন্টেশন লেখা হলো\n")

def fizzbuzz(n):
    return "Fizz"

try:
    test_multiple_of_3()
    print("GREEN -- test_multiple_of_3 পাস করেছে")
except AssertionError as e:
    print(f"অপ্রত্যাশিত FAIL: {e}")

print("\nরাউন্ড ২ (RED) -- নতুন টেস্ট: multiple of 5\n")

def test_multiple_of_5():
    assert fizzbuzz(5) == "Buzz"

try:
    test_multiple_of_5()
    print("অপ্রত্যাশিত PASS")
except AssertionError:
    print(f"RED -- প্রত্যাশিতভাবে ব্যর্থ: fizzbuzz(5) == {fizzbuzz(5)!r}, কিন্তু 'Buzz' প্রত্যাশিত ছিল")

print("\nরাউন্ড ২ (GREEN) -- ইমপ্লিমেন্টেশন সম্প্রসারণ\n")

def fizzbuzz(n):
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

test_multiple_of_3()
test_multiple_of_5()
print("GREEN -- test_multiple_of_3 ও test_multiple_of_5 উভয়ই একসাথে পাস করেছে")

    
লক্ষ্য করুন রাউন্ড ১-এর RED ধাপে ব্যর্থতার কারণ ছিল NameError (ফাংশনটিই অস্তিত্বহীন), কিন্তু রাউন্ড ২-এর RED ধাপে ব্যর্থতার কারণ ছিল AssertionError (ফাংশনটি অস্তিত্বশীল, কিন্তু ভুল মান ফেরত দিচ্ছিল — সবসময় "Fizz") — দুটোই "ব্যর্থতা," কিন্তু ভিন্ন কারণে, কারণ ইমপ্লিমেন্টেশন ততক্ষণে আংশিকভাবে অস্তিত্বশীল ছিল। রাউন্ড ২-এর GREEN ধাপের শেষে test_multiple_of_3() ও test_multiple_of_5() — দুটো টেস্টই একসাথে পাস করেছে, যা নিশ্চিত করে সম্প্রসারিত ইমপ্লিমেন্টেশন আগের ফাংশনালিটি ভাঙেনি (এটিই REFACTOR-পরবর্তী নিরাপত্তা-জালের মূল ধারণা)।
মূল কথা · Key takeaway

TDD টেস্ট লেখাকে ইমপ্লিমেন্টেশনের পরের একটি "চেক-বক্স" কাজ থেকে ডিজাইন প্রক্রিয়ার একটি সক্রিয় চালিকাশক্তিতে পরিণত করে। Red ধাপ নিশ্চিত করে টেস্ট আসলে কার্যকর, Green ধাপ ন্যূনতমবাদী থাকতে বাধ্য করে, আর Refactor ধাপ একটি নিরাপত্তা-জাল সহ ক্রমাগত পরিচ্ছন্নতা বজায় রাখার সুযোগ দেয় — M11/L50-এর রিফ্যাক্টরিং পাঠে এই ধাপটি আরও গভীরভাবে অন্বেষণ করা হবে।

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

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

প্র ০১ যদি কেউ RED ধাপ বাদ দিয়ে সরাসরি টেস্ট ও ইমপ্লিমেন্টেশন একসাথে লিখে ফেলে (টেস্টটি আসলে কখনো ব্যর্থ হতে দেখা না গিয়ে), তাহলে কী ঝুঁকি থেকে যায়?

ঝুঁকি হলো টেস্টটি নিজেই ভুলভাবে লেখা হতে পারে — যেমন ভুল ফাংশন কল করা, বা একটি assertion যা আসলে সবসময় সত্য (একটি ভাঙা ইমপ্লিমেন্টেশনের বিরুদ্ধেও)। যদি টেস্টটি কখনো ব্যর্থ হতে না দেখা হয়, তাহলে এটি নিশ্চিত করার কোনো উপায় নেই যে টেস্টটি প্রকৃতপক্ষে কিছু যাচাই করছে — এটি একটি "ভুয়া" টেস্ট হতে পারে যা সবসময় পাস করবে, ইমপ্লিমেন্টেশন যাই হোক না কেন, এবং কখনো কোনো প্রকৃত বাগ ধরবে না।

প্র ০২ উপরের কোড সেলে রাউন্ড ১-এর GREEN ধাপে ইমপ্লিমেন্টেশন ছিল return "Fizz" — এটি কি একটি "ভালো" বা "সম্পূর্ণ" fizzbuzz ইমপ্লিমেন্টেশন? এটি তখন লেখা কি ঠিক ছিল?

না, এটি স্পষ্টভাবেই একটি সম্পূর্ণ fizzbuzz ইমপ্লিমেন্টেশন নয় — কিন্তু TDD-এর দর্শন অনুযায়ী এটি সেই মুহূর্তে সম্পূর্ণ সঠিক ছিল, কারণ GREEN ধাপের নিয়ম হলো "শুধু ততটুকু কোড লিখুন যা বর্তমান টেস্ট পাস করাতে প্রয়োজন" (YAGNI)। এটি ইচ্ছাকৃতভাবে অসম্পূর্ণ রাখা হয়েছিল, কারণ তখনো শুধু একটি টেস্ট (test_multiple_of_3) ছিল — রাউন্ড ২-এ নতুন টেস্ট যোগ হওয়ার পরই ইমপ্লিমেন্টেশন সম্প্রসারিত করার প্রয়োজন দেখা দিল।

প্র ০৩ কোড সেলে রাউন্ড ২-এর GREEN ধাপের শেষে কেন test_multiple_of_3() এবং test_multiple_of_5() দুটোই আবার চালানো হলো, শুধু নতুনটি নয়?

কারণ ইমপ্লিমেন্টেশন সম্প্রসারণ করার সময় (নতুন if n % 5 == 0 শাখা যোগ করা) পুরনো ফাংশনালিটি ভেঙে যাওয়ার একটি বাস্তব ঝুঁকি থাকে — এটিই রিগ্রেশনের ধারণা (M10/L48-এ আরও বিস্তারিত)। শুধু নতুন টেস্ট পাস করলেই যথেষ্ট নয়; আগের সব টেস্টও একসাথে এখনো পাস করছে কিনা তা নিশ্চিত করা দরকার — এটিই প্রমাণ করে সম্প্রসারণটি একটি নিরাপদ, "additive" পরিবর্তন ছিল, পুরনো আচরণ ভাঙা কোনো পরিবর্তন নয়।

অনুশীলন

  1. চিন্তা করুন: fizzbuzz-এর একটি তৃতীয় নিয়মও থাকে: যে সংখ্যা ৩ এবং ৫ উভয় দ্বারাই বিভাজ্য (যেমন ১৫), তার জন্য "FizzBuzz" ফেরত দিতে হয়। এই তৃতীয় নিয়মটি TDD সাইকেলে যোগ করতে হলে প্রথমে কী লিখতে হবে?

    TDD-এর শৃঙ্খলা অনুযায়ী প্রথমেই একটি নতুন টেস্ট লিখতে হবে — যেমন assert fizzbuzz(15) == "FizzBuzz" — এবং সেটি বর্তমান ইমপ্লিমেন্টেশনের বিরুদ্ধে চালিয়ে দেখতে হবে এটি ব্যর্থ হয় কিনা (রাউন্ড ৩-এর RED)। বর্তমান ইমপ্লিমেন্টেশনে fizzbuzz(15) আসলে "Fizz" ফেরত দেবে (যেহেতু n % 3 == 0 শর্তটি প্রথমে চেক হয় এবং সত্য), তাই টেস্টটি ব্যর্থ হবে — এটিই নতুন GREEN ধাপে n % 15 == 0-এর একটি নতুন, প্রথম-চেক-করা শর্ত যোগ করার প্রয়োজনীয়তা দেখাবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে রাউন্ড ২-এর GREEN ধাপের পরে একটি নতুন লাইন যোগ করুন: print(fizzbuzz(15))। ফলাফল কী প্রিন্ট হয়, এবং এটি কি প্রকৃত fizzbuzz নিয়ম অনুযায়ী সঠিক?

    এটি "Fizz" প্রিন্ট করবে, "FizzBuzz" নয় — কারণ বর্তমান ইমপ্লিমেন্টেশনে প্রথম শর্ত if n % 3 == 0 সত্য হলেই সাথে সাথে "Fizz" ফেরত দিয়ে ফাংশনটি শেষ হয়ে যায়, n % 5 == 0 শর্তটি পরীক্ষা করার সুযোগই পায় না। এটি ঠিক উপরের অনুশীলনের পয়েন্টটিই প্রমাণ করে — বর্তমান ইমপ্লিমেন্টেশন কোনো টেস্ট দিয়ে "১৫ দ্বারা বিভাজ্য" কেস চালিত হয়নি, তাই এটি সেই কেসের জন্য এখনো সঠিক নয়।

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

আগের পাঠ
টেস্টিং লেভেল — ইউনিট, ইন্টিগ্রেশন, সিস্টেম, অ্যাকসেপ্টেন্স