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

চূড়ান্ত প্রকল্প — একটি সম্পূর্ণ টেস্ট স্যুট ডিজাইন ও ইমপ্লিমেন্ট করা

Capstone — designing and implementing a complete test suite
২৫ মিনিট পড়া উন্নত · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • একটি ছোট বাস্তব সিস্টেমের জন্য শূন্য থেকে একটি সম্পূর্ণ টেস্ট স্যুট কীভাবে ধাপে ধাপে বানানো হয়
  • ইউনিট টেস্ট, টেস্ট ডাবল, বাউন্ডারি/ইকুইভ্যালেন্স টেস্ট, কভারেজ পরিমাপ, ও মিউটেশন টেস্টিং কীভাবে একসাথে কাজ করে একটি স্যুটের প্রকৃত শক্তি তৈরি করে
  • কীভাবে একটি "মিউট্যান্ট" (ইচ্ছাকৃতভাবে ইনজেক্ট করা বাগ) ব্যবহার করে যাচাই করা যায় যে একটি টেস্ট স্যুট সত্যিই কার্যকর, শুধু "পাস করছে" তা নয়
  • একাধিক প্রকৃত ফলাফল একত্রিত করে কীভাবে একটি অর্থবহ চূড়ান্ত কোয়ালিটি সারাংশ তৈরি করা যায়

১ · সিস্টেম-আন্ডার-টেস্ট: একটি ছোট শপিং কার্ট মডিউল

এই ক্যাপস্টোনের বিষয়বস্তু একটি ছোট, স্ব-সম্পূর্ণ (হাইপোথেটিক্যাল) শপিং কার্ট মডিউল, চারটি ফাংশন নিয়ে গঠিত: add_item(cart, item, price, qty) কার্টে একটি আইটেম যোগ করে, apply_coupon(cart, coupon_code) একটি কুপন কোড সংযুক্ত করে, calculate_total(cart) কুপনের টায়ার্ড ডিসকাউন্ট নিয়ম প্রয়োগ করে চূড়ান্ত মূল্য গণনা করে, আর checkout(cart, payment_gateway) একটি বাহ্যিক পেমেন্ট-গেটওয়ে ডিপেন্ডেন্সিকে কল করে চেকআউট সম্পন্ন করে। SAVE10 কুপনের নিয়ম: সাবটোটাল ৫০০০ বা তার বেশি হলে ১৫% ছাড়, ২০০০-৪৯৯৯ হলে ১০%, ৫০০-১৯৯৯ হলে ৫%, তার নিচে কোনো ছাড় নেই।

নিচে ছয়টি মডিউলে ক্রমান্বয়ে এই সিস্টেমের জন্য একটি সম্পূর্ণ টেস্ট স্যুট বানানো হচ্ছে — প্রতিটি কোড সেল আগের সেলগুলোর ওপর নির্ভর করে (একই পেজে ক্রমানুসারে Run করুন), ঠিক যেমন একটি বাস্তব প্রজেক্টে টেস্ট স্যুট ধাপে ধাপে গড়ে ওঠে।

মডিউল ১-৩
ইউনিট টেস্ট, মক টেস্ট ডাবল, বাউন্ডারি/ইকুইভ্যালেন্স টেস্ট — তিনটি real TestCase ক্লাস।
মডিউল ৪
sys.settrace() দিয়ে calculate_total-এর স্টেটমেন্ট কভারেজ পরিমাপ।
মডিউল ৫
একটি বাউন্ডারি-বাগ মিউট্যান্ট ইনজেক্ট করে টেস্ট স্যুট সেটি ধরে কি না তা যাচাই।
মডিউল ৬
আগের সব ধাপের প্রকৃত ফলাফল একত্রিত করে চূড়ান্ত কোয়ালিটি সারাংশ।

২ · মডিউল ১ — কোর ফাংশন + ইউনিট টেস্ট (L10-L14-এর ধাঁচে)

প্রথমে add_item, apply_coupon, ও calculate_total ফাংশন সংজ্ঞায়িত করা হচ্ছে, এবং সাথে সাথেই real unittest.TestCase দিয়ে পাঁচটি ইউনিট টেস্ট — কোনো কুপন ছাড়া, একাধিক আইটেম নিয়ে, ও কুপনের বিভিন্ন টায়ারে। এই কোর্সের CLAUDE.md নিয়ম অনুযায়ী বেয়ার unittest.main() ব্যবহার করা হচ্ছে না — বরং একটি স্যুট বানিয়ে TextTestRunner দিয়ে চালানো হচ্ছে।

