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

টেস্টযোগ্য কোড লেখা

Writing testable code
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

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

১ · সমস্যা — হার্ডকোড করা ডিপেন্ডেন্সি

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

২ · সমাধান — ডিপেন্ডেন্সি ইনজেকশন

সমাধান হলো ফাংশনটিকে তার ডিপেন্ডেন্সি হার্ডকোড না করে, সেটি একটি প্যারামিটার হিসেবে বাইরে থেকে গ্রহণ করানো। তখন প্রোডাকশনে আসল ফাংশন/ক্লাস পাস করা হয়, কিন্তু টেস্টে একটি সহজ, দ্রুত, ইন-মেমরি "ফেক" পাস করা যায় যেটি বাস্তব I/O না করে শুধু কল রেকর্ড করে রাখে। ফাংশনের নিজস্ব লজিক (কী পাঠানো হচ্ছে, কখন পাঠানো হচ্ছে) তখন সম্পূর্ণ বিচ্ছিন্নভাবে, বাস্তব নেটওয়ার্ক ছাড়াই যাচাই করা যায়।

হার্ডকোড করা
ফাংশন নিজেই সরাসরি একটি নির্দিষ্ট বাস্তব ডিপেন্ডেন্সি ডাকে — কলারের কোনো নিয়ন্ত্রণ নেই, টেস্টে প্রতিস্থাপন অসম্ভব।
ইনজেক্টেড
ডিপেন্ডেন্সিটি প্যারামিটার/কনস্ট্রাক্টর আর্গুমেন্ট — কলার (প্রোডাকশন কোড বা টেস্ট) ঠিক করে দেয় কোন বাস্তবায়ন ব্যবহার হবে।
Full-Stack Web Frameworks / Mobile App Development কোর্সের সাথে সম্পর্ক

ব্যাক-এন্ড ফ্রেমওয়ার্কে ডিপেন্ডেন্সি ইনজেকশন (Full-Stack Web Frameworks) এই একই প্যাটার্নকে একটি ব্যাক-এন্ড ফ্রেমওয়ার্কের কনফিগারেশন/আর্কিটেকচার সমস্যা হিসেবে দেখে, আর মোবাইলের জন্য ক্লিন আর্কিটেকচার লেয়ার (Mobile App Development) একে লেয়ার-বিচ্ছিন্নতার অংশ হিসেবে দেখে — এই পাঠ ঠিক একই ধারণাকে বিশুদ্ধভাবে টেস্টযোগ্যতার দৃষ্টিকোণ থেকে দেখাচ্ছে।

৩ · পাশাপাশি — টেস্ট-অযোগ্য বনাম টেস্টযোগ্য ভার্সন

নিচের কোড সেলে দুটি ভার্সন আছে। notify_user_hard_to_test সরাসরি send_real_email ডাকে — একে বাস্তব নেটওয়ার্ক ছাড়া টেস্ট করার কোনো উপায় নেই, তাই আমরা এটির জন্য কোনো ইউনিট টেস্ট লিখছিই না (এটাই সমস্যার প্রমাণ)। notify_user_testable-এ একই কাজ করা হয়েছে, কিন্তু ইমেইল-পাঠানোর ডিপেন্ডেন্সিটি email_sender প্যারামিটার হিসেবে ইনজেক্ট করা — টেস্টে একটি সাধারণ FakeEmailSender ক্লাস পাস করে, বাস্তব I/O ছাড়াই একটি সত্যিকারের, পাসিং ইউনিট টেস্ট লেখা হয়েছে।

Python
import unittest

# ---- হার্ডকোড করা, টেস্ট-অযোগ্য ভার্সন ----
def send_real_email(to, subject, body):
    # বাস্তবে এখানে একটি প্রকৃত SMTP সংযোগ খোলা হতো — সত্যিকারের নেটওয়ার্ক I/O
    raise ConnectionError("বাস্তব SMTP সার্ভারে সংযোগের চেষ্টা — এই স্যান্ডবক্সে/ইউনিট টেস্টে সম্ভব নয়")

def notify_user_hard_to_test(username, message):
    # সমস্যা: send_real_email সরাসরি হার্ডকোড করা — টেস্টে প্রতিস্থাপনের কোনো উপায় নেই
    return send_real_email(username, "নোটিফিকেশন", message)

