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

স্টেট ট্রানজিশন টেস্টিং

State transition testing
৯ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • স্টেট ট্রানজিশন টেস্টিং কখন প্রাসঙ্গিক (যখন আচরণ ইতিহাস/অবস্থা-নির্ভর)
  • একটি ছোট স্টেট মেশিনকে ALLOWED ট্রানজিশন ডিকশনারি দিয়ে মডেল করা
  • এমন একটি টেস্ট সিকোয়েন্স ডিজাইন করা যা সবগুলো বৈধ ট্রানজিশন কভার করে
  • Python-এর unittest দিয়ে বৈধ ট্রানজিশন সিকোয়েন্স ও একটি অবৈধ ট্রানজিশন প্রত্যাখ্যান — উভয়ই যাচাই করা

১ · স্টেট ট্রানজিশন টেস্টিং কী

স্টেট ট্রানজিশন টেস্টিংState Transition Testingএকটি সিস্টেমকে সীমিত সংখ্যক "অবস্থা" (state) ও তাদের মধ্যে বৈধ "ট্রানজিশন" হিসেবে মডেল করে, তারপর প্রতিটি বৈধ ট্রানজিশন অন্তত একবার ঘটিয়ে এবং অবৈধ ট্রানজিশন প্রত্যাখ্যাত হয় কি না যাচাই করে নিয়মতান্ত্রিকভাবে টেস্ট করা। একটি অর্ডারের কথা চিন্তা করুন — এটি placed (তৈরি হয়েছে) দিয়ে শুরু হয়ে shipped (পাঠানো হয়েছে) হয়ে delivered (পৌঁছেছে)-এ যায়। এছাড়া placed বা shipped থেকে সরাসরি cancelled (বাতিল) হওয়া যায়, কিন্তু একবার delivered বা cancelled হয়ে গেলে আর কোনো ট্রানজিশন সম্ভব নয়। ইনপুট নিজে থেকে যথেষ্ট নয় এখানে — "cancelled" এ যাওয়া চেষ্টা বৈধ কি না তা নির্ভর করে অর্ডারটি এখন কোন স্টেটে আছে তার উপর।

placed shipped delivered cancelled
চারটি স্টেট, চারটি বৈধ ট্রানজিশন: placed→shipped, shipped→delivered, placed→cancelled, shipped→cancelled। delivered ও cancelled থেকে আর কোনো ট্রানজিশন নেই।

২ · কোড সেলে সম্পূর্ণ কভারেজ ডিজাইন করা

একটি ভালো টেস্ট সিকোয়েন্সের লক্ষ্য হলো চারটি বৈধ ট্রানজিশনই অন্তত একবার ঘটানো — শুধু একটি "হ্যাপি পাথ" (placed → shipped → delivered) টেস্ট করলে cancelled-এর দুটো পথ কখনোই যাচাই হতো না। নিচের কোডে তিনটি ভিন্ন অর্ডার দিয়ে চারটি ট্রানজিশনই কভার করা হয়েছে, এবং কভারেজ সত্যিকারভাবে গণনা করা হয়েছে।

Python
import unittest

ALLOWED = {
    "placed":    {"shipped", "cancelled"},
    "shipped":   {"delivered", "cancelled"},
    "delivered": set(),
    "cancelled": set(),
}

class Order:
    def __init__(self):
        self.state = "placed"

    def transition(self, new_state):
        if new_state not in ALLOWED[self.state]:
            raise ValueError(f"অবৈধ ট্রানজিশন: {self.state} -> {new_state}")
        self.state = new_state
        return self.state

# স্টেট মেশিনে সংজ্ঞায়িত সবগুলো বৈধ ট্রানজিশনের সেট -- কভারেজ যাচাইয়ের জন্য
ALL_VALID_TRANSITIONS = {(src, dst) for src, dests in ALLOWED.items() for dst in dests}

def do_transition(order, dest):
    src = order.state
    order.transition(dest)
    return (src, dest)

exercised = set()
o1 = Order()
exercised.add(do_transition(o1, "shipped"))
exercised.add(do_transition(o1, "delivered"))
o2 = Order()
exercised.add(do_transition(o2, "cancelled"))
o3 = Order()
exercised.add(do_transition(o3, "shipped"))
exercised.add(do_transition(o3, "cancelled"))

coverage_pct = len(exercised & ALL_VALID_TRANSITIONS) / len(ALL_VALID_TRANSITIONS) * 100
print(f"মোট বৈধ ট্রানজিশন: {len(ALL_VALID_TRANSITIONS)}")
print(f"টেস্ট সিকোয়েন্সে যাচাইকৃত ট্রানজিশন: {sorted(exercised)}")
print(f"স্টেট ট্রানজিশন কভারেজ: {coverage_pct:.0f}%")

