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

কখন মক ব্যবহার করবেন না — ওভার-মকিং সমস্যা

When not to mock — over-mocking pitfalls
১০ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ওভার-মকিং কী, এবং এটি কীভাবে একটি টেস্টকে অকার্যকর ("মিথ্যা নিরাপত্তা") করে তোলে
  • ব্রিটল টেস্ট কী — ইমপ্লিমেন্টেশন-ডিটেইল মক করা কীভাবে নিরাপদ রিফ্যাক্টরিংকেও ব্যর্থ দেখায়
  • "শুধু সীমানার বাইরের dependency মক করুন" নীতিটি একটি কংক্রিট উদাহরণে প্রয়োগ করা
  • একই বাগের বিপরীতে ওভার-মকড টেস্ট (পাস, ভুলভাবে) বনাম ব্যালেন্সড টেস্ট (ফেইল, সঠিকভাবে) সরাসরি চালিয়ে দেখা

১ · ওভার-মকিং — যখন টেস্ট আসল কোডের বদলে মককেই টেস্ট করে

একটি ফাংশনের ভেতরে সাধারণত দুই ধরনের অংশ থাকে — নিজের আসল হিসাব-লজিক (যা টেস্ট করাই মূল লক্ষ্য), আর সীমানার বাইরের প্রকৃত dependency-র সাথে যোগাযোগ (external API, DB, ইমেইল সার্ভিস — যেগুলো ধীর/অস্থিতিশীল বলে মক করা হয়, পাঠ ১৫-১৭)। ওভার-মকিং ঘটে যখন কেউ শুধু সীমানার বাইরের অংশ নয়, ফাংশনের নিজের ভেতরের হিসাব-লজিকও মক করে ফেলে — তখন টেস্টটি প্রমাণ করে শুধু এটুকু যে "মক যা রিটার্ন করেছে, তা দিয়ে বাকি হিসাবটা ঠিক আছে," আসল লজিক আদৌ সঠিক কি না তা নিয়ে কিছু বলে না।

calculate_total() — ফাংশন-আন্ডার-টেস্টের সীমানা compute_subtotal() আসল হিসাব-লজিক এখানে মক করলে বাগ চিরদিন আড়ালে থাকে tax_service প্রকৃত External Dependency — এটাই মক করা উচিত
ভালো টেস্ট শুধু সীমানার বাইরের প্রকৃত dependency (tax_service) মক করে। সীমানার ভেতরের আসল হিসাব-লজিক (compute_subtotal) মক করে ফেলাই ওভার-মকিং — এতে সেই লজিকের যেকোনো বাগ চিরকাল অদৃশ্য থেকে যায়।

২ · কোড দিয়ে প্রমাণ — একই বাগ, দুই ভিন্ন ফলাফল

নিচে PricingCalculator-এর একটি ভাঙা সংস্করণ (PricingCalculatorBroken) আছে, যার compute_subtotal()-এ একটি বাস্তব বাগ আছে — এটি quantity সম্পূর্ণ উপেক্ষা করে। এই একই ভাঙা ক্লাসের বিপরীতে দুটো টেস্ট চালানো হয়েছে: একটি ওভার-মকড (নিজের compute_subtotal মেথডকেই মক করে দেয়), আরেকটি ব্যালেন্সড (শুধু প্রকৃত external dependency tax_service মক করে, compute_subtotal-কে তার নিজের আসল, বাগ-সহ কোড দিয়েই চলতে দেয়)।

Python
import unittest
from unittest.mock import Mock


class PricingCalculator:
    def compute_subtotal(self, price, quantity):
        return price * quantity

    def calculate_total(self, price, quantity, tax_service):
        subtotal = self.compute_subtotal(price, quantity)
        tax_rate = tax_service.get_rate()
        return subtotal + subtotal * tax_rate


class PricingCalculatorBroken(PricingCalculator):
    def compute_subtotal(self, price, quantity):
        # বাগ: quantity সম্পূর্ণ উপেক্ষা করা হয়েছে, price * quantity হওয়ার কথা ছিল
        return price


