পাঠ ৪৮ · ৫৭-এর মধ্যে · মডিউল ১১
Home / Courses / Software Testing & Quality Assurance / টেস্ট ম্যানেজমেন্ট ও প্রসেস

ডিফেক্ট/বাগ লাইফসাইকেল

Defect & bug lifecycle
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • একটি সাধারণ ডিফেক্ট/বাগ লাইফসাইকেলের অবস্থাগুলো এবং প্রতিটির অর্থ
  • ALLOWED ট্রানজিশন ডিকশনারি দিয়ে বৈধ/অবৈধ অবস্থা-পরিবর্তন কীভাবে মডেল করতে হয়
  • একটি সত্যিকারের transition(new_state, reason) মেথড যা অবৈধ পরিবর্তন প্রত্যাখ্যান করে
  • একটি বাস্তবসম্মত সিকোয়েন্স চালিয়ে সম্পূর্ণ ট্রানজিশন হিস্টোরি — রিওপেনসহ — সত্যিকারভাবে যাচাই

১ · একটি ডিফেক্টের জীবনচক্র

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

new
বাগ সবেমাত্র রিপোর্ট হয়েছে, এখনো কেউ পর্যালোচনা করেনি।
triaged
QA লিড/টিম যাচাই করেছে এটি সত্যিকারের বাগ কি না, এবং প্রায়োরিটি ঠিক করেছে।
in_progress
একজন ডেভেলপারকে অ্যাসাইন করা হয়েছে, ফিক্সের কাজ চলছে।
fixed
ফিক্স কমিট ও ডিপ্লয় করা হয়েছে (সাধারণত স্টেজিং-এ), কিন্তু এখনো QA দ্বারা যাচাই হয়নি।
verified
QA ফিক্সটি স্বতন্ত্রভাবে পরীক্ষা করে নিশ্চিত করেছে এটি সত্যিই কাজ করছে।
closed
রিলিজে অন্তর্ভুক্ত হয়েছে, বাগটি চূড়ান্তভাবে বন্ধ।

কিন্তু বাস্তবে ফিক্স সবসময় প্রথমবারেই কাজ করে না — প্রোডাকশনে গিয়ে দেখা যায় একই বাগ (বা এর একটি অংশ) এখনো আছে। তখন দরকার হয় একটি reopened পথ — verified বা closed থেকে আবার in_progress-এ ফিরিয়ে নেওয়ার।

new triaged in_progress fixed verified closed reopened — ফিক্স কাজ করেনি, verified থেকেও এই পথ সম্ভব
সবুজ তীর হলো "স্বাভাবিক" পথ (happy path); লাল ড্যাশড তীর হলো reopened পথ — verified বা closed থেকে আবার in_progress-এ ফিরে যাওয়া।

২ · স্টেট মেশিন দিয়ে মডেল করা

এই ধরনের "নির্দিষ্ট কিছু অবস্থা, নির্দিষ্ট কিছু বৈধ পরিবর্তন" আচরণ মডেল করার সবচেয়ে পরিষ্কার উপায় একটি স্টেট মেশিন — একটি ALLOWED ডিকশনারি যেখানে প্রতিটি কী (key) একটি বর্তমান অবস্থা, আর মান (value) হলো সেখান থেকে যেসব অবস্থায় বৈধভাবে যাওয়া যায় তার একটি সেট। একটি transition(new_state, reason) মেথড অনুরোধ করা পরিবর্তনটি এই ডিকশনারির বিপরীতে যাচাই করে — বৈধ হলে অবস্থা পরিবর্তন করে ও কারণ লগ করে, অবৈধ হলে এরর তোলে।

Python
# বর্তমান অবস্থা থেকে যেসব অবস্থায় বৈধভাবে যাওয়া যায়
ALLOWED = {
    "new":         {"triaged"},
    "triaged":     {"in_progress"},
    "in_progress": {"fixed"},
    "fixed":       {"verified"},
    "verified":    {"closed", "reopened"},
    "closed":      {"reopened"},
    "reopened":    {"in_progress"},
}

class Defect:
    def __init__(self, defect_id):
        self.defect_id = defect_id
        self.state = "new"
        self.history = [("new", "ডিফেক্ট রিপোর্ট করা হয়েছে")]

    def transition(self, new_state, reason):
        allowed = ALLOWED.get(self.state, set())
        if new_state not in allowed:
            raise ValueError(
                f"অবৈধ ট্রানজিশন: '{self.state}' থেকে '{new_state}'-এ যাওয়া যায় না "
                f"(অনুমোদিত: {sorted(allowed) if allowed else 'কোনোটিই না'})"
            )
        self.state = new_state
        self.history.append((new_state, reason))

    def __repr__(self):
        return f"Defect({self.defect_id}, state={self.state!r})"

    

