স্টাব ও ফেক বাস্তবে ব্যবহার
এই পাঠে যা শিখবেন
unittest.mock.Mockদিয়ে কীভাবে.return_valueসেট করে একটি স্টাব তৈরি করা যায়.side_effectদিয়ে কীভাবে একটি এক্সেপশন বা ভিন্ন ভিন্ন রিটার্ন ভ্যালু সিমুলেট করা যায়- কখন প্লেইন
Mock-ই যথেষ্ট, আর কখন একটি real (কিন্তু সরল) ফেক ক্লাস লেখা ভালো সিদ্ধান্ত - একটি dict-backed ফেক রিপোজিটরি বাস্তবায়ন করে real ডেটাবেসের বদলে ইউনিট টেস্টে ব্যবহার করা
১ · স্টাব হিসেবে Mock().return_value
ধরুন should_carry_umbrella(city, weather_client) ফাংশনটি একটি external weather API
(weather_client.get_forecast(city)) কল করে বৃষ্টির সম্ভাবনা জেনে ছাতা নেওয়া উচিত কি না
সিদ্ধান্ত নেয়। ইউনিট টেস্টে real API কল করা যাবে না (ধীর, নেটওয়ার্ক-নির্ভর, এবং একই ইনপুটে বারবার একই ফলাফল
নিশ্চিত না-ও হতে পারে) — তাই weather_client-এর জায়গায় একটি Mock() পাস করে তার
.get_forecast.return_value সরাসরি সেট করে দেওয়া হয়, যাতে এটি সবসময় একটি নির্দিষ্ট বাঁধাধরা
উত্তর রিটার্ন করে — এটাই Stub-এর সংজ্ঞা, শুধু হাতে ক্লাস না লিখে।
import unittest
from unittest.mock import Mock
def should_carry_umbrella(city, weather_client):
forecast = weather_client.get_forecast(city)
return forecast["rain_chance"] >= 50
class UmbrellaAdviceTests(unittest.TestCase):
def test_high_rain_chance_recommends_umbrella(self):
stub_client = Mock()
stub_client.get_forecast.return_value = {"rain_chance": 80}
self.assertTrue(should_carry_umbrella("Dhaka", stub_client))
def test_low_rain_chance_no_umbrella(self):
stub_client = Mock()
stub_client.get_forecast.return_value = {"rain_chance": 10}
self.assertFalse(should_carry_umbrella("Dhaka", stub_client))
def test_api_failure_propagates_via_side_effect(self):
stub_client = Mock()
stub_client.get_forecast.side_effect = ConnectionError("Weather API নামানো যায়নি")
with self.assertRaises(ConnectionError):
should_carry_umbrella("Dhaka", stub_client)
suite = unittest.TestLoader().loadTestsFromTestCase(UmbrellaAdviceTests)
unittest.TextTestRunner(verbosity=2).run(suite)
.return_value ব্যবহার করে দুটো ভিন্ন পরিস্থিতি (বেশি বৃষ্টি /
কম বৃষ্টি) সিমুলেট করা হয়েছে। তৃতীয়টিতে .side_effect-এ সরাসরি একটি এক্সেপশন ইনস্ট্যান্স
বসিয়ে দেওয়া হয়েছে — Mock যখন দেখে side_effect একটি এক্সেপশন, তখন কল হওয়ার সাথে
সাথে সেটি রেইজ করে দেয়, রিটার্ন করার বদলে। এভাবে "API ডাউন থাকলে কী হয়" এই পরিস্থিতিও real API ছাড়াই
নির্ভরযোগ্যভাবে টেস্ট করা যায়।
২ · ফেক হিসেবে একটি dict-backed রিপোজিটরি
সব জায়গায় Mock যথেষ্ট নয়। ধরুন register_and_greet(user_id, name, repository)
ফাংশনটি প্রথমে চেক করে ইউজার ইতিমধ্যে আছে কি না (repository.exists()), তারপর না থাকলে সেভ করে
(repository.save())। এখানে ডিপেন্ডেন্সিটার সত্যিকারের state দরকার — একবার সেভ করা
ইউজার পরের কলে "exists" হিসেবে দেখাতে হবে। এটা Mock দিয়ে করা যাবে (কল-সংখ্যা গুনে
side_effect-এ কৃত্রিমভাবে বসিয়ে), কিন্তু তার চেয়ে অনেক সহজ ও পরিষ্কার সমাধান — একটি ছোট্ট,
সত্যিই কাজ করা Fake ক্লাস লেখা, যা ভেতরে শুধু একটি সাধারণ Python dict ব্যবহার
করে real ডেটাবেসের বদলি হিসেবে কাজ করে।
import unittest
class FakeUserRepository:
"""ফেক — বাস্তব ডেটাবেসের বদলে in-memory dict, কিন্তু আসলেই কাজ করে।"""
def __init__(self):
self._data = {}
def save(self, user_id, name):
self._data[user_id] = name
def get(self, user_id):
return self._data.get(user_id)
def exists(self, user_id):
return user_id in self._data
def register_and_greet(user_id, name, repository):
if repository.exists(user_id):
return f"{name} ইতিমধ্যে নিবন্ধিত"
repository.save(user_id, name)
return f"স্বাগতম, {name}!"
class RegisterAndGreetTests(unittest.TestCase):
def setUp(self):
self.repo = FakeUserRepository() # প্রতিটি টেস্টের আগে একদম নতুন, খালি ফেক রিপোজিটরি
def test_new_user_registers_and_is_saved(self):
result = register_and_greet(1, "নাদিয়া", self.repo)
self.assertEqual(result, "স্বাগতম, নাদিয়া!")
self.assertTrue(self.repo.exists(1))
self.assertEqual(self.repo.get(1), "নাদিয়া")
def test_existing_user_not_re_registered(self):
self.repo.save(2, "ফারহান")
result = register_and_greet(2, "ফারহান", self.repo)
self.assertEqual(result, "ফারহান ইতিমধ্যে নিবন্ধিত")
suite2 = unittest.TestLoader().loadTestsFromTestCase(RegisterAndGreetTests)
unittest.TextTestRunner(verbosity=2).run(suite2)
setUp() (পাঠ ১২-এ শেখা fixture প্যাটার্ন) প্রতিটি টেস্টের আগে একটি একদম
খালি FakeUserRepository তৈরি করে দেয়, তাই দ্বিতীয় টেস্টে save(2, "ফারহান") করার
পরও প্রথম টেস্টের user_id=1 ডেটার কোনো প্রভাব পড়ে না — টেস্ট আইসোলেশন বজায় থাকে।
৩ · Stub (Mock) নাকি Fake — কীভাবে বাছবেন
যদি ডিপেন্ডেন্সিটার কাছ থেকে শুধু একটি নির্দিষ্ট, স্থির উত্তর দরকার হয় (যেমন "এই ইনপুটে এই রেসপন্স ফেরত
দাও") — Mock().return_value/.side_effect-ই যথেষ্ট এবং দ্রুততম সমাধান। কিন্তু
যদি টেস্টের যুক্তি ডিপেন্ডেন্সির একাধিক কলের মধ্যে ধারাবাহিক state-এর উপর নির্ভর করে
(সেভ করে পরে পড়া, একাধিকবার আপডেট হওয়া ইত্যাদি), একটি ছোট্ট real Fake ক্লাস লেখা টেস্টকে অনেক বেশি পড়ার
উপযোগী ও রক্ষণাবেক্ষণযোগ্য রাখে — জটিল side_effect ফাংশন দিয়ে state সিমুলেট করার চেয়ে।
unittest.mock.Mock এক লাইনে Stub-এর কাজ করে দেয় (.return_value/.side_effect),
কিন্তু state-নির্ভর আচরণের জন্য একটি ছোট্ট, সত্যিই-কাজ-করা Fake ক্লাস লেখাই প্রায়ই বেশি স্পষ্ট সমাধান। দুটোই
একই লক্ষ্যে কাজ করে — real, ধীর, অস্থিতিশীল dependency ছাড়াই দ্রুত ও নির্ভরযোগ্য ইউনিট টেস্ট।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
stub_client.get_forecast.return_value = {"rain_chance": 80} — এই লাইনটি ঠিক কী করে,
এবং get_forecast-কে যেকোনো আর্গুমেন্ট দিয়ে কল করলেই কেন একই ফলাফল আসে?
এটি Mock অবজেক্টের get_forecast নামের (স্বয়ংক্রিয়ভাবে তৈরি হওয়া) অ্যাট্রিবিউটের
return_value সেট করে দেয় — অর্থাৎ "যখনই এটাকে কল করা হবে, এই ডিকশনারিটাই রিটার্ন করবে,
আর্গুমেন্ট যা-ই হোক না কেন।" Mock ডিফল্টভাবে আর্গুমেন্টের ভিত্তিতে আলাদা আচরণ করে না —
এজন্যই এটি Stub হিসেবে এত সহজ ও সরাসরি।
প্র ০২
কেন FakeUserRepository-এর জন্য Mock-এর বদলে সত্যিই একটি ক্লাস লেখা হলো,
যেখানে পাঠের শুরুতেই বলা হয়েছে Mock অনেক কিছু স্বয়ংক্রিয় করে দেয়?
কারণ এখানে টেস্টের যুক্তি নির্ভর করে ডিপেন্ডেন্সির state-এর উপর — save() কল করার
পর exists()/get()-এ সেই একই ডেটা দেখাতে হবে। একটি প্লেইন Mock-এ
এই আচরণ পেতে হলে side_effect-এ একটি কাস্টম ফাংশন লিখে ম্যানুয়ালি state ট্র্যাক করতে হতো —
যা মূলত একটি ফেক ক্লাসই আবার লেখার শামিল, কিন্তু কম পরিষ্কারভাবে। তাই সরাসরি একটি ছোট্ট real ক্লাস
লেখাই সহজ সমাধান।
প্র ০৩
test_api_failure_propagates_via_side_effect টেস্টে side_effect-এ একটি
এক্সেপশন ইনস্ট্যান্স (যেমন ConnectionError("...")) বসানো হয়েছে, ফাংশন নয় কেন?
Mock.side_effect কয়েক রকম মান গ্রহণ করতে পারে। এটি একটি এক্সেপশন ক্লাস বা ইনস্ট্যান্স হলে,
Mock কল হওয়ার সাথে সাথে সেটি রেইজ করে দেয়। এটি একটি ফাংশন হলে, সেই ফাংশনটি একই
আর্গুমেন্ট দিয়ে কল হয় এবং তার রিটার্ন ভ্যালুই আসল রিটার্ন ভ্যালু হিসেবে ব্যবহৃত হয় (বা সেই ফাংশনও একটি
এক্সেপশন রেইজ করতে পারে)। এখানে যেহেতু কাস্টম লজিকের দরকার নেই, শুধু একটি নির্দিষ্ট এক্সেপশন রেইজ করাই
লক্ষ্য — তাই সরাসরি একটি ইনস্ট্যান্স বসানোই সবচেয়ে সংক্ষিপ্ত।
অনুশীলন
-
চিন্তা করুন: একটি "কারেন্সি কনভার্টার" ফাংশন একটি external exchange-rate API-এর উপর
নির্ভর করে। এটি টেস্ট করতে
Mockব্যবহার করলে.return_valueনাকি.side_effectবেশি উপযোগী হবে বলে মনে হয়?সাধারণ, সফল কনভার্শনের জন্য
.return_valueই যথেষ্ট (একটি নির্দিষ্ট রেট রিটার্ন করা)। তবে "API রেট আনতে ব্যর্থ হলে কী হয়" — এই এজ কেস টেস্ট করতে.side_effect-এ একটি এক্সেপশন বা টাইমআউট-জাতীয় এরর বসানো উপযোগী, ঠিক এই পাঠের তৃতীয় টেস্টের মতোই। -
পরীক্ষা করুন: দ্বিতীয় কোড সেলে একটি তৃতীয় টেস্ট মেথড যোগ করুন যেখানে একই
user_id-তে দুইবার ভিন্ন নামেregister_and_greetকল করা হয় — দ্বিতীয়বার কী মেসেজ আসে, এবংself.repo.get(user_id)-এ কোন নামটা থেকে যায় তা যাচাই করুন।দ্বিতীয়বার কল করলে
repository.exists(user_id)True রিটার্ন করবে (প্রথমবারেরsave()-এর কারণে), তাই ফাংশনটি "ইতিমধ্যে নিবন্ধিত" বার্তা রিটার্ন করবে এবংsave()আবার কল হবে না — অর্থাৎself.repo.get(user_id)-এ এখনও প্রথম নামটাই থেকে যাবে, দ্বিতীয়বারের নতুন নাম দিয়ে ওভাররাইট হবে না। এটিFakeUserRepository-এর সত্যিকারের state-ধারণ ক্ষমতার একটি সরাসরি প্রমাণ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
-
পরের পাঠ L17
মক ও ইন্টারঅ্যাকশন ভেরিফিকেশন —
assert_called_once_with()দিয়ে ডিপেন্ডেন্সি ঠিকভাবে কল হয়েছে কি না যাচাই করা। - আগের পাঠ L15 টেস্ট ডাবলস ওভারভিউ — ডামি, স্টাব, ফেক, স্পাই, মক-এর পূর্ণাঙ্গ পরিচিতি।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন।