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

টেস্ট ডাবলস ওভারভিউ — ডামি, স্টাব, ফেক, স্পাই, মক

Test doubles overview — dummy, stub, fake, spy, mock
১০ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টেস্ট ডাবল কী এবং কেন সরাসরি রিয়েল ডিপেন্ডেন্সি দিয়ে ইউনিট টেস্ট লেখা সমস্যাজনক
  • পাঁচ ধরনের টেস্ট ডাবল — Dummy, Stub, Fake, Spy, Mock — এর সংজ্ঞা ও একে অপরের থেকে পার্থক্য
  • প্রতিটি ধরনের আচরণ একটি একক "নোটিফায়ার" ডিপেন্ডেন্সি দৃশ্যকল্পে প্লেইন Python ক্লাস দিয়ে বাস্তবে দেখা
  • কোন পরিস্থিতিতে কোন ধরনের ডাবল বেছে নেওয়া উচিত তার একটি ব্যবহারিক সিদ্ধান্ত-নির্দেশিকা

১ · টেস্ট ডাবল কী ও কেন দরকার

ধরুন register_user(username, notifier) নামের একটি ফাংশন আছে, যেটি সফল রেজিস্ট্রেশনের পর notifier ডিপেন্ডেন্সি দিয়ে ইউজারকে একটি স্বাগত-বার্তা পাঠায়। যদি notifier একটি সত্যিকারের ইমেইল বা SMS সার্ভিস হয়, তাহলে প্রতিবার টেস্ট চালালে বাস্তবে ইমেইল/SMS পাঠানো হয়ে যাবে — যা ধীর, অস্থিতিশীল (নেটওয়ার্ক নির্ভর), ব্যয়বহুল, এবং টেস্ট পরিবেশে সম্পূর্ণ অপ্রয়োজনীয়। পাঠ ১৪-এ শেখা ডিপেন্ডেন্সি ইনজেকশন প্যাটার্নের কারণে notifier একটি parameter — তাই টেস্টে আমরা এর বদলে একটি সরল, নিয়ন্ত্রণযোগ্য টেস্ট ডাবল পাস করে দিতে পারি।

২ · পাঁচ ধরনের টেস্ট ডাবল

Dummy
শুধু parameter পূরণ করার জন্য পাস করা হয় — টেস্টের পথে কখনও আসলে ব্যবহৃত হয় না।
Stub
যা-ই জিজ্ঞেস করা হোক, সবসময় একটি বাঁধাধরা (canned) উত্তর রিটার্ন করে — বাস্তব লজিক নেই।
Fake
আসলেই কাজ করে, কিন্তু একটি সরলীকৃত বাস্তবায়ন — যেমন real DB-এর বদলে in-memory dict।
Spy
কীভাবে, কতবার, কোন আর্গুমেন্ট দিয়ে কল হয়েছে — তা রেকর্ড করে রাখে, পরে যাচাই করা যায়।
Mock
আগে থেকে একটি নির্দিষ্ট প্রত্যাশা প্রোগ্রাম করা থাকে, এবং সেই প্রত্যাশা পূরণ হলো কি না নিজেই যাচাই করতে পারে।
Stub আর Mock প্রায়ই গুলিয়ে ফেলা হয়। মূল পার্থক্য: Stub শুধু একটি প্রয়োজনীয় ইনপুট/উত্তর জোগান দেয় যাতে টেস্ট চলতে পারে (তার নিজের কোনো "যাচাই" নেই)। Mock-এর নিজস্ব একটি প্রত্যাশা থাকে এবং সেই প্রত্যাশা মেলে কি না তা নিজেই বলে দিতে পারে — অর্থাৎ Mock একধরনের "স্মার্ট" Spy যেখানে verification যুক্ত থাকে।

৩ · একটি দৃশ্যকল্পে পাঁচটি ডাবল বাস্তবে