Python
# ========== মডিউল ১: shopping_cart কোর ফাংশন + ইউনিট টেস্ট ==========
import unittest

def new_cart():
    return {"items": [], "coupon": None}

def add_item(cart, item, price, qty=1):
    cart["items"].append({"item": item, "price": price, "qty": qty})
    return cart

def apply_coupon(cart, coupon_code):
    cart["coupon"] = coupon_code
    return cart

def calculate_total(cart):
    subtotal = sum(i["price"] * i["qty"] for i in cart["items"])
    coupon = cart.get("coupon")
    rate = 0.0
    if coupon == "SAVE10":
        if subtotal >= 5000:
            rate = 0.15
        elif subtotal >= 2000:
            rate = 0.10
        elif subtotal >= 500:
            rate = 0.05
    return round(subtotal * (1 - rate), 2)


class ShoppingCartCoreTests(unittest.TestCase):
    def test_add_single_item(self):
        cart = new_cart()
        add_item(cart, "Bluetooth Speaker", 1500, 1)
        self.assertEqual(calculate_total(cart), 1500)

    def test_add_multiple_items_multiplies_qty(self):
        cart = new_cart()
        add_item(cart, "Notebook", 60, 3)
        add_item(cart, "Pen", 20, 2)
        # 60*3 + 20*2 = 180 + 40 = 220
        self.assertEqual(calculate_total(cart), 220)

    def test_apply_coupon_below_threshold_no_discount(self):
        cart = new_cart()
        add_item(cart, "Hair Oil", 280, 1)
        apply_coupon(cart, "SAVE10")
        self.assertEqual(calculate_total(cart), 280)

    def test_apply_coupon_mid_tier_discount(self):
        cart = new_cart()
        add_item(cart, "Rice Cooker", 3200, 1)
        apply_coupon(cart, "SAVE10")
        # 3200 * (1 - 0.10) = 2880.0
        self.assertEqual(calculate_total(cart), 2880.0)

    def test_invalid_coupon_code_ignored(self):
        cart = new_cart()
        add_item(cart, "Rice Cooker", 3200, 1)
        apply_coupon(cart, "NOT-A-REAL-CODE")
        self.assertEqual(calculate_total(cart), 3200)


def make_suite1():
    # প্রতিবার একটি নতুন TestSuite তৈরি করা হচ্ছে -- unittest.TestSuite.run() ডিফল্টভাবে
    # প্রতিটি টেস্ট চলার পর তার রেফারেন্স None দিয়ে বদলে দেয় (মেমরি বাঁচাতে), তাই একই suite
    # instance দ্বিতীয়বার .run() করলে "'NoneType' object is not callable" এরর দেয়। এই
    # ফাংশনটি পরের মডিউলগুলোতেও (কভারেজ, মিউটেশন, চূড়ান্ত সারাংশ) পুনরায় কল করা হবে যাতে
    # প্রতিবারই একটি genuinely তাজা suite পাওয়া যায়।
    return unittest.TestLoader().loadTestsFromTestCase(ShoppingCartCoreTests)

unittest.TextTestRunner(verbosity=2).run(make_suite1())

    
পাঁচটি টেস্টই পাস করে — লক্ষ্য করুন calculate_total-এর ডিসকাউন্ট লজিক তিনটি স্বাধীন if/elif শাখায় লেখা (কোনো বেয়ার else ছাড়াই), যা পরে মডিউল ৪-এ লাইন-কভারেজ পরিমাপকে নির্ভরযোগ্য করে তুলবে।

৩ · মডিউল ২ — checkout() + পেমেন্ট-গেটওয়ে টেস্ট ডাবল (L15-L17-এর ধাঁচে)

এখন checkout() ফাংশন যোগ করা হচ্ছে, যা একটি বাহ্যিক payment_gateway ডিপেন্ডেন্সিকে কল করে। বাস্তব পেমেন্ট গেটওয়ে টেস্টে ব্যবহার করা অব্যবহারিক (ধীর, খরচযুক্ত, অনির্ভরযোগ্য) — তাই real unittest.mock.Mock দিয়ে একটি টেস্ট ডাবল বানিয়ে যাচাই করা হচ্ছে checkout() গেটওয়েকে সঠিক পরিমাণ দিয়ে সঠিকভাবে কল করছে কি না, এবং খালি কার্টে গেটওয়েকে কখনোই কল করছে না তাও যাচাই করা হচ্ছে।

