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

অ্যাসারশন ও টেস্ট অর্গানাইজেশন

Assertions and test organization
৯ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • unittest-এর পাঁচটি গুরুত্বপূর্ণ অ্যাসারশন মেথড ও তাদের সঠিক ব্যবহারক্ষেত্র
  • কেন float তুলনায় assertAlmostEqual দরকার, assertEqual নয়
  • টেস্ট মেথড নামকরণ ও অর্গানাইজেশনের ভালো অভ্যাস
  • একটি সত্যিকারের TestCase — ছয়টি ভিন্ন অ্যাসারশন, সবগুলো সত্যিকারভাবে পাস

১ · মূল অ্যাসারশন মেথডগুলো

একটি অ্যাসারশনAssertionএকটি একক দাবি — "এই মানটি ওই মানের সমান হওয়া উচিত" জাতীয় — যা মিথ্যা প্রমাণিত হলে টেস্টটি ব্যর্থ (FAIL) হিসেবে রিপোর্ট হয়। হলো একটি একক, নির্দিষ্ট দাবি। unittest.TestCase-এর ভেতরে ডজনখানেক অ্যাসারশন মেথড আছে, কিন্তু প্রতিদিনের কাজে এই পাঁচটিই সবচেয়ে বেশি ব্যবহৃত হয়:

assertEqual(a, b)
দুটি মান হুবহু সমান কি না — সংখ্যা, স্ট্রিং, লিস্ট, ডিকশনারি ইত্যাদির জন্য।
assertTrue / assertFalse
একটি শর্ত সত্য/মিথ্যা কি না — বুলিয়ান ফলাফল বা কোনো condition যাচাইয়ে।
assertIn(item, collection)
item-টি কোনো লিস্ট/সেট/স্ট্রিং-এর ভেতরে আছে কি না — membership যাচাইয়ে।
assertRaises(Error)
একটি নির্দিষ্ট ব্যতিক্রম ছোঁড়া হচ্ছে কি না — with ব্লক আকারে।
assertAlmostEqual(a, b)
দুটি float প্রায়-সমান কি না (ডিফল্ট ৭ দশমিক স্থান পর্যন্ত) — floating-point রাউন্ডিং সহ্য করে।

২ · কেন float-এর জন্য assertEqual যথেষ্ট নয়

Python-এর float IEEE 754 বাইনারি ফরম্যাটে সংরক্ষিত হয়, যেখানে 0.1, 0.2, 0.3-এর মতো "সহজ" দশমিক সংখ্যাও বাইনারিতে ঠিক অনুবাদ হয় না — ফলে 0.1 + 0.2 Python-এ 0.30000000000000004 হয়ে যায়, ঠিক 0.3 নয়। এই কারণে assertEqual দিয়ে দুটি হিসাব-করা float তুলনা করলে সেগুলো গাণিতিকভাবে সঠিক হলেও টেস্ট ব্যর্থ হতে পারে। সমাধান: assertAlmostEqual(a, b), যা দুটি মানের পার্থক্য একটি খুব ছোট সহনশীলতার (tolerance) মধ্যে আছে কি না দেখে — ঠিক এই কারণেই আসল কম্পিউটেশন যাচাইয়ে এটি সঠিক পছন্দ।

৩ · নামকরণ ও অর্গানাইজেশনের ভালো অভ্যাস

একটি ভালো টেস্ট মেথড নাম থেকেই বোঝা উচিত ঠিক কী আচরণ যাচাই হচ্ছে — যেমন test_convert_amount_rejects_unsupported_currency, শুধু test_1 নয়। একটি সাধারণ নিয়ম: এক টেস্ট মেথড = এক আচরণ/এক পরিস্থিতি — এতে একটি টেস্ট ব্যর্থ হলে ঠিক কোন আচরণ ভেঙেছে তা সাথে সাথে বোঝা যায়, আলাদাভাবে ডিবাগ না করেই। সম্পর্কিত টেস্টগুলো একই TestCase ক্লাসে একসাথে রাখা হয় (এখানে যেমন সব AssertionShowcaseTests-এর ভেতরে), যাতে একটি ফাইল/ক্লাস দেখেই বোঝা যায় একটি নির্দিষ্ট মডিউল/ফাংশনের জন্য কী কী আচরণ কভার করা হয়েছে।