নিচের কোড সেলে একই notifier ডিপেন্ডেন্সির জন্য পাঁচটি আলাদা প্লেইন Python ক্লাস লেখা হয়েছে — প্রতিটি ভিন্ন ধরনের টেস্ট ডাবলের আচরণ দেখায়। খেয়াল করুন, এখানে unittest.mock ব্যবহার করা হয়নি — এই আচরণগুলো আগে হাতে-লেখা ক্লাস দিয়ে বোঝাই যাতে পরের পাঠে unittest.mock ঠিক কী স্বয়ংক্রিয় করে দিচ্ছে তা পরিষ্কার হয়।

Python
class DummyNotifier:
    """ডামি — parameter হিসেবে দিতে হয়, কিন্তু বাস্তবে কখনও ব্যবহৃত হয় না।"""
    def send(self, message):
        raise AssertionError("Dummy কখনও কল হওয়ার কথা না!")


class StubNotifier:
    """স্টাব — যা-ই পাঠানো হোক না কেন, সবসময় একটি বাঁধাধরা (canned) উত্তর রিটার্ন করে।"""
    def send(self, message):
        return True  # বাস্তব ডেলিভারি লজিক নেই, সবসময় "সফল" বলে দেয়


class FakeNotifier:
    """ফেক — সত্যিকারের কাজ করে এমন একটি সরলীকৃত বাস্তবায়ন (real network call-এর বদলে in-memory)।"""
    def __init__(self):
        self.inbox = []

    def send(self, message):
        self.inbox.append(message)
        return True


class SpyNotifier:
    """স্পাই — কল হয়েছে কি না, কতবার, কোন আর্গুমেন্ট দিয়ে — সবকিছু রেকর্ড করে রাখে।"""
    def __init__(self):
        self.calls = []

    def send(self, message):
        self.calls.append(message)
        return True


class MockNotifier:
    """মক — আগে থেকে একটি প্রত্যাশা (expectation) প্রোগ্রাম করা থাকে, এবং তা পূরণ হলো কি না যাচাই করার মেথড থাকে।"""
    def __init__(self, expected_message):
        self.expected_message = expected_message
        self.received = None

    def send(self, message):
        self.received = message
        return True

    def verify(self):
        assert self.received == self.expected_message, \
            f"প্রত্যাশা ছিল {self.expected_message!r}, পাওয়া গেছে {self.received!r}"
        print("Mock verify: OK — প্রত্যাশা অনুযায়ী কল হয়েছে")


def register_user(username, notifier):
    # ইউজারনেম ৩ অক্ষরের কম হলে রেজিস্ট্রেশন বাতিল — এক্ষেত্রে notifier কল হওয়ার কথা না
    if len(username) < 3:
        return False
    notifier.send(f"স্বাগতম, {username}!")
    return True


# ১) DUMMY — ইনভ্যালিড ইনপুটে notifier একবারও কল হয় না, তাই Dummy-এর AssertionError কখনও ওঠে না
dummy = DummyNotifier()
print("dummy কেস, রেজাল্ট:", register_user("ab", dummy))

# ২) STUB — সবসময় canned True রিটার্ন করে
stub = StubNotifier()
print("stub কেস, রেজাল্ট:", register_user("রাহিম", stub))

# ৩) FAKE — সত্যিই মেসেজ ধরে রাখে (in-memory)
fake = FakeNotifier()
register_user("করিম", fake)
print("fake.inbox:", fake.inbox)

# ৪) SPY — কতবার, কী আর্গুমেন্ট দিয়ে কল হয়েছে রেকর্ড করে
spy = SpyNotifier()
register_user("সুমি", spy)
register_user("রিমা", spy)
print("spy.calls:", spy.calls)
print("spy কল হয়েছে", len(spy.calls), "বার")