Python
# ========== মডিউল ২: checkout() + payment gateway টেস্ট ডাবল (unittest.mock) ==========
from unittest.mock import Mock

def checkout(cart, payment_gateway):
    total = calculate_total(cart)
    if total <= 0:
        raise ValueError("খালি বা শূন্য-মূল্যের কার্ট চেকআউট করা যাবে না")
    result = payment_gateway.charge(total)
    if result.get("status") != "success":
        raise RuntimeError("পেমেন্ট ব্যর্থ হয়েছে")
    return {
        "status": "confirmed",
        "amount_charged": total,
        "transaction_id": result.get("transaction_id"),
    }


class CheckoutMockTests(unittest.TestCase):
    def test_checkout_calls_gateway_with_correct_amount(self):
        cart = new_cart()
        add_item(cart, "Electric Kettle", 1200, 1)
        mock_gateway = Mock()
        mock_gateway.charge.return_value = {"status": "success", "transaction_id": "TXN-001"}

        result = checkout(cart, mock_gateway)

        mock_gateway.charge.assert_called_once_with(1200)
        self.assertEqual(result["status"], "confirmed")
        self.assertEqual(result["transaction_id"], "TXN-001")

    def test_checkout_raises_when_gateway_declines(self):
        cart = new_cart()
        add_item(cart, "Electric Kettle", 1200, 1)
        mock_gateway = Mock()
        mock_gateway.charge.return_value = {"status": "declined"}

        with self.assertRaises(RuntimeError):
            checkout(cart, mock_gateway)

    def test_checkout_rejects_empty_cart_without_calling_gateway(self):
        cart = new_cart()
        mock_gateway = Mock()

        with self.assertRaises(ValueError):
            checkout(cart, mock_gateway)

        mock_gateway.charge.assert_not_called()


def make_suite2():
    # (একই কারণে -- দেখুন মডিউল ১-এর make_suite1()-এর কমেন্ট) প্রতিবার একটি নতুন suite তৈরি করা হচ্ছে
    return unittest.TestLoader().loadTestsFromTestCase(CheckoutMockTests)

unittest.TextTestRunner(verbosity=2).run(make_suite2())

    
তিনটি টেস্টই পাস করে। assert_called_once_with(1200) সত্যিকারভাবে যাচাই করে গেটওয়ে ঠিক একবার, ঠিক ১২০০ টাকা দিয়ে কল হয়েছে — আর assert_not_called() নিশ্চিত করে খালি কার্টে গেটওয়ে একেবারেই স্পর্শ করা হয়নি। এটিই M4-এ শেখা "ইন্টারঅ্যাকশন ভেরিফিকেশন"-এর প্রকৃত প্রয়োগ।

৪ · মডিউল ৩ — কুপন-ডিসকাউন্টের বাউন্ডারি ও ইকুইভ্যালেন্স টেস্ট (L05-L06-এর ধাঁচে)

SAVE10-এর তিনটি থ্রেশহোল্ড (৫০০, ২০০০, ৫০০০) হলো ঠিক সেই জায়গা যেখানে অফ-বাই-ওয়ান বাগ লুকিয়ে থাকার সম্ভাবনা সবচেয়ে বেশি (L01-এর calculate_shipping_cost-এর বাউন্ডারি বাগের একই স্পিরিট)। নিচে প্রতিটি থ্রেশহোল্ডের ঠিক নিচে/উপরে/ঠিক তাতে, এবং প্রতিটি রেঞ্জ থেকে একটি প্রতিনিধি (ইকুইভ্যালেন্স) মান নিয়ে একটি একক টেস্ট মেথডে self.subTest(...) ব্যবহার করে ১২টি কেস যাচাই করা হচ্ছে।