class OrderStateTransitionTests(unittest.TestCase):
    def test_full_delivery_path(self):
        order = Order()
        self.assertEqual(order.transition("shipped"), "shipped")
        self.assertEqual(order.transition("delivered"), "delivered")

    def test_cancel_from_placed(self):
        order = Order()
        self.assertEqual(order.transition("cancelled"), "cancelled")

    def test_cancel_from_shipped(self):
        order = Order()
        order.transition("shipped")
        self.assertEqual(order.transition("cancelled"), "cancelled")

    def test_invalid_transition_rejected(self):
        order = Order()
        order.transition("shipped")
        order.transition("delivered")
        with self.assertRaises(ValueError):
            order.transition("shipped")  # delivered থেকে আর কোনো ট্রানজিশন বৈধ নয়

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

    
কভারেজ গণনায় দেখা যায় exercised সেটটি ঠিক ALL_VALID_TRANSITIONS-এর সমান — অর্থাৎ চারটি বৈধ ট্রানজিশনই অন্তত একবার সিকোয়েন্সে ঘটেছে, কভারেজ ১০০%। এরপর test_invalid_transition_rejected নিশ্চিত করে delivered স্টেট থেকে shipped-এ ফিরে যাওয়ার চেষ্টা সঠিকভাবে ValueError রেইজ করে প্রত্যাখ্যাত হয় — চারটি unittest টেস্টই পাস করে, কারণ এই Order ক্লাসটি স্পেসিফিকেশন অনুযায়ী সঠিকভাবেই লেখা।
মূল কথা · Key takeaway

স্টেট ট্রানজিশন টেস্টিং শুধু "কী ইনপুট দিলাম" নয়, "সিস্টেম এখন কোথায় আছে" প্রশ্নটিও যোগ করে। একটি ALLOWED ট্রানজিশন ডিকশনারি স্পেসিফিকেশনকে কোডে সরাসরি প্রতিফলিত করে, আর সবগুলো বৈধ ট্রানজিশন অন্তত একবার কভার করা প্লাস অন্তত একটি অবৈধ ট্রানজিশন প্রত্যাখ্যান যাচাই করাই একটি ন্যূনতম-শক্তিশালী স্টেট ট্রানজিশন টেস্ট সেট তৈরি করে।

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

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

প্র ০১ যদি টেস্ট সিকোয়েন্সে শুধু test_full_delivery_path রাখা হতো (বাকি টেস্টগুলো বাদ দিয়ে), কভারেজ কত শতাংশ হতো?

50% — কারণ শুধু placed→shipped আর shipped→delivered এই দুটো ট্রানজিশন ঘটত, মোট চারটির মধ্যে। placed→cancelled ও shipped→cancelled পথ দুটো একেবারেই অযাচাইকৃত থেকে যেত — যদি সেই কোডে বাগ থাকত, তা কখনোই ধরা পড়ত না।

প্র ০২ ALLOWED["delivered"] আর ALLOWED["cancelled"] খালি সেট (set()) কেন — এর মানে কী?

এর মানে delivered ও cancelled হলো টার্মিনাল স্টেট — একবার এই দুটোর কোনো একটাতে পৌঁছালে, সিস্টেমের নিয়ম অনুযায়ী আর কোনো ট্রানজিশন সম্ভব নয়। খালি সেট মানে "এই স্টেট থেকে কোথাও যাওয়া বৈধ নয়", তাই transition()-এর যেকোনো চেষ্টা স্বয়ংক্রিয়ভাবে ValueError রেইজ করবে।

প্র ০৩ বাস্তব জীবনে এমন কোন কোন সিস্টেমে স্টেট ট্রানজিশন টেস্টিং স্বাভাবিকভাবেই প্রযোজ্য বলে মনে হয়?

যেকোনো সিস্টেম যেখানে একটি এনটিটি সুনির্দিষ্ট কয়েকটি "অবস্থা"-র মধ্য দিয়ে যায় — যেমন একটি লগইন সেশন (লগড-আউট → লগড-ইন → এক্সপায়ার্ড), একটি বাগ-রিপোর্ট (M11/L48-এ বিস্তারিত), একটি পেমেন্ট (pending → authorized → captured/failed), বা একটি অ্যাপের লাইফসাইকেল। যেকোনো জায়গায় "আগে কী হয়েছিল" ভবিষ্যতের বৈধ পদক্ষেপ ঠিক করে দেয়, সেখানেই এই টেকনিক প্রাসঙ্গিক।

অনুশীলন

  1. চিন্তা করুন: যদি স্পেসিফিকেশনে নতুন একটি নিয়ম যোগ হয় — delivered থেকে returned (রিটার্ন করা) স্টেটে যাওয়া যাবে — তাহলে ALLOWED ডিকশনারিতে কী পরিবর্তন করতে হবে, এবং টেস্ট সিকোয়েন্সে কী যোগ করতে হবে?

    ALLOWED["delivered"]-কে set()-এর বদলে {"returned"} করতে হবে, এবং ALLOWED["returned"] = set() (যদি এটিও টার্মিনাল হয়) যোগ করতে হবে। তারপর একটি নতুন টেস্ট মেথড লিখতে হবে যা placed→shipped→delivered→returned সিকোয়েন্স চালিয়ে এই নতুন ট্রানজিশনটিও কভারেজে যোগ করে।

  2. পরীক্ষা করুন: উপরের কোড সেলে o2 = Order() ও তার do_transition(o2, "cancelled") লাইন দুটো মুছে ফেলে Run চেপে দেখুন কভারেজ শতাংশ কীভাবে বদলায়।

    কভারেজ 100% থেকে 75%-এ নেমে যাবে (৪টির মধ্যে ৩টি ট্রানজিশন কভার হবে), কারণ placed→cancelled পথটি আর কোনো অর্ডার দিয়ে যাচাই হবে না — o3-এর shipped→cancelled পথটি অবশ্য থেকে যাবে।

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

আগের পাঠ
ডিসিশন টেবিল টেস্টিং