# উপরের ফাংশনের জন্য ইচ্ছাকৃতভাবে কোনো ইউনিট টেস্ট লেখা হয়নি —
# কারণ এটি চালালেই send_real_email ডাকা হবে, যা বাস্তব নেটওয়ার্ক I/O দাবি করে।

# ---- ডিপেন্ডেন্সি-ইনজেক্টেড, টেস্টযোগ্য ভার্সন ----
def notify_user_testable(username, message, email_sender):
    return email_sender(username, "নোটিফিকেশন", message)

class FakeEmailSender:
    """প্রোডাকশনে ব্যবহৃত হয় না — শুধু টেস্টে বাস্তব ইমেইল সার্ভারের বদলি হিসেবে।"""
    def __init__(self):
        self.sent_emails = []

    def __call__(self, to, subject, body):
        self.sent_emails.append((to, subject, body))
        return True

class NotifyUserTests(unittest.TestCase):
    def test_notify_user_uses_injected_sender_without_real_io(self):
        fake_sender = FakeEmailSender()
        result = notify_user_testable("rahim", "আপনার অর্ডার শিপড হয়েছে", fake_sender)

        self.assertTrue(result)
        self.assertEqual(len(fake_sender.sent_emails), 1)
        self.assertEqual(
            fake_sender.sent_emails[0],
            ("rahim", "নোটিফিকেশন", "আপনার অর্ডার শিপড হয়েছে")
        )

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

    
Run চাপলে NotifyUserTests-এর একমাত্র টেস্টটি পাস করে — কোনো বাস্তব নেটওয়ার্ক কল ছাড়াই। notify_user_testable-কে যদি সত্যিকারের প্রোডাকশন কোডে ব্যবহার করা হতো, সেখানে email_sender হিসেবে send_real_email-এর মতো একটি প্রকৃত বাস্তবায়ন পাস করা হতো; টেস্টে শুধু FakeEmailSender পাস করা হয়েছে। ফাংশনের নিজস্ব লজিক — এটি সঠিক আর্গুমেন্টসহ email_sender-কে ঠিক একবার ডাকছে কি না — সম্পূর্ণ বিচ্ছিন্নভাবে যাচাই হয়ে গেছে। notify_user_hard_to_test-এর জন্য এই ধরনের কোনো টেস্ট লেখাই সম্ভব ছিল না, কারণ সেখানে বদলি করার কোনো "হুক" নেই — এটাই ডিপেন্ডেন্সি ইনজেকশনের ব্যবহারিক মূল্য।
মূল কথা · Key takeaway

টেস্টযোগ্যতা কোনো টেস্টিং টুলের বিষয় নয় — এটি কোড কীভাবে ডিজাইন করা হয়েছে তার বিষয়। ডিপেন্ডেন্সি হার্ডকোড না করে প্যারামিটার/কনস্ট্রাক্টর আর্গুমেন্ট হিসেবে ইনজেক্ট করলে, একই কোড প্রোডাকশনে বাস্তব ডিপেন্ডেন্সি নিয়ে চলে আর টেস্টে একটি সাধারণ ফেক নিয়ে — কোনো পরিবর্তন ছাড়াই। M4-এ (টেস্ট ডাবলস) আমরা দেখব কীভাবে unittest.mock এই ধরনের ইনজেক্টেড ডিপেন্ডেন্সির জন্য আরও শক্তিশালী, প্রস্তুত-করা ফেক/মক সরবরাহ করে।

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

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

প্র ০১ notify_user_hard_to_test-এর জন্য কেন কোনো ইউনিট টেস্ট লেখা হয়নি — শুধু "কঠিন" নাকি সত্যিই "অসম্ভব"?

এই কোড সেলের প্রেক্ষাপটে এটি প্রায় সত্যিই অসম্ভব — send_real_email সরাসরি হার্ডকোড করা, কোনো প্যারামিটার নেই যেখানে একটি ফেক পাস করা যাবে। এটি চালালেই ConnectionError ছোঁড়া হবে (এই স্যান্ডবক্সে সত্যিকারের নেটওয়ার্ক না থাকায়)। বাস্তব জগতেও এই ধরনের কোড টেস্ট করতে জটিল, ভঙ্গুর টেকনিক (যেমন মডিউল-লেভেল মাংকি-প্যাচিং) লাগে, যা M4-এ দেখা unittest.mock.patch-এর মতো টুল দিয়ে কিছুটা সহজ করা গেলেও, শুরু থেকেই ডিপেন্ডেন্সি ইনজেক্ট করে লেখা কোডের চেয়ে সবসময় বেশি জটিল।