Python
# ========== মডিউল ৩: কুপন ডিসকাউন্ট -- বাউন্ডারি ও ইকুইভ্যালেন্স টেস্ট ==========
BOUNDARY_AND_EQUIVALENCE_CASES = [
    # (subtotal, expected_discount_rate, description)
    (250,  0.00, "ইকুইভ্যালেন্স: প্রথম রেঞ্জ থেকে প্রতিনিধি মান"),
    (499,  0.00, "বাউন্ডারি: প্রথম থ্রেশহোল্ডের ঠিক নিচে"),
    (500,  0.05, "বাউন্ডারি: প্রথম থ্রেশহোল্ডে ঠিক"),
    (501,  0.05, "বাউন্ডারি: প্রথম থ্রেশহোল্ডের ঠিক উপরে"),
    (1250, 0.05, "ইকুইভ্যালেন্স: দ্বিতীয় রেঞ্জ থেকে প্রতিনিধি মান"),
    (1999, 0.05, "বাউন্ডারি: দ্বিতীয় থ্রেশহোল্ডের ঠিক নিচে"),
    (2000, 0.10, "বাউন্ডারি: দ্বিতীয় থ্রেশহোল্ডে ঠিক"),
    (2001, 0.10, "বাউন্ডারি: দ্বিতীয় থ্রেশহোল্ডের ঠিক উপরে"),
    (3500, 0.10, "ইকুইভ্যালেন্স: তৃতীয় রেঞ্জ থেকে প্রতিনিধি মান"),
    (4999, 0.10, "বাউন্ডারি: তৃতীয় থ্রেশহোল্ডের ঠিক নিচে"),
    (5000, 0.15, "বাউন্ডারি: তৃতীয় থ্রেশহোল্ডে ঠিক"),
    (5001, 0.15, "বাউন্ডারি: তৃতীয় থ্রেশহোল্ডের ঠিক উপরে"),
]

class CouponBoundaryTests(unittest.TestCase):
    def test_discount_rate_across_boundaries_and_partitions(self):
        for subtotal, expected_rate, description in BOUNDARY_AND_EQUIVALENCE_CASES:
            with self.subTest(subtotal=subtotal, desc=description):
                cart = new_cart()
                add_item(cart, "BoundaryTestItem", subtotal, 1)
                apply_coupon(cart, "SAVE10")
                expected_total = round(subtotal * (1 - expected_rate), 2)
                self.assertEqual(
                    calculate_total(cart), expected_total,
                    f"{description}: subtotal={subtotal} হলে {expected_rate*100:.0f}% ছাড় প্রত্যাশিত ছিল",
                )


def make_suite3():
    # (একই কারণে -- দেখুন মডিউল ১-এর make_suite1()-এর কমেন্ট) প্রতিবার একটি নতুন suite তৈরি করা হচ্ছে
    return unittest.TestLoader().loadTestsFromTestCase(CouponBoundaryTests)

unittest.TextTestRunner(verbosity=2).run(make_suite3())

    
সবগুলো সাব-টেস্ট পাস করে — বর্তমান calculate_total প্রতিটি থ্রেশহোল্ডে সঠিকভাবে >= ব্যবহার করছে। এই ১২টি কেস মডিউল ৪ ও ৫-এ গুরুত্বপূর্ণ ভূমিকা রাখবে — এগুলোই সেই টেস্ট যা প্রতিটি ডিসকাউন্ট শাখাকে অন্তত একবার চালায়, এবং পরে ঠিক এই বাউন্ডারিতেই একটি ইনজেক্ট করা বাগ ধরে ফেলবে।

৫ · মডিউল ৪ — calculate_total-এর কভারেজ পরিমাপ (M6-এর ধাঁচে, sys.settrace())

এখন পর্যন্ত লেখা তিনটি টেস্ট স্যুট মিলিয়ে calculate_total-এর কতটুকু প্রকৃতপক্ষে চলেছে তা পরিমাপ করা হচ্ছে — কোনো তৃতীয়-পক্ষের coverage.py ছাড়াই, শুধু Python-এর বিল্ট-ইন sys.settrace() দিয়ে। calculate_total-এর বডিতে (def লাইন বাদে) হাতে গোনা ঠিক ১১টি executable স্টেটমেন্ট-লাইন আছে — এটি একটি ম্যানুয়ালি যাচাই করা টোটাল, কারণ dynamically exec করা কোডে inspect.getsource() সবসময় নির্ভরযোগ্যভাবে কাজ নাও করতে পারে।

Python
# ========== মডিউল ৪: calculate_total-এর কভারেজ পরিমাপ -- sys.settrace() ==========
import sys

# calculate_total-এর বডিতে (def লাইন বাদে) ঠিক ১১টি নন-ব্ল্যাংক executable স্টেটমেন্ট-লাইন আছে --
# ম্যানুয়ালি গোনা টোটাল ব্যবহার করা হচ্ছে (CLAUDE.md-এর অনুমোদিত সরলীকরণ)
TOTAL_EXECUTABLE_LINES = 11

