পাঠ ২২ · ৫৭-এর মধ্যে · মডিউল ৫
Home / Courses / Software Testing & Quality Assurance / রিগ্রেশন টেস্টিং

রিগ্রেশন টেস্টিং ও টেস্ট স্যুট মেইনটেন্যান্স

Regression testing and test suite maintenance
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • রিগ্রেশন টেস্টিং কী এবং এটি কেন প্রতিটি কোড পরিবর্তনের পর জরুরি
  • কীভাবে একটি বিদ্যমান, অপরিবর্তিত টেস্ট স্যুট একটি নতুন রিগ্রেশন ধরে ফেলতে পারে
  • টেস্ট স্যুট মেইনটেন্যান্স — কেন পুরনো টেস্ট রাখা, বাদ দেওয়া বা আপডেট করার সিদ্ধান্ত সাবধানে নিতে হয়
  • একটি সত্যিকারের, চলমান রিগ্রেশন-ধরার ডেমো — narration নয়, সত্যিকারের computed ফলাফল

১ · রিগ্রেশন টেস্টিং কী, এবং কেন এটি এত গুরুত্বপূর্ণ

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

কোড পরিবর্তন (ফিচার/ফিক্স/রিফ্যাক্টর) বিদ্যমান টেস্ট স্যুট (অপরিবর্তিত) আবার চালানো রিগ্রেশন ধরা পড়লো একটি আগে-পাস-করা টেস্ট এখন FAIL
টেস্ট কোড নিজে বদলায়নি — শুধু production কোড বদলেছে, আর সেই বদলে একটি পুরনো, সঠিক আচরণ ভেঙে গেছে বলে একটি টেস্ট এখন ব্যর্থ হচ্ছে।

২ · একই টেস্ট স্যুট, দুটো ভার্সন — সত্যিকারের ডেমো

নিচের কোড সেলে প্রথমে calculate_discount-এর একটি সঠিক ভার্সন সংজ্ঞায়িত করা হয়েছে (নিয়ম: মেম্বার হলে ১০% ছাড়, নাহলে কোনো ছাড় নেই), তারপর তিনটি টেস্ট কেসসহ একটি unittest.TestCase লেখা হয়েছে। সেই টেস্ট স্যুট প্রথমে পুরনো ভার্সনের বিরুদ্ধে চালানো হয় (সব পাস করার কথা), তারপর ফাংশনটির নাম একটি নতুন, "পরিবর্তিত" ভার্সনের সাথে পুনরায় বাইন্ড করা হয় — টেস্ট কোডের একটি অক্ষরও না বদলে — এবং ঠিক একই টেস্ট স্যুট আবার চালানো হয়।

Python
import unittest

# --- ধাপ ১: ফাংশনের পুরনো, সঠিক ভার্সন ---
def calculate_discount(price, is_member):
    # নিয়ম: মেম্বার হলে ১০% ছাড়, নাহলে কোনো ছাড় নেই
    if is_member:
        return round(price * 0.9, 2)
    return price

# --- ধাপ ২: বিদ্যমান টেস্ট স্যুট (এই কোড ভবিষ্যতে আর বদলাবে না) ---
class DiscountTests(unittest.TestCase):
    def test_member_gets_discount(self):
        self.assertEqual(calculate_discount(1000, True), 900)

    def test_non_member_no_discount(self):
        self.assertEqual(calculate_discount(1000, False), 1000)

    def test_zero_price(self):
        self.assertEqual(calculate_discount(0, True), 0)

def run_suite(label):
    print(f"\n=== {label} ===")
    suite = unittest.TestLoader().loadTestsFromTestCase(DiscountTests)
    return unittest.TextTestRunner(verbosity=2).run(suite)

# --- ধাপ ৩: পুরনো ভার্সনের বিরুদ্ধে বিদ্যমান স্যুট চালানো ---
result_old = run_suite("পুরনো ভার্সনের বিরুদ্ধে টেস্ট স্যুট")
print(f"পুরনো ভার্সন: {result_old.testsRun - len(result_old.failures)}/{result_old.testsRun} টেস্ট পাস করেছে")

# --- ধাপ ৪: এখন কল্পনা করুন ডেভেলপার ফাংশনটি "রিফ্যাক্টর" করলেন,
#             কিন্তু ভুলবশত একটি রিগ্রেশন ঢুকিয়ে দিলেন —
#             is_member চেক করা ভুলে গিয়ে সবসময় ছাড় প্রয়োগ করছেন ---
def calculate_discount(price, is_member):
    # বাগ (রিগ্রেশন): is_member যাই হোক না কেন, সবসময় ১০% ছাড় প্রয়োগ হচ্ছে
    return round(price * 0.9, 2)

