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

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

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

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

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

১ · Red-Green-Refactor চক্র

টেস্ট-ড্রিভেন ডেভেলপমেন্টTest-Driven Development (TDD)একটি ডেভেলপমেন্ট পদ্ধতি যেখানে ফিচারের কোড লেখার আগেই সেই ফিচারের জন্য একটি টেস্ট লেখা হয়, তারপর ঠিক ততটুকু কোড লেখা হয় যা টেস্টটি পাস করাতে দরকার। একটি ছোট, বারবার-পুনরাবৃত্ত চক্রে চলে:

Red
একটি নতুন আচরণের জন্য টেস্ট লেখা হয় — যেহেতু সেই আচরণের কোড এখনও নেই, টেস্টটি ব্যর্থ (FAIL) বা ERROR হয়। এটি নিশ্চিত করে টেস্টটি আসলেই কিছু যাচাই করছে।
Green
ঠিক ততটুকু কোড লেখা হয় যা টেস্টটি পাস করাতে দরকার — এখানে "সুন্দর" বা "সাধারণীকৃত" কোড লেখার তাড়া নেই, শুধু পাস করানোই লক্ষ্য।
Refactor
টেস্ট পাস অবস্থায় রেখেই কোডের গঠন উন্নত করা হয় (নাম পরিষ্কার করা, ডুপ্লিকেশন সরানো, এজ-কেস হ্যান্ডলিং যোগ করা) — টেস্ট প্রতিবার চালিয়ে নিশ্চিত করা হয় আচরণ অপরিবর্তিত আছে।

এই চক্রটি M12/L51-এর "শিফট-লেফট" নীতির সবচেয়ে চরম প্রয়োগ — যাচাই কোডিং-এরও আগে চলে আসে, কারণ টেস্টই প্রথমে লেখা হয়।

২ · Red — একটি সত্যিকারের ব্যর্থ টেস্ট

ধরা যাক আমরা একটি calculate_late_fee(days_late) ফাংশন বানাব — নিয়ম: প্রতিদিন দেরির জন্য ১০ টাকা জরিমানা। TDD অনুযায়ী প্রথমে টেস্ট লেখা হয়, ফাংশনের ভেতরে এখনও কোনো প্রকৃত implementation নেই (এটি NotImplementedError রেইজ করে)। নিচের সেলটি Run করলে টেস্টটি সত্যিই ERROR দেবে — এটি "FAIL" থেকে আলাদা টার্ম, কারণ এখানে একটি assertion ব্যর্থ হয়নি, বরং টেস্ট চলাকালীন একটি exception রেইজ হয়েছে।

Python · ধাপ ১ / Red
import unittest

def calculate_late_fee(days_late):
    # এখনও কোনো implementation লেখা হয়নি -- TDD-তে টেস্ট আগে লেখা হয়, কোড পরে
    raise NotImplementedError("calculate_late_fee এখনও implement করা হয়নি")

class LateFeeTests(unittest.TestCase):
    def test_three_days_late_charges_30(self):
        # নিয়ম: প্রতিদিন দেরির জন্য ১০ টাকা জরিমানা
        self.assertEqual(calculate_late_fee(3), 30)

suite = unittest.TestLoader().loadTestsFromTestCase(LateFeeTests)
result = unittest.TextTestRunner(verbosity=2).run(suite)

print(f"\nমোট টেস্ট: {result.testsRun}, ব্যর্থ (failures): {len(result.failures)}, এরর: {len(result.errors)}")
assert len(result.errors) == 1, "প্রত্যাশিত: ঠিক একটি ERROR (NotImplementedError-এর কারণে)"
print("যাচাই সফল: Red ধাপ নিশ্চিত -- ফাংশনটি এখনও implement করা হয়নি বলে টেস্টটি genuinely ERROR দিয়েছে।")

    
calculate_late_fee(3) কল হওয়ার সাথে সাথে NotImplementedError রেইজ হয়, যা test_three_days_late_charges_30-কে assertEqual পর্যন্ত পৌঁছাতেই দেয় না — তাই unittest একে "ERROR" হিসেবে গণনা করে (F নয়, E)। result.errors-এর দৈর্ঘ্য সত্যিই ১, যা কোড সেলের নিজের assert দিয়ে যাচাই করা হয়েছে — এটি একটি প্রকৃত, চালানো ব্যর্থতা, বর্ণনামূলক অনুমান নয়।