_executed_lines = set()

def _trace_calls(frame, event, arg):
    if frame.f_code is calculate_total.__code__:
        return _trace_lines
    return None

def _trace_lines(frame, event, arg):
    if event == "line":
        _executed_lines.add(frame.f_lineno)
    return _trace_lines

sys.settrace(_trace_calls)
try:
    _cov_r1 = unittest.TestResult(); make_suite1().run(_cov_r1)
    _cov_r2 = unittest.TestResult(); make_suite2().run(_cov_r2)
    _cov_r3 = unittest.TestResult(); make_suite3().run(_cov_r3)
finally:
    sys.settrace(None)  # ট্রেসিং বন্ধ করা আবশ্যক, নাহলে পরের সব কোড লাইন-বাই-লাইন ট্রেস হতে থাকবে

lines_hit = len(_executed_lines)
coverage_percent = round(100 * lines_hit / TOTAL_EXECUTABLE_LINES, 1)

tests_replayed = _cov_r1.testsRun + _cov_r2.testsRun + _cov_r3.testsRun
replay_bad = (len(_cov_r1.failures) + len(_cov_r1.errors) +
              len(_cov_r2.failures) + len(_cov_r2.errors) +
              len(_cov_r3.failures) + len(_cov_r3.errors))

print(f"কভারেজ পরিমাপের জন্য {tests_replayed}টি টেস্ট আবার চালানো হলো (ব্যর্থ/এরর: {replay_bad})")
print(f"calculate_total-এ ট্রেস করে পাওয়া ইউনিক এক্সিকিউটেড লাইন সংখ্যা: {lines_hit}")
print(f"স্টেটমেন্ট কভারেজ: {lines_hit}/{TOTAL_EXECUTABLE_LINES} = {coverage_percent}%")

    
কভারেজ দাঁড়ায় ১০০% (১১/১১ লাইন) — কারণ মডিউল ১-এর no-coupon/mid-tier কেসগুলো এবং মডিউল ৩-এর ১২টি বাউন্ডারি কেস মিলিয়ে if/elif-এর প্রতিটি শাখা অন্তত একবার চলে। তবে M6/L26-এ শেখা সতর্কতা মনে রাখা জরুরি — ১০০% স্টেটমেন্ট কভারেজ মানে কোড নিখুঁত তা প্রমাণ করে না, এটি শুধু বলে প্রতিটি লাইন অন্তত একবার চলেছে। পরের মডিউলে ঠিক এই কারণেই একটি মিউটেশন টেস্ট চালানো হচ্ছে — যাচাই করতে যে অ্যাসারশনগুলো সত্যিই যথেষ্ট শক্তিশালী কি না।

৬ · মডিউল ৫ — মিউটেশন টেস্টিং: একটি বাউন্ডারি-বাগ ইনজেক্ট করা (M10/L42-এর ধাঁচে)

এখন calculate_total-এর একটি কপিতে ইচ্ছাকৃতভাবে একটি ক্লাসিক বাউন্ডারি বাগ ইনজেক্ট করা হচ্ছে — তৃতীয় শাখার subtotal >= 500-কে subtotal > 500 দিয়ে বদলে দেওয়া হয়েছে। এই "মিউট্যান্ট"-এর বিরুদ্ধে মডিউল ১-৩-এর বিদ্যমান পুরো টেস্ট স্যুট (নতুন কোনো টেস্ট না লিখেই) আবার চালিয়ে দেখা হচ্ছে এটি ধরা পড়ে (killed) নাকি অলক্ষিত থেকে যায় (survived)।

Python
# ========== মডিউল ৫: মিউটেশন টেস্টিং -- একটি বাউন্ডারি-বাগ মিউট্যান্ট ==========
def calculate_total_mutant_boundary(cart):
    # মিউটেশন: তৃতীয় শাখার >= কে > দিয়ে বদলানো হয়েছে (ক্লাসিক বাউন্ডারি বাগ, L06-এর ধাঁচে)
    subtotal = sum(i["price"] * i["qty"] for i in cart["items"])
    coupon = cart.get("coupon")
    rate = 0.0
    if coupon == "SAVE10":
        if subtotal >= 5000:
            rate = 0.15
        elif subtotal >= 2000:
            rate = 0.10
        elif subtotal > 500:          # মিউটেশন এখানে -- মূল কোডে ছিল >=
            rate = 0.05
    return round(subtotal * (1 - rate), 2)