# --- ধাপ ৫: ঠিক সেই একই, অপরিবর্তিত টেস্ট স্যুট নতুন ভার্সনের বিরুদ্ধে আবার চালানো ---
result_new = run_suite("পরিবর্তিত/রিগ্রেসড ভার্সনের বিরুদ্ধে ঠিক সেই একই টেস্ট স্যুট")
print(f"নতুন ভার্সন: {result_new.testsRun - len(result_new.failures)}/{result_new.testsRun} টেস্ট পাস করেছে")

if result_new.failures:
    print("\nরিগ্রেশন ধরা পড়েছে এই টেস্টে(গুলোতে):")
    for test_case, traceback_text in result_new.failures:
        print(" -", test_case)

    
মনোযোগ দিয়ে লক্ষ্য করুন — DiscountTests ক্লাসের ভেতরের কোডের একটি অক্ষরও বদলানো হয়নি। পুরনো ভার্সনে test_non_member_no_discount পাস করে কারণ calculate_discount(1000, False) সঠিকভাবে 1000 রিটার্ন করে। কিন্তু রিগ্রেসড ভার্সনে একই কল round(1000 * 0.9, 2) মানে 900.0 রিটার্ন করে — যা প্রত্যাশিত 1000-এর সমান নয়, তাই ঠিক এই টেস্টটিই এখন FAIL করে, যদিও অন্য দুটো টেস্ট (test_member_gets_discount, test_zero_price) এখনও পাস করে, কারণ সেই দুটো ক্ষেত্রে পুরনো ও নতুন ফাংশন একই মান রিটার্ন করে। এটাই রিগ্রেশন টেস্টিং-এর আসল শক্তি — কোড পরিবর্তন হওয়ার সাথে সাথেই ঠিক কোন আচরণ ভেঙেছে তা নির্দিষ্টভাবে ধরিয়ে দেয়।

৩ · টেস্ট স্যুট মেইনটেন্যান্স

সময়ের সাথে সাথে একটি টেস্ট স্যুট নিজেও যত্ন চায়। কিছু ব্যবহারিক নীতি:

প্রতিটি বাগ ফিক্সে একটি নতুন টেস্ট
একটি প্রোডাকশন বাগ ধরা পড়লে ও ঠিক করা হলে, সেই নির্দিষ্ট বাগের জন্য একটি নতুন টেস্ট কেস যোগ করুন — যাতে ভবিষ্যতে এটি আবার ফিরে এলে রিগ্রেশন টেস্টিং সাথে সাথে ধরে ফেলে।
অপ্রাসঙ্গিক টেস্ট সরিয়ে ফেলুন
একটি ফিচার সম্পূর্ণ সরিয়ে ফেলা হলে, সেই ফিচারের জন্য লেখা টেস্টও সরিয়ে ফেলা উচিত — নাহলে সেগুলো অপ্রয়োজনীয়ভাবে ব্যর্থ হতে থাকবে বা মিথ্যা আশ্বাস দেবে।
দ্রুত ফলাফল, ঘন ঘন চালানো
রিগ্রেশন টেস্টিং তখনই সবচেয়ে কার্যকর যখন এটি ঘন ঘন চালানো যায় (প্রতিটি কমিটে, CI/CD-তে — M12-এ বিস্তারিত) — যা প্রয়োজন করে টেস্ট স্যুট মোটামুটি দ্রুত থাকুক।
মূল কথা · Key takeaway

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

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

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

প্র ০১ কোড সেলে ঠিক কেন test_member_gets_discount ও test_zero_price পুরনো ও নতুন — দুই ভার্সনেই পাস করে, কিন্তু test_non_member_no_discount শুধু পুরনো ভার্সনে পাস করে?

test_member_gets_discount-এ is_member=True, তাই পুরনো ও নতুন — দুই ভার্সনই round(price * 0.9, 2) রিটার্ন করে — একই ফলাফল, তাই পাস। test_zero_price-এ price=0, তাই 0 * 0.9 = 0 — is_member যাই হোক, ফলাফল একই। কিন্তু test_non_member_no_discount-এ is_member=False — পুরনো ভার্সন এই ক্ষেত্রে ছাড় প্রয়োগ করে না (price অপরিবর্তিত রিটার্ন করে), কিন্তু নতুন (রিগ্রেসড) ভার্সন is_member-কে সম্পূর্ণ উপেক্ষা করে সবসময় ছাড় প্রয়োগ করে — এই একটি নির্দিষ্ট আচরণের পার্থক্যই একে একমাত্র ব্যর্থ টেস্টে পরিণত করে।