৩ · Green — ন্যূনতম কোডে পাস করানো

এখন ঠিক ততটুকু কোড লেখা হবে যা টেস্টটি পাস করাতে দরকার — একটি সরল গুণ। টেস্ট ক্লাস ও মেথড অবিকল আগের মতোই, শুধু ফাংশনের ভেতরের implementation পাল্টেছে।

Python · ধাপ ২ / Green
import unittest

def calculate_late_fee(days_late):
    return days_late * 10   # ন্যূনতম implementation -- শুধু টেস্ট পাস করানোর জন্য যথেষ্ট কোড

class LateFeeTests(unittest.TestCase):
    def test_three_days_late_charges_30(self):
        self.assertEqual(calculate_late_fee(3), 30)

suite = unittest.TestLoader().loadTestsFromTestCase(LateFeeTests)
result = unittest.TextTestRunner(verbosity=2).run(suite)

print(f"\nমোট টেস্ট: {result.testsRun}, ব্যর্থ (failures): {len(result.failures)}, এরর: {len(result.errors)}")
assert result.wasSuccessful(), "Green ধাপে টেস্টটি পাস করার কথা ছিল"
print("যাচাই সফল: Green ধাপ নিশ্চিত -- ন্যূনতম implementation-এই টেস্টটি genuinely PASS করেছে।")

    
days_late * 10 — এটুকুই যথেষ্ট, কারণ টেস্ট শুধু days_late=3 ইনপুটে 30 প্রত্যাশা করছে। TDD-তে এই ধাপে "সাধারণীকৃত" বা "সুন্দর" কোড লেখার তাড়া থাকে না — শুধু বর্তমান টেস্টগুলো পাস করানোই লক্ষ্য। result.wasSuccessful() সত্যিই True রিটার্ন করে, যা কোড সেলের নিজস্ব assert দিয়ে যাচাই করা।

৪ · Refactor — একই টেস্ট, উন্নত কোড

এবার কোডের গঠন উন্নত করা হবে — ম্যাজিক নাম্বার 10-কে একটি অর্থবহ নামে বের করে আনা, এবং নেগেটিভ days_late (যেমন ভুলবশত পাঠানো একটি অসম্ভব ইনপুট) থেকে রক্ষা করা। লক্ষ্য করুন টেস্ট ক্লাসটি অবিকল অপরিবর্তিত — নতুন কোনো assertion যোগ হয়নি, শুধু ফাংশনের ভেতরের কোড বদলেছে।

Python · ধাপ ৩ / Refactor
import unittest

RATE_PER_DAY = 10  # রিফ্যাক্টর: ম্যাজিক নাম্বার ১০-কে একটি অর্থবহ নামে বের করে আনা হলো

def calculate_late_fee(days_late):
    # রিফ্যাক্টর: নেগেটিভ days_late (যেমন সময়ের আগে ফেরত দেওয়া) থেকে রক্ষা করা হলো,
    # কিন্তু days_late=3 ইনপুটে ফলাফল অপরিবর্তিত -- বাহ্যিক আচরণ একই থাকে
    safe_days = max(0, days_late)
    return safe_days * RATE_PER_DAY

class LateFeeTests(unittest.TestCase):
    def test_three_days_late_charges_30(self):
        self.assertEqual(calculate_late_fee(3), 30)