নিচের কোড সেলে একটি ছোট মডিউল আছে — average (float নিয়ে কাজ করে), is_valid_username (বুলিয়ান ফেরত দেয়), আর convert_amount (সমর্থিত না হলে ব্যতিক্রম ছোঁড়ে)। একটি TestCase-এ ছয়টি ভিন্ন অ্যাসারশন ব্যবহার করে এগুলো সবগুলো যাচাই করা হয়েছে।

Python
import unittest

def average(numbers):
    return sum(numbers) / len(numbers)

def is_valid_username(name):
    return name.isalnum() and 3 <= len(name) <= 20

SUPPORTED_CURRENCIES = ["BDT", "USD", "EUR", "GBP"]

def convert_amount(amount, currency):
    if currency not in SUPPORTED_CURRENCIES:
        raise ValueError(f"অসমর্থিত মুদ্রা: {currency}")
    return amount

class AssertionShowcaseTests(unittest.TestCase):
    def test_average_of_floats_is_almost_equal_to_expected(self):
        # 0.1 + 0.2 + 0.3 বাইনারি রাউন্ডিং-এর কারণে ঠিক 0.6 নয় — assertEqual এখানে ব্যর্থ হতো
        self.assertAlmostEqual(average([0.1, 0.2, 0.3]), 0.2)

    def test_is_valid_username_accepts_good_name(self):
        self.assertTrue(is_valid_username("abcl123"))

    def test_is_valid_username_rejects_too_short_name(self):
        self.assertFalse(is_valid_username("ab"))

    def test_usd_is_a_supported_currency(self):
        self.assertIn("USD", SUPPORTED_CURRENCIES)

    def test_convert_amount_returns_same_amount_for_supported_currency(self):
        self.assertEqual(convert_amount(500, "BDT"), 500)

    def test_convert_amount_rejects_unsupported_currency(self):
        with self.assertRaises(ValueError):
            convert_amount(500, "XXX")

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

    
Run চাপলে ছয়টি টেস্টই পাস করে। বিশেষভাবে লক্ষ্য করুন test_average_of_floats_is_almost_equal_to_expected — average([0.1, 0.2, 0.3])-এর প্রকৃত মান 0.2-এর সাথে হুবহু সমান নয় (বাইনারি রাউন্ডিং-এর কারণে সামান্য পার্থক্য থাকে), কিন্তু assertAlmostEqual সেই ক্ষুদ্র পার্থক্যকে সহনশীল ধরে টেস্টটি পাস করায় — assertEqual ব্যবহার করলে এই একই টেস্ট ব্যর্থ হতো।
মূল কথা · Key takeaway

সঠিক অ্যাসারশন বেছে নেওয়া টেস্ট কোডকে শুধু ছোট রাখে না, ব্যর্থতার বার্তাও অনেক স্পষ্ট করে তোলে — আর বর্ণনামূলক টেস্ট নাম ও এক-আচরণ-এক-টেস্ট নীতি একটি টেস্ট স্যুটকে সময়ের সাথে সহজে বোঝা ও রক্ষণাবেক্ষণযোগ্য রাখে। পরের পাঠে (L12) আমরা দেখব একাধিক টেস্টের মধ্যে সাধারণ সেটআপ কোড কীভাবে setUp/tearDown দিয়ে গুছিয়ে রাখা যায়।

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

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

প্র ০১ কেন 0.1 + 0.2 == 0.3 Python-এ False হয়, আর এটি টেস্ট লেখার সময় কী শেখায়?

কারণ float কম্পিউটারে বাইনারি (base-2) ফরম্যাটে সংরক্ষিত হয়, আর 0.1/0.2/0.3-এর মতো "সহজ" দশমিক ভগ্নাংশও বাইনারিতে সসীমভাবে প্রকাশ করা যায় না — ঠিক যেমন 1/3 দশমিকে অসীম পুনরাবৃত্তি হয়। ফলে ছোট রাউন্ডিং ত্রুটি জমা হয়। এটি শেখায় যে float জড়িত কোনো হিসাব যাচাই করার সময় সবসময় assertAlmostEqual ব্যবহার করা উচিত, কখনোই সরাসরি assertEqual নয়।