প্র ০২ যদি এই রিগ্রেশনটি প্রোডাকশনে ধরা পড়তো (টেস্ট স্যুট চালানোর আগেই ডিপ্লয় হয়ে যেতো), তাহলে এর প্রভাব কী হতে পারতো, এবং এটি L03-এর "খরচ" ধারণার সাথে কীভাবে সম্পর্কিত?

প্রতিটি non-member গ্রাহক ভুলভাবে ১০% ছাড় পেতে থাকতো, যার মানে ব্যবসার সরাসরি রাজস্ব ক্ষতি — এবং এটি হয়তো অনেকদিন ধরা নাও পড়তে পারতো যতক্ষণ না কেউ হিসাব মিলিয়ে দেখতো। L03-এ যেমন আলোচনা হয়েছে, প্রোডাকশনে ধরা পড়া একটি বাগ ঠিক করার খরচ (রাজস্ব ক্ষতি + জরুরি হটফিক্স + গ্রাহকের আস্থা) ডেভেলপমেন্ট পর্যায়ে ধরা পড়া একই বাগের তুলনায় বহুগুণ বেশি — যা ঠিক এই কারণেই রিগ্রেশন টেস্টিংকে ডিপ্লয়মেন্টের আগে চালানো এত গুরুত্বপূর্ণ করে তোলে।

প্র ০৩ "পেস্টিসাইড প্যারাডক্স" (M1/L04-এ উল্লেখিত) অনুযায়ী, শুধু পুরনো টেস্ট স্যুট বারবার চালানোই কি যথেষ্ট, নাকি আরও কিছু দরকার?

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

অনুশীলন

  1. চিন্তা করুন: যদি রিগ্রেসড ভার্সনে বাগটি অন্যভাবে থাকতো — যেমন is_member=True হলেও ছাড় প্রয়োগ না করা (উল্টো দিকের বাগ) — তাহলে তিনটি টেস্টের মধ্যে কোনটি(গুলো) এখন ব্যর্থ হতো বলে আপনার মনে হয়?

    এই ক্ষেত্রে test_member_gets_discount ব্যর্থ হতো, কারণ calculate_discount(1000, True) এখন ভুলভাবে 1000 রিটার্ন করতো (ছাড় ছাড়াই), অথচ প্রত্যাশিত মান 900। test_non_member_no_discount এখনও পাস করতো (non-member-দের আচরণ অপরিবর্তিত), আর test_zero_price-ও পাস করতো কারণ 0 মূল্যে ছাড় থাকুক বা না থাকুক ফলাফল সবসময় 0।

  2. পরীক্ষা করুন: কোড সেলে রিগ্রেসড ভার্সনটি return round(price * 0.9, 2) if not is_member else price-এ পরিবর্তন করুন (উপরের প্রশ্নের উল্টো-বাগ) এবং Run চেপে দেখুন সত্যিই test_member_gets_discount ব্যর্থ হয় কি না, ঠিক যেমনটা চিন্তা করেছিলেন।

    হ্যাঁ — এই পরিবর্তনের পর দ্বিতীয় run_suite() কলে দেখা যাবে test_member_gets_discount ব্যর্থ হচ্ছে (calculate_discount(1000, True) এখন 1000 রিটার্ন করে, প্রত্যাশিত 900-এর বদলে), আর বাকি দুটো টেস্ট এখনও পাস করছে — ঠিক যেমনটা মূল ডেমোতে দেখা গিয়েছিলো, শুধু এবার ভিন্ন একটি টেস্ট রিগ্রেশন ধরেছে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Software Engineering Principles & Git কোর্স সহোদর কোর্স সংস্করণ নিয়ন্ত্রণ (Git) ও কমিট-ভিত্তিক ওয়ার্কফ্লোর ভিত্তি সেই কোর্সে কভার করা হয়েছে — প্রতিটি কমিটে রিগ্রেশন টেস্ট চালানোর প্র্যাকটিক্যাল প্রেক্ষাপট সেখান থেকেই আসে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety ও Software Testing & Quality Assurance — সব এক জায়গায়।
আগের পাঠ
সিস্টেম টেস্টিং ও এন্ড-টু-এন্ড টেস্টিং