suite = unittest.TestLoader().loadTestsFromTestCase(LateFeeTests)
result = unittest.TextTestRunner(verbosity=2).run(suite)

print(f"\nমোট টেস্ট: {result.testsRun}, ব্যর্থ (failures): {len(result.failures)}, এরর: {len(result.errors)}")
assert result.wasSuccessful(), "Refactor-এর পরও একই টেস্ট পাস করার কথা ছিল"
print("যাচাই সফল: Refactor ধাপ নিশ্চিত -- কোডের গঠন বদলেছে, কিন্তু অপরিবর্তিত টেস্টটি এখনও genuinely PASS করছে।")

# রিফ্যাক্টরের বাড়তি সুবিধা: নেগেটিভ ইনপুটেও এখন যুক্তিসঙ্গত ফলাফল দেয়
print(f"\nবোনাস (নতুন টেস্ট নয়, শুধু ম্যানুয়াল যাচাই): calculate_late_fee(-2) = {calculate_late_fee(-2)} (আগের implementation দিত -20, যা অর্থহীন ছিল)")

    
max(0, days_late) যোগ করার পরও calculate_late_fee(3) এখনও 30-ই রিটার্ন করে (যেহেতু max(0, 3) == 3), তাই বিদ্যমান, অপরিবর্তিত টেস্টটি এখনও পাস করে। কিন্তু কোডটি এখন নেগেটিভ ইনপুটের ক্ষেত্রে বেশি সুরক্ষিত — এটিই রিফ্যাক্টরিং-এর মূল ধারণা: বহিরাগত আচরণ (যা টেস্ট যাচাই করে) অপরিবর্তিত রেখে অভ্যন্তরীণ গঠন উন্নত করা। যদি রিফ্যাক্টরের সময় ভুলবশত আচরণ বদলে যেত (যেমন RATE_PER_DAY-কে ভুল করে 11 লেখা হতো), তাহলে এই একই, অপরিবর্তিত টেস্টটিই তা ধরে ফেলত — এটাই TDD-তে টেস্টকে "সেফটি নেট" বলার কারণ।
মূল কথা · Key takeaway

TDD-এর তিনটি ধাপ একসাথে একটি নিরাপদ, পুনরাবৃত্তিযোগ্য চক্র তৈরি করে: Red প্রমাণ করে টেস্টটি আসলেই কাজ করছে (ব্যর্থ হতে পারছে), Green প্রমাণ করে ন্যূনতম কোডেই কাজ চলছে, আর Refactor প্রমাণ করে কোড উন্নত করার সময়ও আচরণ অক্ষত থাকছে — প্রতিটি ধাপেই একই টেস্ট-রানার সত্যিকারের ফলাফল দিচ্ছে, বর্ণনা নয়। M12/L53-এ দেখা যাবে কীভাবে বড় পরিসরে, ব্যবসায়িক ভাষায় আচরণ বর্ণনা করার জন্য BDD এই একই ধারণাকে সম্প্রসারিত করে।

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

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

প্র ০১ Red ধাপে টেস্টটি "ERROR" দিয়েছে, "FAIL" নয় — এই দুটোর মধ্যে পার্থক্য কী, এবং TDD-এর জন্য দুটোই কি সমানভাবে গ্রহণযোগ্য "Red"?

"FAIL" হয় যখন একটি assert চেক করা হয় কিন্তু প্রত্যাশিত মান মেলে না (যেমন assertEqual(5, 6))। "ERROR" হয় যখন টেস্ট মেথড চলাকালীন কোনো অপ্রত্যাশিত exception রেইজ হয় (যেমন এখানে NotImplementedError, বা AttributeError যদি ফাংশনটি একেবারেই না থাকত)। TDD-এর প্রথম ধাপের জন্য দুটোই গ্রহণযোগ্য "Red" — মূল কথা হলো টেস্টটি এখনো-না-লেখা কোডের বিরুদ্ধে চালালে সবুজ (PASS) না হয়ে থামা উচিত।