class OverMockingTests(unittest.TestCase):
    def test_over_mocked_test_still_passes_on_broken_code(self):
        calc = PricingCalculatorBroken()
        calc.compute_subtotal = Mock(return_value=1000)  # নিজের ভেতরের হিসাব-লজিকই মক করে দেওয়া হলো!
        tax_service = Mock()
        tax_service.get_rate.return_value = 0.05
        result = calc.calculate_total(100, 10, tax_service)
        self.assertEqual(result, 1050)  # আসল compute_subtotal কখনও চলেইনি, তবুও পাস করে

    def test_balanced_mocking_catches_the_real_bug(self):
        calc = PricingCalculatorBroken()
        tax_service = Mock()  # শুধু প্রকৃত external dependency (tax service) মক করা হলো
        tax_service.get_rate.return_value = 0.05
        result = calc.calculate_total(100, 10, tax_service)  # compute_subtotal-এর আসল (বাগসহ) লজিক চলবে
        self.assertEqual(result, 1050)  # ব্যর্থ হবে — বাগ ধরা পড়লো


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

    
Run চেপে দেখুন — test_over_mocked_test_still_passes_on_broken_code সত্যিই পাস করে, যদিও এটি PricingCalculatorBroken (একটি ভাঙা ক্লাস) দিয়ে চালানো! কারণ calc.compute_subtotal = Mock(return_value=1000) লাইনটি ইনস্ট্যান্স-লেভেলে আসল মেথডটিকেই ঢেকে দিয়েছে — তাই calculate_total() ভেতরে self.compute_subtotal(100, 10) ডাকলেও আসলে সবসময় বাঁধাধরা 1000ই পাওয়া যায়, price/quantity যা-ই হোক না কেন। ফলে 1000 + 1000 × 0.05 = 1050 মিলে যায় এবং টেস্টটি পাস করে — অথচ বাগযুক্ত compute_subtotal এক লাইনও চলেনি।

অন্যদিকে test_balanced_mocking_catches_the_real_bug সত্যিই ফেইল করে — এখানে শুধু tax_service মক করা হয়েছে, তাই calc.compute_subtotal(100, 10) ভাঙা ক্লাসের আসল কোড দিয়েই চলে এবং ভুলভাবে 100 রিটার্ন করে (quantity উপেক্ষা করে), ফলে চূড়ান্ত ফলাফল হয় 100 + 100 × 0.05 = 105 — যা প্রত্যাশিত 1050-এর সাথে না মিলে AssertionError দেয়। এটাই দেখায়: একই বাগ, কিন্তু ওভার-মকিং সেটা আড়াল করে দেয়, ব্যালেন্সড মকিং সেটা ধরে ফেলে।

৩ · ব্রিটলনেস — ইমপ্লিমেন্টেশন-ডিটেইল মক করার আরেকটি মূল্য

ওভার-মকিং শুধু "বাগ আড়াল করে" তা-ই নয় — এটি টেস্টকে ব্রিটল (ভঙ্গুর)ও করে তোলে। যদি ভবিষ্যতে কেউ compute_subtotal()-এর নামই পাল্টে দেয় বা এর ভেতরের হিসাব পুনর্গঠন (refactor) করে — আচরণ (input → output) অপরিবর্তিত থাকলেও — যে টেস্টগুলো compute_subtotal-কে সরাসরি মক করে রেখেছিল সেগুলো ভেঙে যাবে, কারণ মক করা মেথডের নাম বা সিগনেচার আর মেলে না। অথচ ব্যালেন্সড টেস্ট (শুধু tax_service মক করা) এই ধরনের অভ্যন্তরীণ রিফ্যাক্টরে সম্পূর্ণ অক্ষত থাকত, কারণ এটি ফাংশনের পাবলিক আচরণ টেস্ট করে, এর ভেতরের গঠন নয়।

সহজ নিয়ম

মক করুন সেই জিনিসটাকেই যা আপনার কোডের সীমানার বাইরে — নেটওয়ার্ক, ডেটাবেস, ফাইল সিস্টেম, ইমেইল/SMS গেটওয়ে, ঘড়ি/random-এর মতো নন-ডিটারমিনিস্টিক উৎস। আপনার নিজের কোডের ভেতরের হেল্পার ফাংশন/মেথড — যেগুলো টেস্ট করার কথাই ছিল — সেগুলো কখনও মক করবেন না; সেগুলোকে তাদের আসল লজিক দিয়েই চলতে দিন, প্রয়োজনে ছোট আলাদা ইউনিট টেস্টে সরাসরি টেস্ট করুন।

মূল কথা · Key takeaway

মক একটি ধারালো টুল — সঠিক জায়গায় (সত্যিকারের external dependency) ব্যবহার করলে টেস্টকে দ্রুত ও নির্ভরযোগ্য করে (পাঠ ১৬-১৭), কিন্তু ভুল জায়গায় (নিজের ভেতরের লজিক) ব্যবহার করলে টেস্টকে অর্থহীন ও ভঙ্গুর করে তোলে। প্রতিবার একটা কিছু মক করার আগে নিজেকে জিজ্ঞেস করুন — "এটা কি সত্যিই আমার কোডের বাইরের কিছু, নাকি আমি যা টেস্ট করতে চাইছি তারই একটা অংশ?"

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

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

প্র ০১ উপরের কোডে test_over_mocked_test_still_passes_on_broken_code ঠিক কেন পাস করে, যদিও এটি একটি প্রমাণিতভাবে বাগযুক্ত ক্লাস (PricingCalculatorBroken) দিয়ে চালানো হয়েছে?

