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

প্যারামিটারাইজড ও ডেটা-ড্রিভেন ইউনিট টেস্ট

Parameterized and data-driven unit tests
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন এই কোর্সে pytest.mark.parametrize ব্যবহার করা যায় না, আর তার আসল stdlib বিকল্প কী
  • self.subTest()-এর সঠিক ব্যবহার — একই টেস্ট মেথডে একাধিক ইনপুট-প্রত্যাশা কেস চালানো
  • subTest ছাড়া সাধারণ লুপ আর subTest-সহ লুপের ব্যর্থতা-রিপোর্টিং আচরণের পার্থক্য
  • একটি সত্যিকারের ডেমো — একটি ইচ্ছাকৃত ভুল কেসসহ, যা দেখায় ঠিক কোন কেসটি ব্যর্থ হয়েছে তা subTest কীভাবে নির্দিষ্টভাবে চিহ্নিত করে

১ · কেন self.subTest(), আর pytest.mark.parametrize নয়

বাস্তব ইন্ডাস্ট্রিতে প্যারামিটারাইজড টেস্টিং (একই টেস্ট লজিক অনেকগুলো ইনপুট-প্রত্যাশা জোড়ায় চালানো) প্রায়ই pytest-এর @pytest.mark.parametrize ডেকোরেটর দিয়ে করা হয় — কিন্তু এই কোর্সের Pyodide স্যান্ডবক্সে কোনো থার্ড-পার্টি প্যাকেজ (তাই pytest-ও) ইনস্টল করা নেই, শুধু Python-এর স্ট্যান্ডার্ড লাইব্রেরি আছে। ভাগ্যক্রমে unittest-এরই একটি সত্যিকারের, বিল্ট-ইন সমাধান আছে: self.subTest()subTestএকটি TestCase মেথডের ভেতরে একটি context manager, যা একটি লুপের প্রতিটি ইটারেশনকে একটি স্বতন্ত্র, আলাদাভাবে-রিপোর্ট-হওয়া সাব-টেস্ট হিসেবে চিহ্নিত করে। — একটি লুপের প্রতিটি ইটারেশনকে with self.subTest(...): দিয়ে ঘিরে দিলে প্রতিটি কেস আলাদাভাবে ট্র্যাক ও রিপোর্ট হয়, যদিও সবগুলো একই টেস্ট মেথডের ভেতরে লেখা।

২ · সাধারণ লুপ বনাম subTest-সহ লুপ

একটি সাধারণ for লুপের ভেতরে সরাসরি self.assertEqual(...) রাখলে, প্রথম যে কেসটি ব্যর্থ হয় সেখানেই একটি AssertionError ছোঁড়া হয় এবং পুরো টেস্ট মেথডটি সাথে সাথে থেমে যায় — লুপের বাকি কেসগুলো আর চলেই না, ফলে তাদের মধ্যে আরও কোনো ভুল আছে কি না তা এক রানে জানা সম্ভব হয় না। with self.subTest(...): ব্যবহার করলে একটি কেসের ব্যর্থতা লুপ থামায় না — unittest সেই ব্যর্থতা মনে রাখে, লুপ চালিয়ে যায়, এবং শেষে প্রতিটি ব্যর্থ কেস আলাদাভাবে, তার subTest-এ দেওয়া প্যারামিটার-সহ রিপোর্ট করে।

Full-Stack Web Frameworks / Mobile App Development কোর্সের সাথে সম্পর্ক

সাধারণভাবে "একই টেস্ট লজিক বহু ইনপুটে চালানো" ধারণাটি সাইট-জুড়ে বিভিন্ন কোর্সে ওয়েটেড-স্কোরিং বা মাল্টি-কেস লুপ প্যাটার্ন হিসেবে ফিরে আসে — এখানে unittest-এর নিজস্ব subTest মেকানিজম দিয়ে এটি বাস্তবায়ন দেখানো হলো, যা এই কোর্সেরই কেন্দ্রীয় টুল।

৩ · একটি সত্যিকারের ডেমো — একটি ইচ্ছাকৃত ভুল কেসসহ