প্র ০২ Green ধাপে calculate_late_fee-এর implementation ছিল শুধু days_late * 10 — এটি কি "যথেষ্ট ভালো" কোড? TDD কেন তাও গ্রহণযোগ্য বলে?

এটি সম্পূর্ণ "প্রোডাকশন-রেডি" নয় (যেমন নেগেটিভ ইনপুট হ্যান্ডল করে না), কিন্তু TDD-এর দর্শন হলো প্রতিটি ধাপে ছোট, নিশ্চিত অগ্রগতি করা — আগে টেস্ট পাস করানো, তারপর (Refactor ধাপে) মান উন্নত করা। একসাথে "নিখুঁত ও পাসিং" কোড লেখার চেষ্টা করলে ভুল হওয়ার সুযোগ বেশি; ছোট ধাপে এগোলে প্রতিটি ধাপেই একটি পাসিং টেস্ট-স্যুট থাকে, যা পরবর্তী পরিবর্তনের জন্য একটি নিরাপদ ভিত্তি দেয়।

প্র ০৩ Refactor ধাপে যদি কেউ ভুলবশত RATE_PER_DAY = 10-কে RATE_PER_DAY = 12 লিখে ফেলত, তাহলে কী হতো?

calculate_late_fee(3) তখন 36 রিটার্ন করত, কিন্তু টেস্টটি এখনও assertEqual(calculate_late_fee(3), 30) প্রত্যাশা করছে — তাই টেস্টটি genuinely FAIL করত (36 != 30)। এটিই দেখায় Refactor ধাপে টেস্ট বারবার চালানো কেন গুরুত্বপূর্ণ — টেস্ট পাস করাই নিশ্চিত করে রিফ্যাক্টরিং সত্যিই আচরণ অক্ষত রেখেছে, শুধু কোড "সুন্দর লাগছে" বলে ধরে নেওয়া নয়।

অনুশীলন

  1. চিন্তা করুন: ধাপ ১ (Red)-এর কোড সেলে calculate_late_fee সম্পূর্ণ মুছে ফেলা হলে (একেবারেই ডিফাইন না করলে) টেস্ট রান করলে কী হতো — এখনকার মতোই "ERROR" আসত, নাকি ভিন্ন কিছু?

    তখনও "ERROR" আসত, তবে ভিন্ন exception টাইপে — NameError: name 'calculate_late_fee' is not defined। ফলাফলের ধরন (ERROR, FAIL নয়) একই থাকত, কারণ উভয় ক্ষেত্রেই assertEqual-এ পৌঁছানোর আগেই একটি exception রেইজ হয়ে টেস্ট থেমে যায় — শুধু exception-এর সুনির্দিষ্ট কারণ ভিন্ন।

  2. পরীক্ষা করুন: ধাপ ৩ (Refactor)-এর কোড সেলে test_three_days_late_charges_30 মেথডে আরেকটি লাইন যোগ করুন — self.assertEqual(calculate_late_fee(-5), 0) — এবং Run চেপে দেখুন এটি পাস করে কি না।

    হ্যাঁ, এটি পাস করবে — কারণ রিফ্যাক্টর করা কোডে safe_days = max(0, days_late) থাকায় calculate_late_fee(-5) প্রথমে -5-কে 0-এ পরিণত করে, তারপর 0 * 10 = 0 রিটার্ন করে। মজার বিষয় হলো, যদি এই একই নতুন assertion ধাপ ২ (Green)-এর কোডের বিরুদ্ধে চালানো হতো, সেটি FAIL করত (-5 * 10 = -50 != 0) — এটি দেখায় রিফ্যাক্টর শুধু কোড পরিষ্কার করেনি, প্রকৃতপক্ষে একটি বাস্তব দুর্বলতাও ঠিক করেছে।

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

আগের পাঠ
শিফট-লেফট টেস্টিং