৩ · একটি বাস্তবসম্মত সিকোয়েন্স চালিয়ে দেখা

এখন উপরের Defect ক্লাস ব্যবহার করে একটি সম্পূর্ণ, বাস্তবসম্মত জীবনচক্র চালিয়ে দেখা যাক — স্বাভাবিক পথ দিয়ে closed পর্যন্ত যাওয়া, তারপর প্রোডাকশনে একই বাগ আবার দেখা দেওয়ায় reopened হয়ে আবার পুরো চক্র সম্পন্ন করা। মাঝখানে একটি ইচ্ছাকৃতভাবে অবৈধ ট্রানজিশনও চেষ্টা করা হয়েছে — verified অবস্থা থেকে সরাসরি in_progress-এ যাওয়ার চেষ্টা, যা ALLOWED অনুযায়ী বৈধ নয় (শুধু reopened হয়ে তারপর in_progress-এ যাওয়া যায়, সরাসরি নয়)।

Python
bug = Defect("BUG-1042")

bug.transition("triaged", "QA লিড যাচাই করে ট্রায়াজ করেছেন, প্রায়োরিটি: হাই")
bug.transition("in_progress", "ডেভেলপারকে অ্যাসাইন করা হয়েছে, কাজ শুরু")
bug.transition("fixed", "ফিক্স কমিট ও স্টেজিং-এ ডিপ্লয় করা হয়েছে")
bug.transition("verified", "QA স্টেজিং-এ ফিক্স যাচাই করেছে, সঠিক আচরণ করছে")

# ইচ্ছাকৃতভাবে অবৈধ ট্রানজিশন — verified থেকে সরাসরি in_progress-এ যাওয়ার চেষ্টা
try:
    bug.transition("in_progress", "ভুলবশত সরাসরি in_progress-এ ফেরানোর চেষ্টা")
except ValueError as e:
    print("প্রত্যাশিতভাবে প্রত্যাখ্যাত:", e)

bug.transition("closed", "রিলিজ ২.৪-এ অন্তর্ভুক্ত হয়েছে, বন্ধ করা হলো")

# --- প্রোডাকশনে একই বাগ আবার দেখা গেছে ---
bug.transition("reopened", "প্রোডাকশনে একই সমস্যা আবার রিপোর্ট হয়েছে")
bug.transition("in_progress", "ডেভেলপার আবার কাজ শুরু করেছেন, রুট কজ ভিন্ন ছিল")
bug.transition("fixed", "নতুন, সম্পূর্ণ ফিক্স কমিট করা হয়েছে")
bug.transition("verified", "QA আবার যাচাই করেছে, এবার সত্যিই ঠিক আছে")
bug.transition("closed", "চূড়ান্তভাবে বন্ধ করা হলো")

print(f"\n{bug.defect_id}-এর চূড়ান্ত অবস্থা: {bug.state}")
print("সম্পূর্ণ ট্রানজিশন হিস্টোরি:")
for i, (state, reason) in enumerate(bug.history, start=1):
    print(f"  {i}. [{state}] {reason}")

    
লক্ষ্য করুন আউটপুটে প্রত্যাখ্যাত ট্রানজিশনের চেষ্টাটি (verified → in_progress) history-তে যোগ হয়নি — কারণ transition() মেথড ভ্যালিডেশন ব্যর্থ হলে self.state পরিবর্তন করার আগেই ValueError তোলে। চূড়ান্ত হিস্টোরিতে মোট ১১টি এন্ট্রি দেখা যাবে — প্রথম new এন্ট্রি থেকে শুরু করে দুইবার সম্পূর্ণ চক্র (একটি স্বাভাবিক, একটি রিওপেনের পর) সম্পন্ন হওয়া পর্যন্ত — প্রতিটি ধাপ ALLOWED ডিকশনারি অনুযায়ী সত্যিই বৈধ ছিল বলেই সফল হয়েছে।
এটি M2/L08-এর স্টেট ট্রানজিশন টেস্টিং-এর সাথে সম্পর্কিত

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

মূল কথা · Key takeaway

একটি ডিফেক্ট এলোমেলোভাবে যেকোনো অবস্থায় যেতে পারে না — একটি স্পষ্ট ALLOWED ট্রানজিশন ডিকশনারি এই নিয়মগুলো কোডে এনফোর্স করে, ভুল/অসম্ভব পরিবর্তন ঠেকিয়ে দেয় (যেমন সরাসরি new থেকে closed-এ চলে যাওয়া), এবং reopened পথ বাস্তবতাকে প্রতিফলিত করে — ফিক্স সবসময় প্রথমবারেই কাজ করে না। বাস্তব JIRA/Bugzilla-এর মতো টুল ঠিক এই ধরনের ওয়ার্কফ্লো ইঞ্জিন ব্যবহার করে, যদিও তাদের নির্দিষ্ট অবস্থার নাম ও পথ ভিন্ন হতে পারে।

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

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