# ৫) MOCK — নির্দিষ্ট প্রত্যাশা যাচাই করে
mock = MockNotifier(expected_message="স্বাগতম, তানিয়া!")
register_user("তানিয়া", mock)
mock.verify()

    
লক্ষ্য করুন — dummy কেসে register_user("ab", dummy) ইনভ্যালিড ইউজারনেমের কারণে notifier.send() একবারও কল হয় না, তাই DummyNotifier.send()-এর AssertionError কখনও ওঠে না — এটাই Dummy-এর বৈশিষ্ট্য: থাকে, কিন্তু ব্যবহৃত হয় না। বাকি চারটি কেসে ইউজারনেম ভ্যালিড, তাই notifier.send() সত্যিই কল হয় এবং প্রতিটি ডাবল তার নিজস্ব ধরন অনুযায়ী সাড়া দেয়।

৪ · কোনটা কখন বেছে নেবেন

দ্রুত সিদ্ধান্ত-নির্দেশিকা

ডিপেন্ডেন্সিটা আসলে ব্যবহৃতই হবে না? → Dummy। শুধু একটা নির্দিষ্ট মান ফেরত দিতে হবে যাতে ফাংশন চলতে পারে? → Stub। ডিপেন্ডেন্সির বাস্তব আচরণ (state রাখা, খুঁজে বের করা) দরকার কিন্তু real I/O ছাড়া? → Fake। ডিপেন্ডেন্সি ঠিকভাবে কল হয়েছে কি না পরে পরীক্ষা করতে চান? → Spy। কল হওয়ার আগেই একটি নির্দিষ্ট প্রত্যাশা ঠিক করে সেটা স্বয়ংক্রিয়ভাবে যাচাই করাতে চান? → Mock। পরের পাঠে (১৬-১৭) দেখা যাবে unittest.mock.Mock এই Stub, Spy ও Mock তিনটি ভূমিকাই একটি একক টুল দিয়ে খেলতে পারে।

মূল কথা · Key takeaway

টেস্ট ডাবল মানে "নকল অবজেক্ট" নয় নিছক শব্দার্থে — এটি পাঁচটি ভিন্ন, স্পষ্টভাবে সংজ্ঞায়িত ভূমিকার একটি পরিবার, প্রতিটির নিজস্ব উদ্দেশ্য আছে। সঠিক ধরনের ডাবল বেছে নেওয়া টেস্টকে সহজ, দ্রুত ও নির্ভরযোগ্য রাখে — ভুল ধরন বেছে নিলে (যেমন যেখানে Stub যথেষ্ট সেখানে জটিল Fake বানানো) অকারণ জটিলতা তৈরি হয়।

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

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

প্র ০১ Dummy আর Stub-এর মধ্যে ঠিক কী পার্থক্য — দুটোই তো "আসল নয়" এমন অবজেক্ট?

পার্থক্যটা ব্যবহারে। Dummy শুধু parameter পূরণ করার জন্য থাকে — কোড পথে সেটার কোনো মেথড আসলে কল হওয়ার কথাই না (উপরের কোডে DummyNotifier.send() কল হলে বরং একটি error ছোঁড়ে, কারণ কল হওয়াটাই একটি বাগের লক্ষণ)। Stub-এর মেথড সত্যিই কল হয়, এবং সে একটি প্রয়োজনীয় বাঁধাধরা মান রিটার্ন করে যাতে ফাংশন-আন্ডার-টেস্ট স্বাভাবিকভাবে চলতে পারে।

প্র ০২ কেন সরাসরি একটি real ইমেইল/SMS সার্ভিস দিয়ে ইউনিট টেস্ট লেখা সমস্যাজনক?

কয়েকটি কারণে: (১) ধীরগতি — নেটওয়ার্ক কলের জন্য অপেক্ষা করতে হয়, শত শত টেস্ট চালালে মিনিট লেগে যেতে পারে; (২) অস্থিতিশীলতা — ইন্টারনেট বা তৃতীয়-পক্ষ সার্ভিস ডাউন থাকলে টেস্ট ব্যর্থ হবে, যদিও কোডে কোনো বাগ নেই (এটাই পাঠ ৩০-এ "flaky test"-এর একটি কারণ হিসেবে আলোচিত হবে); (৩) পার্শ্ব-প্রতিক্রিয়া — প্রতিবার টেস্ট চালালে বাস্তবে ইমেইল/SMS পাঠানো হয়ে যাওয়া, যা টেস্ট পরিবেশে একদমই অনাকাঙ্ক্ষিত।

প্র ০৩ MockNotifier.verify() মেথডটি ঠিক কী কাজ করে, এবং এটি কেন শুধু "রিটার্ন ভ্যালু চেক করা" থেকে আলাদা একটি ক্ষমতা যোগ করে?

verify() শুধু register_user-এর রিটার্ন ভ্যালু দেখে না — এটি পরীক্ষা করে notifier.send()-কে ঠিক কী বার্তা দিয়ে কল করা হয়েছিল তা প্রত্যাশার সাথে মেলে কি না। অর্থাৎ এটি ডিপেন্ডেন্সির সাথে ইন্টারঅ্যাকশনের বিস্তারিত যাচাই করে, শুধু চূড়ান্ত ফলাফল নয় — এই ধারণাটিই পাঠ ১৭-এ unittest.mock-এর assert_called_once_with() দিয়ে অনেক বেশি শক্তিশালীভাবে করা হবে।

অনুশীলন

  1. চিন্তা করুন: একটি বাস্তব ই-কমার্স অ্যাপে "পেমেন্ট গেটওয়ে" ও "লগার" — এই দুটো ডিপেন্ডেন্সির জন্য ইউনিট টেস্টে কোন ধরনের ডাবল সবচেয়ে উপযুক্ত হবে বলে মনে হয়, এবং কেন?

    পেমেন্ট গেটওয়ের ক্ষেত্রে প্রায়ই Mock ব্যবহার করা হয়, কারণ পরীক্ষা করার মূল বিষয়ই হলো — সঠিক পরিমাণ ও ঠিক একবার চার্জ করার কল হয়েছে কি না তা যাচাই করা (পাঠ ১৭-এ এই ঠিক দৃশ্যকল্পটাই বিস্তারিত হবে)। লগারের ক্ষেত্রে অনেক সময় শুধু Dummy বা Stubই যথেষ্ট, কারণ বেশিরভাগ টেস্টের জন্য লগ সত্যিই লেখা হলো কি না তা গুরুত্বপূর্ণ নয় — এটি মূল লজিকের সঠিকতা যাচাইয়ে অপ্রাসঙ্গিক।

  2. পরীক্ষা করুন: উপরের কোড সেলে register_user-এর ভেতরে len(username) < 3-এর বদলে len(username) < 5 করে দিন, তারপর spy-ভিত্তিক অংশটুকু আবার Run করে দেখুন spy.calls-এ এখন কী দেখায় (মনে রাখুন "রাহিম" ৫ অক্ষরের, তাই থ্রেশহোল্ড বদলালে কোন কলগুলো প্রভাবিত হবে ভেবে দেখুন)।

    স্পাই-এ ব্যবহৃত "সুমি" ও "রিমা" দুটোই ৩ অক্ষরের/চার অক্ষরের — থ্রেশহোল্ড ৫ করলে এগুলো এখন len(username) < 5 শর্তে সত্যি হয়ে যাবে (রেজিস্ট্রেশন বাতিল), ফলে notifier.send() আর কল হবে না এবং spy.calls একটি খালি লিস্ট [] দেখাবে — এটি প্রমাণ করে Spy ঠিক কতবার ও কখন কল হয়েছে তা নির্ভুলভাবে প্রতিফলিত করে, ফাংশনের ভেতরের শর্ত পরিবর্তনের সাথে সাথেই।

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

  • পরের পাঠ L16 স্টাব ও ফেক বাস্তবে ব্যবহার — এবার সত্যিকারের unittest.mock.Mock ও একটি dict-backed ফেক রিপোজিটরি দিয়ে।
  • আগের পাঠ L14 টেস্টযোগ্য কোড লেখা — ডিপেন্ডেন্সি ইনজেকশন ছাড়া টেস্ট ডাবল প্রয়োগ করাই সম্ভব হতো না।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন।
আগের পাঠ
টেস্টযোগ্য কোড লেখা