def _run_and_count(suite):
    result = unittest.TestResult()
    suite.run(result)
    bad = len(result.failures) + len(result.errors)
    passed = result.testsRun - bad
    return result.testsRun, passed, bad


_original_calculate_total = calculate_total
_SUITE_FACTORIES_TO_CHECK = [
    ("মডিউল ১ (কোর ইউনিট টেস্ট)", make_suite1),
    ("মডিউল ২ (checkout mock টেস্ট)", make_suite2),
    ("মডিউল ৩ (বাউন্ডারি টেস্ট)", make_suite3),
]

print("--- বেসলাইন: আসল calculate_total-এর বিরুদ্ধে পুরো টেস্ট স্যুট ---")
calculate_total = _original_calculate_total
for label, make_suite in _SUITE_FACTORIES_TO_CHECK:
    run, passed, bad = _run_and_count(make_suite())
    print(f"  {label}: {passed}/{run} পাস, {bad} ব্যর্থ/এরর")

print("\n--- মিউট্যান্ট: subtotal > 500 (মূলে ছিল >= 500) দিয়ে একই টেস্ট স্যুট আবার চালানো হলো ---")
calculate_total = calculate_total_mutant_boundary
mutant_bad_total = 0
for label, make_suite in _SUITE_FACTORIES_TO_CHECK:
    run, passed, bad = _run_and_count(make_suite())
    mutant_bad_total += bad
    print(f"  {label}: {passed}/{run} পাস, {bad} ব্যর্থ/এরর")

calculate_total = _original_calculate_total   # আসল ফাংশন পুনরুদ্ধার -- পরবর্তী সব কোডের জন্য জরুরি

mutant_killed = mutant_bad_total > 0
mutation_score = 1.0 if mutant_killed else 0.0
print(f"\nমিউট্যান্ট killed? {'হ্যাঁ' if mutant_killed else 'না'}  (মোট ব্যর্থ/এরর টেস্ট: {mutant_bad_total})")
print(f"মিউটেশন স্কোর: {mutation_score * 100:.0f}% (1/1 মিউট্যান্ট killed)")

    
মডিউল ১ ও ২-এর টেস্ট মিউট্যান্টের বিরুদ্ধেও পাস করে যায় — কারণ তাদের কোনো কেসের সাবটোটাল ঠিক ৫০০ নয় (এই সূক্ষ্ম মিউটেশন > বনাম >= শুধু সাবটোটাল ঠিক ৫০০ হলেই ভিন্ন ফলাফল দেয়)। কিন্তু মডিউল ৩-এর বাউন্ডারি টেস্ট স্যুট ঠিক এই কেসটি (subtotal=500) কভার করে বলেই এটি ব্যর্থ হয়ে মিউট্যান্টটি ধরে ফেলে — অর্থাৎ পুরো স্যুট মিলিয়ে মিউট্যান্ট killed। এটিই সরাসরি দেখায় কেন শুধু "সাধারণ" ইউনিট টেস্ট যথেষ্ট নয় — বাউন্ডারি-ভ্যালু টেস্ট (মডিউল ৩) ছাড়া এই বাগটি ১০০% কভারেজ সত্ত্বেও অলক্ষিত থেকে যেত।

৭ · মডিউল ৬ — চূড়ান্ত টেস্ট-স্যুট কোয়ালিটি সারাংশ

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

Python
# ========== মডিউল ৬: চূড়ান্ত টেস্ট-স্যুট কোয়ালিটি সারাংশ ==========
_final_r1 = unittest.TestResult(); make_suite1().run(_final_r1)
_final_r2 = unittest.TestResult(); make_suite2().run(_final_r2)
_final_r3 = unittest.TestResult(); make_suite3().run(_final_r3)

final_tests_run = _final_r1.testsRun + _final_r2.testsRun + _final_r3.testsRun
final_bad = (len(_final_r1.failures) + len(_final_r1.errors) +
             len(_final_r2.failures) + len(_final_r2.errors) +
             len(_final_r3.failures) + len(_final_r3.errors))
final_passed = final_tests_run - final_bad

print(f"ইউনিট + মক + বাউন্ডারি টেস্ট (৩টি TestCase ক্লাস মিলিয়ে): {final_passed}/{final_tests_run} পাস")
print(f"calculate_total-এর স্টেটমেন্ট কভারেজ: {coverage_percent}% ({lines_hit}/{TOTAL_EXECUTABLE_LINES} লাইন)")
print(f"বাউন্ডারি-বাগ মিউট্যান্ট: {'killed (ধরা পড়েছে)' if mutant_killed else 'survived (ধরা পড়েনি)'}, মিউটেশন স্কোর {mutation_score * 100:.0f}%")

overall_healthy = (final_bad == 0) and (coverage_percent == 100.0) and mutant_killed
print("\n" + "=" * 70)
print(
    f"চূড়ান্ত সারাংশ: {final_passed}/{final_tests_run} ইউনিট টেস্ট পাস, "
    f"{coverage_percent}% স্টেটমেন্ট কভারেজ, মিউট্যান্ট killed: {'হ্যাঁ' if mutant_killed else 'না'} "
    f"-- টেস্ট স্যুট {'স্বাস্থ্যকর' if overall_healthy else 'পর্যালোচনা প্রয়োজন'}"
)

    
চূড়ান্ত ফলাফল: ৯/৯ ইউনিট টেস্ট পাস (৫টি মডিউল ১ থেকে, ৩টি মডিউল ২ থেকে, ১টি মডিউল ৩ থেকে, যার ভেতরে ১২টি সাব-টেস্ট আছে), ১০০.০% স্টেটমেন্ট কভারেজ, এবং ইনজেক্ট করা মিউট্যান্ট killed — এই তিনটি সংখ্যাই আগের ছয়টি মডিউলের প্রকৃত এক্সিকিউশন থেকে সরাসরি গণনা করা, কোথাও হাতে লেখা নয়। এটিই একটি "সম্পূর্ণ" টেস্ট স্যুটের প্রকৃত অর্থ — শুধু টেস্ট থাকা নয়, বরং সেই টেস্টগুলো পরিমাপযোগ্যভাবে কাজ করছে তা প্রমাণ করা।
মূল কথা · Key takeaway

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

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

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

প্র ০১ মডিউল ৪-এ ১০০% স্টেটমেন্ট কভারেজ পাওয়ার পরও কেন মডিউল ৫-এ আলাদা করে মিউটেশন টেস্টিং চালানো হলো?

কারণ স্টেটমেন্ট কভারেজ শুধু বলে একটি লাইন চলেছে, তার ফলাফল কোনো টেস্ট সত্যিই যাচাই করেছে কি না তা নয় (M6/L26)। মিউটেশন টেস্টিং একটি ইচ্ছাকৃত বাগ ইনজেক্ট করে সরাসরি প্রমাণ করে যে অ্যাসারশনগুলো সত্যিই যথেষ্ট শক্তিশালী — শুধু কোড চালানো নয়, ভুল ফলাফল ধরতে পারাও জরুরি।

প্র ০২ মডিউল ৫-এ কেন মডিউল ১ ও ২-এর টেস্ট মিউট্যান্ট ধরতে ব্যর্থ হলো, অথচ মডিউল ৩-এর টেস্ট ধরে ফেললো?

মিউটেশনটি (>= থেকে >) শুধুমাত্র সাবটোটাল ঠিক ৫০০ হলে ভিন্ন ফলাফল দেয়। মডিউল ১ ও ২-এর কোনো টেস্ট কেসেই সাবটোটাল ঠিক ৫০০ নয়, তাই তারা মিউট্যান্ট ও আসল ফাংশনের মধ্যে পার্থক্য দেখতেই পায় না। মডিউল ৩-এর বাউন্ডারি টেস্ট ইচ্ছাকৃতভাবে ঠিক এই মানটি অন্তর্ভুক্ত করেছিল বলেই এটি একমাত্র টেস্ট যা এই নির্দিষ্ট বাগটি ধরতে সক্ষম হলো — এটিই দেখায় কেন টেস্ট-কেস নির্বাচনের কৌশল (বাউন্ডারি ভ্যালু অ্যানালাইসিস) গুরুত্বপূর্ণ, শুধু "অনেক টেস্ট লেখা" যথেষ্ট নয়।

প্র ০৩ মডিউল ২-এ কেন সত্যিকারের পেমেন্ট গেটওয়ের বদলে Mock() ব্যবহার করা হলো, আর assert_not_called()-এর ভূমিকা কী?