প্র ০১ ALLOWED ডিকশনারিতে "new": {"triaged"} আছে, কিন্তু "new": {"triaged", "closed"} নেই — এই সীমাবদ্ধতা কেন গুরুত্বপূর্ণ?

যদি new থেকে সরাসরি closed-এ যাওয়া বৈধ হতো, তাহলে কেউ ট্রায়াজ বা ফিক্স ছাড়াই একটি বাগ "বন্ধ" করে দিতে পারতেন — যা ট্র্যাকিং-এর পুরো উদ্দেশ্যকেই নষ্ট করে দেয়। ইচ্ছাকৃতভাবে বাতিল করা বাগের জন্য (যেমন ডুপ্লিকেট বা "বাগ নয়") বাস্তব টুলে সাধারণত আলাদা একটি rejected/wont_fix পথ থাকে, একই closed নয়।

প্র ০২ উপরের কোডে verified থেকে সরাসরি in_progress-এ যাওয়ার চেষ্টা কেন প্রত্যাখ্যাত হলো, অথচ reopened থেকে in_progress-এ যাওয়া বৈধ?

কারণ ALLOWED["verified"] = {"closed", "reopened"} — এতে in_progress নেই। এই ডিজাইনের উদ্দেশ্য হলো, "ফিক্স আবার কাজ শুরু হচ্ছে" এই ঘটনাটি স্পষ্টভাবে reopened অবস্থা হিসেবে লগ হোক, যাতে হিস্টোরি দেখেই বোঝা যায় এটি একটি রিগ্রেশন/রিওপেন কেস — সরাসরি in_progress-এ ফিরে গেলে সেই গুরুত্বপূর্ণ তথ্যটি হিস্টোরি থেকে হারিয়ে যেত।

প্র ০৩ কেন transition() মেথডে ভ্যালিডেশন ব্যর্থ হলে self.state পরিবর্তন করার আগেই এরর তোলা জরুরি, পরে তোলা নয়?

যদি self.state আগে সেট করে তারপর যাচাই করা হতো, তাহলে একটি ব্যর্থ ভ্যালিডেশনও অবস্থা পরিবর্তন করে ফেলতে পারত (অথবা এরর হ্যান্ডলিং-এ ভুল হলে অসামঞ্জস্যপূর্ণ অবস্থা থেকে যেত) — অবজেক্টটি এমন একটি অবস্থায় চলে যেত যা ALLOWED অনুযায়ী কখনোই পৌঁছানো উচিত ছিল না। আগে যাচাই, পরে পরিবর্তন — এই ক্রম নিশ্চিত করে ব্যর্থ চেষ্টা অবজেক্টের প্রকৃত অবস্থাকে স্পর্শই করে না।

অনুশীলন

  1. চিন্তা করুন: আপনি যদি এই স্টেট মেশিনে একটি rejected অবস্থা (ডুপ্লিকেট বা "বাগ নয়" হিসেবে চিহ্নিত হলে) যোগ করতে চান, ALLOWED-এ ঠিক কোথায় কোথায় এন্ট্রি যোগ/পরিবর্তন করতে হবে?

    অন্তত দুটো পরিবর্তন দরকার: ALLOWED["triaged"]-কে {"in_progress", "rejected"}-এ পরিবর্তন করা (ট্রায়াজের সময়ই বোঝা যায় এটি বাগ নয় কি না), এবং ALLOWED["rejected"] = set() যোগ করা (একবার প্রত্যাখ্যাত হলে সেখান থেকে আর কোথাও যাওয়া যাবে না, একটি "টার্মিনাল" অবস্থা হিসেবে) — যদি না আপনি কাউকে ভুল প্রত্যাখ্যান চ্যালেঞ্জ করে আবার triaged-এ ফেরানোর সুযোগও দিতে চান।

  2. পরীক্ষা করুন: উপরের দ্বিতীয় কোড সেলে bug.transition("closed", "...")-এর ঠিক পরে bug.transition("triaged", "ভুল চেষ্টা") যোগ করে Run চেপে দেখুন কী হয়।

    এটি একটি ValueError তুলবে, কারণ ALLOWED["closed"] = {"reopened"} — এতে triaged নেই। যদি এই কলটি try/except-এ না জড়ানো হয়, পুরো কোড সেলের বাকি অংশ (রিওপেন সিকোয়েন্সসহ) আর চলবে না, কারণ এরর প্রোগ্রামটিকে থামিয়ে দেবে — এটিই দেখায় কেন বাস্তব কোডে এই ধরনের ইচ্ছাকৃত-অবৈধ কল সবসময় try/except-এ ঘিরে রাখা হয়।

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

আগের পাঠ
টেস্ট কেস ডিজাইন ও ম্যানেজমেন্ট