প্র ০২ একজন টিম মেম্বার সব টেস্ট মেথডের নাম test_1, test_2, test_3 রাখছে — এতে ভবিষ্যতে কী সমস্যা হতে পারে?

কোনো একটি টেস্ট ব্যর্থ হলে রিপোর্টে শুধু test_3 FAILED দেখাবে — এই নাম থেকে বোঝার কোনো উপায় নেই ঠিক কোন আচরণ ভেঙেছে। কোডটি আবার খুলে দেখতে হবে test_3-এর ভেতরে কী যাচাই হচ্ছিল। বর্ণনামূলক নাম (যেমন test_convert_amount_rejects_unsupported_currency) থাকলে ব্যর্থতার বার্তা থেকেই সমস্যাটি অনেকাংশে স্পষ্ট হয়ে যায়, কোড না খুলেই।

প্র ০৩ উপরের কোড সেলে assertIn("USD", SUPPORTED_CURRENCIES)-এর বদলে assertEqual("USD" in SUPPORTED_CURRENCIES, True) লিখলে কি একই কাজ হতো?

টেকনিক্যালি একই বুলিয়ান ফলাফল যাচাই হতো, কিন্তু assertIn ব্যবহার করলে ব্যর্থ হওয়ার সময় unittest নিজে থেকেই একটি অনেক স্পষ্ট বার্তা দেয় (যেমন "'USD' not found in [...]"), যেখানে assertEqual-এর ক্ষেত্রে শুধু "False != True" দেখাবে — কোন আইটেম কোন কালেকশনে পাওয়া যায়নি তা বলবে না। তাই যেখানে একটি নির্দিষ্ট উদ্দেশ্যমূলক অ্যাসারশন আছে (যেমন assertIn), সেটিই ব্যবহার করা উচিত — এটি টেস্ট কোডকে আরও স্ব-বর্ণনামূলক করে তোলে।

অনুশীলন

  1. চিন্তা করুন: is_valid_username-এর জন্য আর কোন কোন নতুন টেস্ট মেথড লেখা উচিত বলে মনে হয়? (হিন্ট: ঠিক ৩ অক্ষরের নাম? ঠিক ২০ অক্ষরের নাম? স্পেশাল ক্যারেক্টারসহ নাম?)

    test_is_valid_username_accepts_minimum_length (৩ অক্ষর, বাউন্ডারিতে গ্রহণযোগ্য হওয়া উচিত), test_is_valid_username_accepts_maximum_length (২০ অক্ষর), আর test_is_valid_username_rejects_special_characters (যেমন "abc@123", যা isalnum() মিথ্যা রিটার্ন করবে) — এই বাউন্ডারি-কেন্দ্রিক চিন্তাভাবনাই M2/L06-এর "বাউন্ডারি ভ্যালু অ্যানালাইসিস"-এর মূল ভিত্তি।

  2. পরীক্ষা করুন: উপরের কোড সেলে test_average_of_floats_is_almost_equal_to_expected-এর assertAlmostEqual-কে সাময়িকভাবে assertEqual-এ বদলে Run চাপুন — কী হয় দেখুন।

    টেস্টটি ব্যর্থ (FAIL) হবে, কারণ average([0.1, 0.2, 0.3])-এর প্রকৃত ফলাফল হুবহু 0.2 নয় (সামান্য বাইনারি রাউন্ডিং ত্রুটিসহ, যেমন 0.19999999999999998-এর কাছাকাছি কিছু) — assertEqual সেই ক্ষুদ্র পার্থক্যও সহ্য করে না, অথচ গাণিতিকভাবে হিসাবটি সম্পূর্ণ সঠিক। এটিই সরাসরি দেখায় কেন float তুলনায় assertAlmostEqual দরকার।

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

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