সত্যিকারের গেটওয়ে ব্যবহার করলে প্রতিটি টেস্ট রানে সত্যিকারের (বা টেস্ট-মোডের) চার্জ ট্রিগার হতো, যা ধীর, অনির্ভরযোগ্য, ও অপ্রয়োজনীয়ভাবে জটিল করে তুলতো (M4)। Mock() এই ডিপেন্ডেন্সিকে সম্পূর্ণ নিয়ন্ত্রণযোগ্য করে তোলে। assert_not_called() নিশ্চিত করে খালি কার্টের ক্ষেত্রে checkout() ভুলবশত গেটওয়েকে কল করে ফেলছে না — অর্থাৎ ভ্যালিডেশন লজিকটি গেটওয়ে কল হওয়ার আগেই সঠিকভাবে থেমে যাচ্ছে তা প্রমাণ করে।

অনুশীলন

  1. চিন্তা করুন: মডিউল ৫-এর মিউটেশনটি যদি প্রথম শাখায় (subtotal >= 5000-কে subtotal > 5000) করা হতো তার বদলে, তাহলে কোন কোন বিদ্যমান টেস্ট কেস এটি ধরতে পারতো বলে আপনার মনে হয়?

    মডিউল ৩-এর বাউন্ডারি সেটে subtotal=5000 কেসটি ঠিক এই মিউটেশনটিও ধরে ফেলতো, একই যুক্তিতে — সাবটোটাল ঠিক ৫০০০ হলে মূল কোড ১৫% ছাড় দেয় কিন্তু মিউট্যান্ট দেবে না (পরের শাখায় গিয়ে ১০% দেবে), ফলে assertEqual ব্যর্থ হয়ে মিউট্যান্টটি ধরা পড়তো। এটি দেখায় কেন প্রতিটি থ্রেশহোল্ডের জন্য আলাদা বাউন্ডারি কেস রাখা জরুরি — একটি বাউন্ডারি টেস্ট শুধু তার নিজের থ্রেশহোল্ডের বাগ ধরে, অন্যগুলোর নয়।

  2. পরীক্ষা করুন: মডিউল ৩-এর BOUNDARY_AND_EQUIVALENCE_CASES থেকে (500, 0.05, ...) এন্ট্রিটি সাময়িকভাবে মুছে (বা কমেন্ট করে) মডিউল ৩, ৪, ও ৫ আবার ক্রমানুসারে Run করুন — কভারেজ ও মিউটেশন-কিল ফলাফল কী বদলায় লক্ষ্য করুন।

    কভারেজে খুব একটা তফাত পড়বে না (অন্যান্য কেস, যেমন ৫০১ বা ১২৫০, তখনও একই elif শাখা চালাবে), কিন্তু মডিউল ৫-এ মিউট্যান্ট আর ধরা পড়বে না — কারণ সাবটোটাল ঠিক ৫০০-এ পরীক্ষা করা একমাত্র টেস্ট কেসটিই সরিয়ে ফেলা হয়েছে, আর অন্য কোনো কেস > বনাম >=-এর পার্থক্য প্রকাশ করে না। ফলাফল: মিউট্যান্ট survived, এবং মডিউল ৬-এর চূড়ান্ত সারাংশে "টেস্ট স্যুট পর্যালোচনা প্রয়োজন" দেখাবে — এটিই প্রমাণ করে ১০০% কভারেজ থাকা সত্ত্বেও একটি নির্দিষ্ট টেস্ট কেস সরিয়ে ফেললে স্যুটের প্রকৃত শক্তি কমে যেতে পারে।

আরও পড়ুন · কোর্স সম্পন্ন

  • অভিনন্দন — সম্পূর্ণ সিলেবাস আরেকবার দেখুন ৫৭/৫৭টি পাঠ সম্পন্ন আপনি Software Testing & Quality Assurance কোর্সের সবগুলো পাঠ — ফাউন্ডেশন থেকে টেস্ট ডিজাইন, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন, কভারেজ, অটোমেশন, পারফরম্যান্স, সিকিউরিটি, অ্যাডভান্সড টেকনিক, ম্যানেজমেন্ট, CI/CD, ও এই ক্যাপস্টোন পর্যন্ত — সম্পন্ন করেছেন।
  • Ethics in Computing & AI Safety কোর্স অন্য কোর্স এই সেশনে তৈরি করা আরেকটি সম্পূর্ণ কোর্স — সফটওয়্যার ও AI-এর নৈতিক ও নিরাপত্তা দিক নিয়ে গভীর আলোচনা।
  • সব 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 — সব এক জায়গায়।
আগের পাঠ
সাধারণ টেস্টিং অ্যান্টি-প্যাটার্ন ও তা এড়ানোর উপায়