প্র ০২ FakeEmailSender-এ __call__ মেথড কেন ব্যবহার করা হয়েছে, আলাদা একটি send-নামের মেথড কেন নয়?

কারণ notify_user_testable ফাংশনটি email_sender-কে সরাসরি একটি ফাংশনের মতো কল করে (email_sender(to, subject, body)), email_sender.send(...) নয়। যদি FakeEmailSender-এর __call__ মেথড না থাকত, তাহলে এই ফেক অবজেক্টটি সরাসরি "কলযোগ্য" (callable) হতো না এবং notify_user_testable-এর প্রত্যাশিত ইন্টারফেসের সাথে মিলত না। এটি দেখায় যে একটি ফেক অবশ্যই আসল ডিপেন্ডেন্সির ইন্টারফেস (কীভাবে ডাকা হয়) হুবহু মিলিয়ে বানাতে হবে।

প্র ০৩ ডিপেন্ডেন্সি ইনজেকশন ব্যবহার করলে কি প্রোডাকশন কোডের আচরণ কোনোভাবে বদলে যায়?

না — প্রোডাকশনে notify_user_testable("rahim", "...", send_real_email)-এর মতো করে আসল send_real_email পাস করলে ফাংশনটি ঠিক আগের মতোই বাস্তব ইমেইল পাঠাবে। ডিপেন্ডেন্সি ইনজেকশন প্রোডাকশন আচরণ পাল্টায় না — এটি শুধু কোন বাস্তবায়নটি ব্যবহার হবে তা কলারের হাতে ছেড়ে দেয়, যাতে টেস্টের সময় একটি ভিন্ন (ফেক) বাস্তবায়ন বেছে নেওয়া যায়।

অনুশীলন

  1. চিন্তা করুন: আপনার পরিচিত এমন কোনো ফাংশন/ফিচারের কথা ভাবুন যা সরাসরি একটি বাস্তব ডেটাবেস বা ফাইল সিস্টেম কল করে — সেটিকে ডিপেন্ডেন্সি ইনজেকশন দিয়ে কীভাবে টেস্টযোগ্য করা যেত?

    সাধারণ প্যাটার্ন: ফাংশন/ক্লাসটিকে একটি "রিপোজিটরি" বা "স্টোরেজ" প্যারামিটার/কনস্ট্রাক্টর আর্গুমেন্ট নিতে দেওয়া (যেমন save_user(user, repository)), প্রোডাকশনে একটি বাস্তব ডেটাবেস-ব্যাকড রিপোজিটরি পাস করা, আর টেস্টে একটি সাধারণ dict-ব্যাকড ইন-মেমরি ফেক রিপোজিটরি পাস করা — যা M4/L16-এ "ফেক" নামে ফরমালি কভার হবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে NotifyUserTests-এ একটি নতুন টেস্ট মেথড যোগ করুন যা যাচাই করে fake_sender.sent_emails[0][0] (অর্থাৎ "to" আর্গুমেন্ট) ঠিক "rahim"-এর সমান — Run চেপে দেখুন এটিও পাস করে কি না।

    হ্যাঁ, এই নতুন টেস্টও পাস করবে — কারণ FakeEmailSender.__call__-এ to আর্গুমেন্টটি হুবহু sent_emails টাপলের প্রথম উপাদান হিসেবে সংরক্ষিত হয়, আর notify_user_testable("rahim", ...) কল থেকে সেই মান "rahim"-ই আসে। এটি দেখায় ফেক অবজেক্ট শুধু "কাজ চালিয়ে দেওয়া" নয়, বরং প্রকৃত কল-ডেটা নির্ভুলভাবে রেকর্ড করেও রাখে, যা পরবর্তীতে যাচাইয়ের জন্য ব্যবহার করা যায়।

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

  • পরের পাঠ L15 টেস্ট ডাবলস ওভারভিউ — ডামি, স্টাব, ফেক, স্পাই, মক — এই পাঠের "ফেক" ধারণাটি এখানে ফরমালি বিস্তারিত হবে।
  • আগের পাঠ L13 প্যারামিটারাইজড ও ডেটা-ড্রিভেন ইউনিট টেস্ট — self.subTest() দিয়ে একাধিক কেস যাচাই।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
আগের পাঠ
প্যারামিটারাইজড ও ডেটা-ড্রিভেন ইউনিট টেস্ট