কারণ calc.compute_subtotal = Mock(return_value=1000) সেই মেথডটিকেই প্রতিস্থাপন করে দিয়েছে যেখানে বাগটা আছে। ফলে calculate_total() যখন self.compute_subtotal(100, 10) কল করে, তখন সেটা আর ভাঙা কোড চালায় না — বরং সবসময় বাঁধাধরা 1000 রিটার্ন করে। টেস্টটি তখন শুধু এটাই যাচাই করে যে "1000 দিয়ে ট্যাক্স যোগ করলে 1050 হয়" — যা সবসময় সত্যি, বাগের অস্তিত্ব নির্বিশেষে।

প্র ০২ যদি PricingCalculator-এর সঠিক (বাগহীন) সংস্করণ দিয়ে দুটো টেস্টই চালানো হতো, তাহলে কি উভয় টেস্টই পাস করত? এটা কি ওভার-মকিং-এর সমস্যাটাকে মিথ্যা প্রমাণ করে?

হ্যাঁ, সঠিক ক্লাস দিয়ে চালালে দুটো টেস্টই পাস করত — কিন্তু এটাই সমস্যার আসল রূপ, মিথ্যা প্রমাণ নয়! ওভার-মকড টেস্ট সবসময়ই পাস করবে, কোডটা সঠিক থাকুক বা ভাঙা থাকুক — কারণ এটি কখনও আসল compute_subtotal চালায়ই না। একটি টেস্ট যেটা সঠিক ও ভাঙা দুটো কোডেই একই ফলাফল দেয়, সেটা আসলে কিছুই যাচাই করছে না — এটাই ওভার-মকিং-এর মূল ঝুঁকি।

প্র ০৩ ব্যালেন্সড টেস্টে (test_balanced_mocking_catches_the_real_bug) কেন tax_service এখনও মক করা হয়েছে — এটাকেও কি বাদ দিয়ে দেওয়া উচিত ছিল না, যাতে "কিছুই মক না করা" যায়?

না — tax_service একটি প্রকৃত external dependency (ধরুন এটি একটি সরকারি ট্যাক্স-রেট API কল করে)। এটাকে মক না করলে টেস্টটি real network/I/O-এর উপর নির্ভরশীল হয়ে পড়ত — যা পাঠ ১৫-এই আলোচিত সমস্যা (ধীরগতি, অস্থিতিশীলতা)। এই পাঠের নীতি "কিছুই মক করবেন না" নয় — বরং "শুধু প্রকৃত সীমানা-বহির্ভূত dependency মক করুন, নিজের ভেতরের লজিক নয়।"

অনুশীলন

  1. চিন্তা করুন: একটি টিম বলছে, "আমরা প্রতিটি ইউনিট টেস্টে ফাংশনের ভেতরে কল হওয়া প্রতিটি হেল্পার ফাংশনকে মক করে দিই, যাতে টেস্ট দ্রুত চলে এবং একে অপরের উপর নির্ভর না করে" — এই পলিসির ঝুঁকি কী?

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

  2. পরীক্ষা করুন: কোড সেলে PricingCalculatorBroken-এর বদলে সঠিক PricingCalculator ক্লাস ব্যবহার করে (calc = PricingCalculator() করে) দুটো টেস্টই আবার Run করুন — এখন কি ফলাফল বদলায়, এবং কেন এটাই প্রত্যাশিত?

    সঠিক ক্লাস দিয়ে চালালে test_balanced_mocking_catches_the_real_bug-ও এখন পাস করবে, কারণ compute_subtotal(100, 10) সঠিকভাবে 1000 রিটার্ন করবে (বাগ নেই), এবং 1000 + 1000 × 0.05 = 1050 মিলে যাবে। ওভার-মকড টেস্ট আগের মতোই পাস করতে থাকবে — পরিবর্তন হবে না, কারণ এটি আসলে কোনদিনই ক্লাসের প্রকৃত compute_subtotal চালায়নি। এটাই প্রমাণ করে ব্যালেন্সড টেস্টই একমাত্র যেটা কোডের প্রকৃত সঠিকতা/ভুলতার সাথে সংযুক্ত।

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

  • পরের পাঠ L19 ইন্টিগ্রেশন টেস্টিং স্ট্র্যাটেজি — একাধিক মডিউল একসাথে যোগ করে টেস্ট করার কৌশল, যেখানে টেস্ট ডাবলের ভূমিকা বদলে যায়।
  • আগের পাঠ L17 মক ও ইন্টারঅ্যাকশন ভেরিফিকেশন — assert_called_once_with(), call_count, call_args।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন।
আগের পাঠ
মক ও ইন্টারঅ্যাকশন ভেরিফিকেশন