নিচে একটি is_leap_year(year) ফাংশন আছে — গ্রেগরিয়ান ক্যালেন্ডারের প্রকৃত নিয়ম অনুযায়ী লেখা (৪ দিয়ে বিভাজ্য, কিন্তু ১০০ দিয়ে বিভাজ্য হলে অবশ্যই ৪০০ দিয়েও বিভাজ্য হতে হবে)। পাঁচটি (year, expected) কেস টেস্ট করা হয়েছে — কিন্তু ইচ্ছাকৃতভাবে একটি কেসে ভুল প্রত্যাশা দেওয়া হয়েছে (2100-কে লিপ ইয়ার বলে ভুলভাবে প্রত্যাশা করা হয়েছে, যদিও বাস্তবে ২১০০ ১০০ দিয়ে বিভাজ্য কিন্তু ৪০০ দিয়ে নয়, তাই লিপ ইয়ার নয়) — যাতে subTest-এর ব্যর্থতা-রিপোর্ট সত্যিকারভাবে দেখা যায়।

Python
import unittest

def is_leap_year(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

class LeapYearParameterizedTests(unittest.TestCase):
    def test_is_leap_year_multiple_cases(self):
        cases = [
            (2000, True),   # 400 দিয়ে বিভাজ্য -> লিপ ইয়ার
            (1900, False),  # 100 দিয়ে বিভাজ্য কিন্তু 400 দিয়ে নয় -> লিপ ইয়ার নয়
            (2100, True),   # ⚠️ ইচ্ছাকৃত ভুল প্রত্যাশা! বাস্তবে 2100 লিপ ইয়ার নয় (আসল উত্তর: False)
            (2024, True),   # 4 দিয়ে বিভাজ্য, 100 দিয়ে নয় -> লিপ ইয়ার
            (2023, False),  # 4 দিয়ে বিভাজ্য নয় -> লিপ ইয়ার নয়
        ]
        for year, expected in cases:
            with self.subTest(year=year, expected=expected):
                self.assertEqual(is_leap_year(year), expected)

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

    
Run চাপলে লক্ষ্য করুন: টেস্ট মেথডটি সামগ্রিকভাবে FAILED দেখাবে, কিন্তু আউটপুটে একটি নির্দিষ্ট FAIL: ব্লক দেখা যাবে যার হেডারে ঠিক লেখা থাকবে (year=2100, expected=True) — অর্থাৎ এটি বলছে না শুধু "একটা কিছু ভুল হয়েছে", বরং হুবহু বলছে কোন কেসটি ব্যর্থ হয়েছে, সাথে AssertionError: False != True (কারণ is_leap_year(2100) প্রকৃতপক্ষে False রিটার্ন করে, যা এই ইচ্ছাকৃতভাবে ভুল লেখা প্রত্যাশা True-এর সাথে মেলেনি)। বাকি চারটি কেস (2000, 1900, 2024, 2023) স্বাভাবিকভাবে পাস করে — এবং গুরুত্বপূর্ণভাবে, 2100 কেসে ব্যর্থতার পরেও লুপ থামেনি, 2024 ও 2023 কেসদুটোও চলেছে এবং পাস করেছে। এটিই subTest-এর মূল শক্তি: একটি কেসের ব্যর্থতা বাকি কেসগুলোকে "লুকিয়ে" ফেলে না।
মূল কথা · Key takeaway

self.subTest() হলো এই কোর্সের স্যান্ডবক্সে প্যারামিটারাইজড টেস্টিং-এর আসল, বাস্তব stdlib সমাধান — এটি একই টেস্ট মেথডে একাধিক ডেটা-চালিত কেস চালানোর পাশাপাশি প্রতিটি কেসের ব্যর্থতা আলাদাভাবে, তার নির্দিষ্ট প্যারামিটারসহ রিপোর্ট করে, যা ডিবাগিংকে অনেক সহজ করে তোলে। পরের পাঠে (L14) আমরা দেখব কোড কীভাবে লিখলে তা সহজে টেস্টযোগ্য হয়।

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

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

প্র ০১ উপরের কোড সেলে with self.subTest(...): সরিয়ে ফেলে সরাসরি self.assertEqual(is_leap_year(year), expected) লুপের ভেতরে রাখলে ফলাফলে কী পার্থক্য হতো?

2100 কেসে পৌঁছালেই AssertionError ছোঁড়া হতো এবং পুরো টেস্ট মেথডটি সাথে সাথে থেমে যেত — 2024 ও 2023 কেস দুটো আর কখনোই চলত না। রিপোর্টে শুধু একটি সাধারণ ব্যর্থতা দেখা যেত, "কোন year-এর জন্য ব্যর্থ হয়েছে" তা বের করতে ট্রেসব্যাক আলাদাভাবে পড়তে হতো, আর বাকি কেসগুলোর অবস্থা (পাস/ফেল) একেবারেই অজানা থেকে যেত।

প্র ০২ self.subTest(year=year, expected=expected)-এ কীওয়ার্ড আর্গুমেন্ট (year=, expected=) দেওয়ার সুবিধা কী?

এই কীওয়ার্ড আর্গুমেন্টগুলো ব্যর্থতার রিপোর্টে সরাসরি দেখানো হয় (যেমন (year=2100, expected=True)), যা ঠিক কোন ইনপুট-প্রত্যাশা জোড়ায় সমস্যা হয়েছে তা সাথে সাথে বুঝিয়ে দেয় — আলাদাভাবে ট্রেসব্যাকের লাইন নম্বর দেখে অনুমান করতে হয় না। এটি ডিবাগিংকে দ্রুত করে তোলে, বিশেষ করে অনেকগুলো কেস থাকলে।

প্র ০৩ উপরের কোড সেলে 2100-এর প্রত্যাশিত মান True-এর বদলে সঠিক False করলে সামগ্রিক টেস্ট ফলাফল কী হবে?

তখন পাঁচটি কেসই সঠিক প্রত্যাশা পাবে, তাই সবগুলো সাব-কেস পাস করবে এবং সামগ্রিক টেস্ট মেথডটিও OK দেখাবে — আউটপুটে আর কোনো FAIL: ব্লক থাকবে না। এটি দেখায় যে subTest নিজে থেকে কোনো ব্যর্থতা তৈরি করে না — এটি শুধু ইতিমধ্যে-থাকা ব্যর্থতাগুলোকে আরও স্পষ্টভাবে, পৃথকভাবে রিপোর্ট করে।

অনুশীলন

  1. চিন্তা করুন: is_leap_year-এর জন্য আর কোন কোন বছর টেস্ট কেস হিসেবে যোগ করা উচিত বলে মনে হয়, যা এই পাঁচটি কেসে এখনও কভার হয়নি?

    2400 (৪০০ দিয়ে বিভাজ্য, তাই True হওয়া উচিত — 2000-এর মতোই কিন্তু ভিন্ন শতাব্দী), 2001 (৪ দিয়েও বিভাজ্য নয়, সহজ False কেস, কিন্তু 2023-এর থেকে ভিন্ন প্যাটার্নের), আর 0 বা নেগেটিভ বছরের মতো এজ কেস (যদি ফাংশনটি ঐতিহাসিক তারিখ নিয়েও কাজ করার কথা থাকে)।

  2. পরীক্ষা করুন: উপরের কোড সেলে cases লিস্টে (1600, False) যোগ করে (ইচ্ছাকৃতভাবে ভুল — ১৬০০ আসলে ৪০০ দিয়ে বিভাজ্য, তাই লিপ ইয়ার) Run চেপে দেখুন এখন কয়টি subTest ব্যর্থতা রিপোর্ট হয়।

    এখন দুটি স্বতন্ত্র FAIL: ব্লক দেখা যাবে — একটি (year=2100, expected=True)-এর জন্য, আরেকটি নতুন (year=1600, expected=False)-এর জন্য। এটি দেখায় subTest একাধিক ব্যর্থ কেসও একসাথে, প্রতিটি আলাদাভাবে চিহ্নিত করে রিপোর্ট করতে পারে — একটি ব্যর্থতা অন্যটি "ঢেকে" ফেলে না।

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

  • পরের পাঠ L14 টেস্টযোগ্য কোড লেখা — ডিপেন্ডেন্সি ইনজেকশন দিয়ে কোডকে টেস্টযোগ্য করে তোলা।
  • আগের পাঠ L12 টেস্ট ফিক্সচার — setUp ও tearDown দিয়ে টেস্ট আইসোলেশন নিশ্চিত করা।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
আগের পাঠ
টেস্ট ফিক্সচার — setUp ও tearDown