সরাসরি পর্বে যান
পর্ব ৪৯ · ৭০-এর মধ্যে · মডিউল ৮

Test double — dummy, stub, fake, spy, mock আর কখন mock নয়

Test Doubles — Dummy, Stub, Fake, Spy, Mock, and When Not to Mock

Taught in Bangla, this lesson explains test doubles using Meszaros's five types: dummy, stub, spy, mock and fake. It compares state and behaviour verification and the classicist versus mockist schools, shows Python unittest.mock with return_value, side_effect, patch and autospec, hand-written JavaScript spies and fake clocks, and warns about over-mocking, brittle tests and mock drift.

এই পর্বে যা শিখবেন

  • একটি dependency দেখে বলা — এটি test-এ কেন সমস্যা, আর dummy, stub, spy, mock, fake-এর কোনটি মানানসই
  • State verification আর behaviour verification-এর পার্থক্য, আর classicist বনাম mockist বিতর্কের দুই পক্ষের যুক্তি
  • Python-এর unittest.mock দিয়ে stub ও mock বানানো, ঠিক জায়গায় patch করা আর autospec দিয়ে নীরব ভুল ধরা
  • JavaScript-এ হাতে লেখা spy আর fake clock দিয়ে সময়-নির্ভর কোড সত্যি অপেক্ষা ছাড়াই test করা
  • Over-mocking আর mock drift চিনে সমালোচনা করা, আর ভঙ্গুর test-কে fake-ভিত্তিক, ফল-ভিত্তিক test-এ বদলানো

১ · কেন test double — আসল সহযোগী যখন test-এর শত্রু

পর্ব ৪৬-এ unit test-এর গঠন দেখেছেন — Arrange, Act, Assert — আর জেনেছেন একটি ভালো test দ্রুত, একা চলতে পারে এমন আর প্রতিবার একই ফল দেয়। সমস্যা হলো, বাস্তব কোড একা থাকে না। একটি কাল্পনিক অনলাইন বাজার app-এর OrderService order নিতে গিয়ে payment gateway-তে টাকা কাটে, database-এ order লেখে, ক্রেতাকে SMS পাঠায় আর ঘড়ি দেখে ঠিক করে আজকের delivery সম্ভব কি না। যে কোডটি পরীক্ষা করা হচ্ছে, তাকে বলা হয় SUT (system under test); আর যাদের সে ডাকে, তারা তার সহযোগী বা collaborator.

Unit test-এ আসল সহযোগী ব্যবহার করলে চার ধরনের সমস্যা হয়:

সমস্যাউদাহরণ (কাল্পনিক অনলাইন বাজার)Test-এর ওপর প্রভাব
ধীরDatabase-এ সংযোগ, network-এর ওপারে payment APIএকটি test-এ এক সেকেন্ড মানে ৩০০টি test-এ পাঁচ মিনিট; developer test চালানো বন্ধ করে দেন
অনিশ্চিত (non-deterministic)ঘড়ি, random সংখ্যা, network-এর timeoutআজ পাস, কাল ফেল — কোড না বদলেও; একে বলে flaky test
অনুপলব্ধPayment-এর sandbox বন্ধ, অন্য দল এখনো service বানাচ্ছে, laptop-এ internet নেইনিজের কোডে কোনো ভুল না থাকলেও test চালানোই যায় না
বিপজ্জনক side effectসত্যি টাকা কাটা, সত্যি SMS বা email যাওয়া, production database-এ লেখাপ্রতিবার test চালালে খরচ, আর ভুল মানুষের ফোনে বার্তা

এর ওপর আছে আরেকটি কষ্ট: কিছু অবস্থা আসল সহযোগী দিয়ে তৈরিই করা কঠিন। “Payment gateway ৩০ সেকেন্ড পরে timeout দিলে কী হবে?” বা “রাত ১১টা ৫৯-এ order এলে delivery কোন দিনের?” — এগুলো test করতে আসল gateway-কে ধীর করা বা রাত জেগে বসে থাকা চলে না।

সিনেমার শুটিংয়ের কথা ভাবুন। পাহাড় থেকে লাফ দেওয়ার দৃশ্যে নায়ক নিজে লাফান না; লাফান একজন স্টান্ট ডাবল — দূর থেকে দেখতে নায়কের মতো, কিন্তু ঝুঁকির কাজটি নিরাপদে করার জন্য প্রশিক্ষিত। কখনো ক্যামেরায় শুধু পেছন থেকে একজনকে দাঁড় করাতে হয় — তিনি একটি সংলাপও বলেন না। কখনো নকল কাচের বোতল ভাঙা হয়, যা দেখতে আসলের মতো কিন্তু হাত কাটে না। Test double ঠিক এই ভাবনা থেকে নামকরণ করা: test-এর দৃশ্যে আসল সহযোগীর জায়গায় দাঁড়ানো একটি বদলি, যে শুধু ততটুকু করে যতটুকু এই দৃশ্যের দরকার।

Test double বসানো যায় তখনই, যখন SUT তার সহযোগী বাইরে থেকে নেয়। পর্ব ৩৭-এ দেখেছেন, constructor injection থাকলে test-এ FakeMailer দেওয়া এক লাইনের কাজ; আর পর্ব ২৮-এর dependency inversion বলেছিল, policy নির্ভর করবে নিজের ভাষায় লেখা ছোট চুক্তির ওপর — সেই চুক্তি মানলেই যেকোনো double বসে যায়। যে কোডে SmtpMailer() ভেতরে বানানো, সেখানে double বসাতে হয় জোর করে (patch দিয়ে, অংশ ৫-এ), আর পর্ব ৪৩-এ যে seam-এর কথা বলা হয়েছিল, তা খুঁজতে হয়।

মূল ধারণা

Test double একটি সাধারণ নাম — test-এর জন্য আসল সহযোগীর জায়গায় বসানো যেকোনো object. এর উদ্দেশ্য দুটি: SUT-কে বিচ্ছিন্ন করা (দ্রুত, নিশ্চিত, নিরাপদ test) আর এমন অবস্থা তৈরি করা বা দেখা যা আসল সহযোগী দিয়ে কঠিন (timeout, declined card, SMS গেল কি না দেখা)।

২ · Meszaros-এর পাঁচ double — সুনির্দিষ্ট সংজ্ঞা

২০০০ সালের XP সম্মেলনে Tim Mackinnon, Steve Freeman ও Philip Craig-এর “Endo-Testing: Unit Testing with Mock Objects” প্রবন্ধ থেকে “mock object” কথাটি ছড়ায় বলে সাধারণত উল্লেখ করা হয়। কয়েক বছরের মধ্যে নামের বিশৃঙ্খলা শুরু হয় — কেউ সব নকলকে “mock” বলেন, কেউ “stub”, কেউ “fake”, আর একই শব্দ দুই library-তে দুই জিনিস। Gerard Meszaros তাঁর xUnit Test Patterns: Refactoring Test Code (Addison-Wesley, ২০০৭) বইয়ে একটি সাধারণ নাম প্রস্তাব করেন — test double, সিনেমার stunt double-এর অনুকরণে — আর তার পাঁচটি প্রকার আলাদা করেন। একই বছর Martin Fowler তাঁর “Mocks Aren't Stubs” প্রবন্ধের হালনাগাদ সংস্করণে এই শব্দভাণ্ডার গ্রহণ করেন (প্রবন্ধটির প্রথম রূপ কয়েক বছর আগের বলে জানা যায়)। আজ অনেক বই আর দল এই নামগুলো ব্যবহার করে, যদিও library-গুলো প্রায়ই মানে না — সেটি এই অংশের শেষে।

Meszaros দুটি দরকারি শব্দ দেন। SUT তার সহযোগীর কাছ থেকে যা পায় (যেমন gateway-র “declined” উত্তর), তা indirect input — test-এর হাতে সরাসরি নেই, সহযোগীর মাধ্যমে আসে। SUT তার সহযোগীকে যা পাঠায় (যেমন SMS-এর লেখা), তা indirect output — ফেরত মানে দেখা যায় না, সহযোগীর কাছে যায়। পাঁচটি double-এর পার্থক্য বোঝার চাবি এটিই: কোনটি indirect input নিয়ন্ত্রণ করে, কোনটি indirect output দেখে।

Doubleসংজ্ঞাউদাহরণ (OrderService)যাচাই কে করে
Dummyশুধু parameter-এর জায়গা ভরে; test-এর পথে কখনো ব্যবহার হয় নাConstructor চায় একটি audit logger, কিন্তু place() তা ছোঁয় না — object() বা None দিলেই চলেকেউ না
Stubডাকলে আগে থেকে ঠিক করা উত্তর দেয় — indirect input নিয়ন্ত্রণ করেGateway সবসময় "declined" ফেরত দেয়, যেন declined পথটি test করা যায়Test — SUT-এর ফল বা অবস্থা দেখে
SpyStub-এর মতো উত্তর দিতে পারে, আর প্রতিটি ডাক ও তার argument লিখে রাখে — indirect output ধরেSMS sender একটি list-এ (phone, text) জমায়Test — পরে list দেখে
Mockআগে থেকে প্রত্যাশা জানে — কোন ডাক, কোন argument, কতবার; না মিললে নিজেই ব্যর্থতা জানায়“send("01800000000", "Order A4 paid 90 Tk") ঠিক একবার” — এই প্রত্যাশাMock নিজে (verify)
Fakeসত্যিই কাজ করে, কিন্তু production-এর অযোগ্য সংক্ষিপ্ত পথেDatabase-এর বদলে dictionary-তে order রাখা repository; fake clock; in-memory queueTest — fake-এর অবস্থা বা SUT-এর ফল দেখে

পাঁচ test double একটি বর্ণালিতে — কে কী করে, কী যাচাই হয়

ওপর থেকে নিচে: সবচেয়ে সরল থেকে আসল আচরণের সবচেয়ে কাছাকাছি

Dummy থেকে fake — test double-এর বর্ণালি সরল Dummy যাচাই নেই শুধু parameter-এর জায়গা ভরে কখনো ডাকা হয় না · object() বা None Stub State ঠিক করা উত্তর দেয় (indirect input) যাচাই: SUT-এর ফেরত মান বা অবস্থা Spy Behaviour উত্তর দেয় + প্রতিটি ডাক লিখে রাখে যাচাই: test পরে ডাকের তালিকা দেখে Mock Behaviour আগে থেকে প্রত্যাশা জানে যাচাই: mock নিজেই, না মিললে আপত্তি Fake State সত্যি কাজ করে, কিন্তু সরলভাবে in-memory DB · fake clock · fake server আসলের কাছে State verification Behaviour
ছবি ৪৯.১ — Dummy-র কোনো আচরণই নেই; fake-এর আচরণ আসলের সবচেয়ে কাছে। সবুজ দুটি (stub, fake) সাধারণত ফল বা অবস্থা দেখে যাচাইয়ে কাজে লাগে; বেগুনি দুটি (spy, mock) দেখে SUT সহযোগীকে কী বলল. Spy আর mock-এর পার্থক্য সূক্ষ্ম: spy শুধু লিখে রাখে আর বিচার করে test; mock প্রত্যাশা আগে থেকে জানে আর বিচার নিজেই করে।

নিচের কোডে একই OrderService-এর জন্য পাঁচটি double পাশাপাশি। place() gateway-তে টাকা কাটে; উত্তর "ok" হলে order রাখে আর SMS পাঠায়। Constructor একটি audit logger-ও চায়, যা শুধু refund()-এ লাগে — dummy-র জন্য আদর্শ জায়গা।

import unittest, unittest.mock as mock
class OrderService:                    # SUT: the code under test
    def __init__(self, gateway, sms, repo, audit):
        self.gateway, self.sms, self.repo, self.audit = gateway, sms, repo, audit
    def place(self, oid, phone, amount):
        if self.gateway.charge(oid, amount) != "ok": return "declined"
        self.repo.save(oid, amount)
        self.sms.send(phone, f"Order {oid} paid {amount} Tk")
        return "paid"
    def refund(self, oid): self.audit.log("refund " + oid)   # audit: only here
class StubGateway:                     # stub: canned answer to the SUT
    def __init__(self, answer): self.answer = answer
    def charge(self, oid, amount): return self.answer
class SpySms:                          # spy: records calls for later checks
    def __init__(self): self.sent = []
    def send(self, phone, text): self.sent.append((phone, text))
class InMemoryRepo:                    # fake: works, but keeps rows in memory
    def __init__(self): self.rows = {}
    def save(self, oid, amount): self.rows[oid] = amount
def svc(answer="ok", sms=None, repo=None, audit=object()):   # audit: a dummy
    return OrderService(StubGateway(answer), sms or SpySms(), repo or InMemoryRepo(), audit)
class T(unittest.TestCase):
    def test_1_dummy(self): self.assertEqual(svc().place("A1", "01700000000", 500), "paid")
    def test_2_stub(self):  self.assertEqual(svc("declined").place("A2", "01700000000", 500), "declined")
    def test_3_spy(self):
        s = SpySms(); svc(sms=s).place("A3", "01700000000", 250)
        self.assertEqual(s.sent, [("01700000000", "Order A3 paid 250 Tk")])
    def test_4_mock(self):
        m = mock.Mock(spec=["send"]); svc(sms=m).place("A4", "01800000000", 90)
        m.send.assert_called_once_with("01800000000", "Order A4 paid 90 Tk")
    def test_5_fake(self):
        r = InMemoryRepo(); svc(repo=r).place("A5", "01700000000", 700)
        self.assertEqual(r.rows, {"A5": 700})
for name in sorted(n for n in dir(T) if n.startswith("test_")):
    res = unittest.TestResult(); T(name).run(res); print(name, "ok" if res.wasSuccessful() else "FAIL")

আউটপুট:

test_1_dummy ok
test_2_stub ok
test_3_spy ok
test_4_mock ok
test_5_fake ok

test_1_dummy: svc()-এর audit একটি খালি object() — dummy; এর কোনো log method-ই নেই। Test পাস করল — অর্থাৎ place() audit ছোঁয়নি; ছুঁলে AttributeError আসত। test_2_stub: stub "declined" দিল, আর SUT ঠিক পথে গেল — gateway-কে সত্যি declined করানোর ঝামেলা নেই। test_3_spy: SMS-এর লেখা ফেরত মানে নেই, এটি indirect output; spy সেটি list-এ ধরে রাখল, আর test পরে list মিলিয়ে দেখল। test_4_mock: unittest.mock.Mock সব ডাক লিখে রাখে; assert_called_once_with জিজ্ঞেস করে “ঠিক একবার, ঠিক এই argument-এ?” spec=["send"] বলে দেয় শুধু send নামটি বৈধ। test_5_fake: repository সত্যিই order রাখে — শুধু database-এর বদলে dictionary-তে। লক্ষ করুন, প্রতিটি test-এ সব সহযোগীই double (svc() বাকিগুলো বসায়), কিন্তু প্রতিটি test একটি double-এর ভূমিকা স্পষ্ট দেখায়। একটি ছোট সতর্কতা: repo or InMemoryRepo() ধরনের কোডে খালি dictionary-র মতো “falsy” fake দিলে সেটি চুপচাপ বাদ পড়ে — এ কারণে এখানে fake-টি সাধারণ class, dict-এর subclass নয়।

নামের গোলমাল — library Meszaros-কে মানে না। Python-এর unittest.mock.Mock নামে mock, কিন্তু আচরণে অনেকটা spy: আগে থেকে প্রত্যাশা বলতে হয় না, যেকোনো ডাক মেনে নেয় আর লিখে রাখে, যাচাই হয় পরে assert_* দিয়ে। Jest-এর jest.fn()-কে documentation-এ “mock function” বলা হয়, কিন্তু সেটিও মূলত spy; Sinon.JS-এ spy, stub আর mock আলাদা জিনিস। কোনো দলে “এখানে mock লাগবে” বললে প্রায়ই বোঝায় “এখানে যেকোনো একটি double লাগবে”। তাই আলোচনায় নাম নয়, ভূমিকা জিজ্ঞেস করুন: এটি কি উত্তর জোগায়, ডাক ধরে রাখে, নিজে যাচাই করে, না সত্যি কাজ করে? পর্ব ৩৯-এ presenter test করতে যে view বসানো হয়েছিল, যা শুধু কী দেখাতে বলা হলো তা লিখে রাখে — Meszaros-এর ভাষায় সেটি ছিল spy.

৩ · কোন dependency-তে কোন double

পাঁচটি সংজ্ঞা মুখস্থ করার চেয়ে বেশি কাজের হলো প্রতিটি সহযোগীর সামনে দুটি প্রশ্ন করা: (১) এই test-এ সহযোগীটি কি আদৌ ব্যবহার হয়? (২) এটি SUT-কে কিছু দেয় (indirect input), না SUT এর কাছে কিছু পাঠায় যা যাচাই করতে হবে (indirect output), না দুটিই অনেকবার করে? নিচের stepper-এ কাল্পনিক অনলাইন বাজারের OrderService-এর পাঁচটি সহযোগী একে একে বিচার করা হলো।

  1. audit logdummyব্যবহারই হয় না
  2. paymentstubউত্তর জোগায়
  3. SMSspy / mockবার্তাই ফল
  4. repositoryfakeলেখে ও পড়ে
  5. clockfake clockসময় ঠিক করে
কাল্পনিক অনলাইন বাজার · OrderService
সহযোগী
প্রশ্ন ১: ব্যবহার হয়?
প্রশ্ন ২: কী আদান-প্রদান
বেছে নেওয়া double
Test যা দেখবে
  1. Audit log — ব্যবহারই হয় না। place()-এর test-এ audit log ছোঁয়া হয় না, কিন্তু constructor চায়। তাই dummy: object() বা None. ভুল করে ব্যবহার হলে error আসবে — সেটিও একটি তথ্য।
  2. Payment gateway — SUT-কে উত্তর দেয়। Test-এর প্রশ্ন “declined হলে কী হয়?” এখানে gateway indirect input-এর উৎস, তাই stub: প্রতিটি test নিজের দরকারি উত্তর ঠিক করে দেয় — "ok", "declined", বা side_effect দিয়ে একটি timeout error.
  3. SMS sender — বার্তাটিই ফল। SMS-এর কোনো ফেরত মান SUT ব্যবহার করে না; গুরুত্বপূর্ণ হলো বার্তা গেল কি না, কোন নম্বরে, কী লেখায়। এটি indirect output, আর এর দেখা পাওয়ার একমাত্র উপায় ডাকটি ধরা — spy, বা mock-এর assert_called_once_with. এখানে behaviour verification যুক্তিসঙ্গত, কারণ ডাকটিই আচরণ।
  4. Repository — লেখা আর পরে পড়া। একই test-এ order save হয়, তারপর অন্য পথে পড়া হয়। Stub দিয়ে এটি নকল করতে গেলে প্রতিটি ডাকের উত্তর হাতে সাজাতে হয়; একটি in-memory fake নিজেই সামঞ্জস্য রাখে। Test শেষে fake-এর অবস্থা দেখা যায় — state verification.
  5. Clock — সময় নিয়ন্ত্রণ। “রাত ১১টা ৫৯-এ order এলে delivery পরদিন” — আসল ঘড়িতে এটি test করা যায় না। একটি fake clock, যার সময় test ঠিক করে দেয় আর দরকারে এগিয়ে দেয়। শুধু একটি স্থির সময় ফেরত দিলে এটি আসলে stub; timer চালাতে পারলে fake — দুই নামই প্রচলিত।
  6. পাঁচটি একসাথে। একটি test-এ dummy, stub, spy আর দুটি fake — আর প্রতিটির বাছাইয়ের কারণ লেখা যায়। মনে রাখুন, প্রথম পছন্দ সবসময় আসল object, যদি তা দ্রুত, নিশ্চিত আর নিরাপদ হয় (যেমন একটি pure হিসাবের class); double শুধু যেখানে আসলটি সমস্যা।
বাছাইয়ের এক লাইনের নিয়ম

আসল যদি দ্রুত, নিশ্চিত আর নিরাপদ হয় — আসলটিই। না হলে: ব্যবহারই না হলে dummy; শুধু উত্তর লাগলে stub; বাইরে যাওয়া বার্তাই ফল হলে spy বা mock; অনেকবার লেখা-পড়া বা সময়ের মতো জটিল আচরণ হলে fake.

৪ · State বনাম behaviour verification — classicist আর mockist

একটি test পাস-ফেল ঠিক করে দুই উপায়ে। State verification-এ test কাজ শেষে দেখে কী হলো — ফেরত মান, SUT-এর অবস্থা, বা fake-এর ভেতরের অবস্থা: “order-টি repository-তে ৭০০ টাকায় আছে।” Behaviour verification-এ (একে interaction testing-ও বলা হয়) test দেখে কীভাবে হলো — SUT তার সহযোগীদের কোন method, কোন argument-এ, কতবার ডাকল: “repo.save("A5", 700) ঠিক একবার ডাকা হয়েছে।”

রান্নাঘরের রেসিপি খাতা দিয়ে একজন নতুন রাঁধুনির পরীক্ষা নিচ্ছেন। এক পরীক্ষক শুধু বিরিয়ানি চেখে দেখেন — স্বাদ, লবণ, চাল সেদ্ধ কি না (state). আরেকজন পাশে দাঁড়িয়ে খাতায় টিক দেন — আগে পেঁয়াজ, তারপর মাংস, ঠিক দুই চামচ ঘি (behaviour). প্রথমজন রাঁধুনিকে নিজের ধরনে রাঁধতে দেন; দ্বিতীয়জন ভুল ক্রম সাথে সাথে ধরেন, কিন্তু রাঁধুনি একটি ভালো নতুন কৌশল আনলেও নম্বর কাটেন। আবার কিছু কাজের “স্বাদ” নেই — অতিথিকে দাওয়াতের ফোন করা হলো কি না, তা দেখতে পাশে দাঁড়াতেই হয়।

Martin Fowler তাঁর “Mocks Aren't Stubs” প্রবন্ধে (২০০৭ সালের হালনাগাদ রূপ বহুল উদ্ধৃত) এই দুই ধরনের সাথে দুটি ধারার নাম যুক্ত করেন। Classicist ধারা (কখনো Detroit বা Chicago school বলা হয়, Kent Beck-দের প্রথম দিকের XP কাজের সূত্রে) সম্ভব হলে আসল object ব্যবহার করে, অসুবিধাজনক সহযোগীতে stub বা fake, আর বেশিরভাগ যাচাই করে ফল দেখে। Mockist ধারা (London school নামে পরিচিত; Steve Freeman ও Nat Pryce-এর Growing Object-Oriented Software, Guided by Tests, ২০০৯, এর বহুল পরিচিত বই) প্রতিটি আকর্ষণীয় সহযোগীকে mock দিয়ে বদলায় আর object-গুলোর পারস্পরিক বার্তা যাচাই করে। এই নামগুলো আর শ্রেণিবিভাগ মোটা দাগের; বাস্তবে অধিকাংশ দল দুটি মিশিয়ে চলে।

দিকClassicistMockist
Double কখনশুধু যেখানে আসলটি ধীর, অনিশ্চিত বা বিপজ্জনকSUT-এর প্রায় সব আকর্ষণীয় সহযোগীর জায়গায়
যাচাইমূলত state — ফল আর অবস্থামূলত behaviour — কোন বার্তা কাকে গেল
“Unit” মানেএক বা একাধিক class মিলে একটি আচরণ (পর্ব ৪৬-এর sociable test-এর কাছাকাছি)একটি class, সহযোগী থেকে পুরো বিচ্ছিন্ন (solitary test)
নকশায় প্রভাবআগে ভেতরের নিয়ম, পরে বাইরের স্তরবাইরে থেকে ভেতরে (outside-in): mock লিখতে লিখতে সহযোগীর interface আবিষ্কার — পর্ব ৪৮
ভুল কোথায় ধরা পড়েএকটি ভুলে অনেক test লাল হতে পারে — আসল কারণ খুঁজতে হয়ভুলটি ঠিক যে class-এ, শুধু তার test লাল — কিন্তু সহযোগীর আসল আচরণ বদলালে mock তা জানে না
Refactoring-এভেতরের গঠন বদলালেও test সাধারণত সবুজডাকের ক্রম বা ভাগ বদলালেই test ভাঙতে পারে

দুই পক্ষের সবচেয়ে ভালো যুক্তি মনে রাখা দরকার। Mockist-রা বলেন, object-oriented নকশার প্রাণ হলো object-গুলোর বার্তা, তাই বার্তাই test-এর বিষয় হওয়া উচিত; আর mock লিখতে গিয়ে সহযোগীর ছোট, ভূমিকাভিত্তিক interface আপনা থেকে বেরিয়ে আসে — Freeman, Pryce ও সহকর্মীদের ২০০৪ সালের “Mock Roles, not Objects” প্রবন্ধের মূল কথা এটিই বলে উল্লেখ করা হয়। Classicist-রা বলেন, ব্যবহারকারী ফল দেখেন, ভেতরের ডাক দেখেন না; তাই ফল-ভিত্তিক test refactoring-এ টেকে, আর mock-ভারী test অনেক সময় implementation-এর হুবহু প্রতিচ্ছবি — অংশ ৭-এ এর একটি চালানো যায় এমন প্রমাণ দেখবেন। একটি ব্যবহারিক মাঝামাঝি নিয়ম অনেক দলে চলে: query (যা উত্তর দেয়) stub করুন, তার ডাক যাচাই করবেন না; command (যা বাইরের জগৎ বদলায় — SMS, email, payment) হলে তার ডাক যাচাই করুন।

৫ · Python-এর unittest.mock — Mock, patch আর autospec

Python 3.3 থেকে standard library-তে unittest.mock আছে (Michael Foord-এর আলাদা mock library থেকে এসেছে)। এর প্রধান জিনিসগুলো:

  • Mock: একটি নমনীয় নকল object. এর যেকোনো attribute চাইলে সাথে সাথে আরেকটি Mock তৈরি হয়, আর যেকোনো ডাক মেনে নিয়ে লিখে রাখে।
  • MagicMock: Mock-এর মতোই, কিন্তু len(), for, [], with-এর মতো “magic method”-ও আগে থেকে সাজানো। সাধারণ Mock-এ len(m) দিলে TypeError; MagicMock-এ ফল 0.
  • return_value: ডাকলে কী ফেরত আসবে — stub-এর কাজ।
  • side_effect: ডাকলে একটি exception ছোড়া (ConnectionError("timeout")), একটি তালিকা থেকে ক্রমে একেকটি মান দেওয়া ([1, 2] — তালিকা ফুরালে StopIteration), বা একটি function চালিয়ে argument অনুযায়ী উত্তর দেওয়া।
  • যাচাই: call_count, call_args, call_args_list, mock_calls; আর assert_called_once_with, assert_called_with (শুধু শেষ ডাক), assert_any_call, assert_has_calls, assert_not_called.

নিচের stepper-এ একটি Mock লাইন ধরে ধরে কী লিখে রাখে, আর শেষে যাচাই কীভাবে ব্যর্থ হয়, তা দেখুন। সব মান Python ৩.১৪-এ চালিয়ে মেলানো।

1sms = Mock()2sms.send.return_value = "queued"3sms.send("017", "paid")4sms.send("018", "late")5sms.send.assert_called_with("018", "late")6sms.send.assert_called_once_with("018", "late")
Mock-এর ভেতরের খাতা
call_count
call_args
mock_calls
  1. খালি Mock. এখনো কোনো ডাক নেই: call_count 0, call_args None, mock_calls খালি তালিকা।
  2. Stub-এর মতো উত্তর ঠিক করা। sms.send চাওয়ামাত্র একটি child mock তৈরি হলো, আর তার return_value হলো "queued". এটি ডাক নয়, তাই খাতায় কিছু উঠল না।
  3. প্রথম ডাক। SUT send("017", "paid") ডাকল; ফেরত এল 'queued'. খাতায় উঠল: count 1, শেষ argument call('017', 'paid'), আর parent-এর mock_calls-এ call.send(...).
  4. দ্বিতীয় ডাক। send("018", "late"). Count 2; call_args সবসময় শেষ ডাকটি দেখায়; mock_calls পুরো ইতিহাস, ক্রমসহ।
  5. assert_called_with — পাস। এটি শুধু শেষ ডাকটি মেলায়। আগে কী হয়েছিল, কতবার হয়েছিল, দেখে না — দুর্বল যাচাই।
  6. assert_called_once_with — ব্যর্থ। Argument মিলেছে, কিন্তু ডাক ২ বার। বার্তায় পুরো তালিকাও আসে: Calls: [call('017', 'paid'), call('018', 'late')]. “একই SMS দুবার গেল” ধরনের bug এভাবেই ধরা পড়ে।

Patch — “যেখানে খোঁজা হয়, সেখানে”

SUT যদি সহযোগী constructor-এ না নিয়ে নিজেই import করে ব্যবহার করে, তখন unittest.mock.patch দিয়ে test চলাকালে নামটি সাময়িক বদলানো যায় — একে monkey patching বলে। Python documentation-এ “Where to patch” নামে আলাদা অংশ আছে, কারণ এখানেই প্রায় সবাই একবার ভুল করেন। মূল নিয়ম: patch করুন সেই নামটি, যা SUT চালানোর সময় খোঁজে — যেখানে object-টি সংজ্ঞায়িত, সেখানে নয়। কেন, তা বুঝতে Python-এর import-এর আচরণ দেখুন।

  1. sms_libsend_smsসংজ্ঞা এখানে
  2. orderssend_sms (কপি)from … import
  3. notify()orders-এ খোঁজেচালানোর সময়
কোন নাম কোন object-কে দেখায়
sms_lib.send_sms
orders.send_sms
notify() যা পায়
mock called?
  1. Import-এর মুহূর্ত। orders.py-এ লেখা from sms_lib import send_sms. Python orders-এর নিজের namespace-এ send_sms নামে একটি নতুন নাম বানায়, যা ঠিক সেই মুহূর্তের আসল function-টিকে দেখায়। এখন দুটি নাম, একটি object.
  2. ভুল জায়গায় patch. patch("sms_lib.send_sms") শুধু sms_lib-এর নামটি Mock-এ বদলায়। orders-এর কপি আগের মতোই আসল function দেখায় — কেউ তাকে বলেনি।
  3. notify() চলল — আসল SMS! Function-এর ভেতরে send_sms খোঁজা হয় orders-এর global namespace-এ, তাই আসলটি চলল। Mock কখনো ডাক পেল না। বাস্তবে এটি নীরব বিপদ — test হয়তো পাস করে, কিন্তু সত্যি SMS যায় বা সত্যি network-এ ডাক যায়।
  4. Patch শেষ। with block থেকে বেরোলে sms_lib.send_sms আবার আসল function-এ ফিরল — patch সবসময় সাময়িক।
  5. ঠিক জায়গায় patch. patch("orders.send_sms") — ঠিক যে নামটি notify() খোঁজে। এবার ফল fake, আর mock ডাক পেল।
  6. অন্য import-এর ধরন। যদি orders.py-এ থাকত import sms_lib আর ডাকা হতো sms_lib.send_sms(...), তবে খোঁজা হতো চালানোর সময় sms_lib-এর ভেতরে — তখন patch("sms_lib.send_sms")-ই ঠিক। নিয়ম একটাই: নামটি কোথায় খোঁজা হয়, দেখুন।

নিচের কোডে দুটি module (types.ModuleType দিয়ে এক file-এর ভেতরে বানানো, যেন Wandbox-এ চালানো যায়) দিয়ে ঠিক এই ঘটনাটি, আর তারপর autospec-এর কাজ। সাধারণ Mock যেকোনো নাম মেনে নেয় — SUT-এ send-এর বদলে sned লিখলেও কোনো error আসে না। create_autospec আসল class দেখে mock বানায়, তাই অস্তিত্বহীন method আর ভুল argument সাথে সাথে ধরা পড়ে।

import sys, types
from unittest import mock

def make_module(name, source):         # stands in for two real files
    mod = types.ModuleType(name); sys.modules[name] = mod
    exec(source, mod.__dict__); return mod

make_module("sms_lib", "def send_sms(phone, text): return 'REAL SMS to ' + phone")
orders = make_module("orders", """
from sms_lib import send_sms           # copies the name into orders
def notify(phone): return send_sms(phone, 'paid')
""")

with mock.patch("sms_lib.send_sms", return_value="fake") as m:
    print("A patch sms_lib.send_sms ->", orders.notify("017"), "| mock called:", m.called)
with mock.patch("orders.send_sms", return_value="fake") as m:
    print("B patch orders.send_sms  ->", orders.notify("017"), "| mock called:", m.called)

class SmsGateway:                      # the real class we depend on
    def send(self, phone, text): ...

loose = mock.Mock()                    # accepts ANY attribute, any args
loose.sned("017", "hi")                # typo in the code under test
print("C plain Mock, typo sned  -> no error, call_count of send =", loose.send.call_count)
strict = mock.create_autospec(SmsGateway, instance=True)
for label, call in [("typo sned ", lambda: strict.sned("017", "hi")),
                    ("missing arg", lambda: strict.send("017"))]:
    try: call(); print("D autospec,", label, "-> no error")
    except (AttributeError, TypeError) as e: print("D autospec,", label, "->", type(e).__name__ + ":", e)

আউটপুট:

A patch sms_lib.send_sms -> REAL SMS to 017 | mock called: False
B patch orders.send_sms  -> fake | mock called: True
C plain Mock, typo sned  -> no error, call_count of send = 0
D autospec, typo sned  -> AttributeError: Mock object has no attribute 'sned'
D autospec, missing arg -> TypeError: missing a required argument: 'text'

A: ভুল জায়গায় patch — আসল function চলল (REAL SMS), mock ডাকই পেল না। B: orders.send_sms patch করায় ঠিক নামটি বদলাল। C: সাধারণ Mock-এ sned নামে একটি নতুন child mock তৈরি হয়ে ডাক মেনে নিল; send একবারও ডাকা হয়নি। যদি test শুধু “কোনো error হয়নি” দেখে, তবে বানান-ভুলওয়ালা কোডও পাস। D: autospec-এ একই বানান ভুল AttributeError, আর একটি argument কম দিলে আসল signature-এর মতো TypeError. patch("orders.SmsGateway", autospec=True) লিখেও একই সুরক্ষা পাওয়া যায়। mock.Mock(spec=SmsGateway) অস্তিত্বহীন নাম ধরে, কিন্তু method-এর argument মেলায় না; autospec দুটিই করে।

Assertion-এর বানান ভুল: m.assert_called_once_wiht(...)-এর মতো ভুল একসময় নীরবে পাস করত, কারণ এটিও একটি নতুন child mock-এর ডাক! নতুন Python version-গুলোতে assert বা কাছাকাছি কিছু বানানে শুরু হওয়া অজানা নামে AttributeError আসে (কোন বানান ধরা হয়, তা version-ভেদে বেড়েছে)। তবু ভরসা autospec আর একটি অভ্যাস: প্রতিটি নতুন test একবার ইচ্ছা করে লাল করে দেখুন (পর্ব ৪৮-এর red ধাপ)।

৬ · JavaScript-এ double, fake clock — আর সময়, random ও network

JavaScript-এ object আর function হালকা, তাই অনেক সময় library ছাড়াই double লেখা যায়: একটি object literal-ই stub, একটি function যা নিজের ডাক একটি array-তে রাখে সেটিই spy. Test framework-গুলো এর জন্য তৈরি সরঞ্জামও দেয় — লেখার সময়ে যেমন Jest-এ jest.fn() (ডাক লিখে রাখে, mockReturnValue দিয়ে উত্তর ঠিক করা যায়) আর jest.useFakeTimers(), Sinon.JS-এ sinon.spy, sinon.stub, sinon.useFakeTimers, আর Node.js-এর নিজস্ব node:test runner-এ mock.fn() ও timer mock-এর ব্যবস্থা। খুঁটিনাটি API version-ভেদে বদলায়, তাই আপনার version-এর documentation দেখে নিন। ভেতরের ভাবনা বুঝতে নিচে হাতে লেখা spy আর fake clock.

কাল্পনিক একটি কোচিং সেন্টারের reminder app-এ ReminderScheduler কয়েক মিনিট পরে SMS পাঠায়। আসল setTimeout দিয়ে test করলে ৬০ মিনিটের reminder-এর জন্য ৬০ মিনিট অপেক্ষা! সমাধান: scheduler ঘড়ি আর sender দুটিই constructor-এ পায় (পর্ব ৩৭), আর test দেয় একটি fake clock, যার সময় test নিজে এগিয়ে দেয়।

// Fake clock + hand-written spy: test a reminder scheduler with no real waiting.
class FakeClock {
  constructor(start) { this.t = start; this.timers = []; }
  now() { return this.t; }
  setTimeout(fn, ms) { this.timers.push({ at: this.t + ms, fn }); }
  advance(ms) {                                  // jump forward, fire due timers in order
    const end = this.t + ms;
    this.timers.sort((a, b) => a.at - b.at);
    while (this.timers.length && this.timers[0].at <= end) {
      const next = this.timers.shift(); this.t = next.at; next.fn();
    }
    this.t = end;
  }
}
class ReminderScheduler {                        // SUT: gets clock and sender injected
  constructor(clock, send) { this.clock = clock; this.send = send; }
  remind(phone, text, minutes) {
    this.clock.setTimeout(() => this.send(phone, text, this.clock.now()), minutes * 60000);
  }
}
function makeSpy() {                             // records every call
  const spy = (...args) => spy.calls.push(args); spy.calls = []; return spy;
}
const clock = new FakeClock(0), spy = makeSpy();
const s = new ReminderScheduler(clock, spy);
s.remind("01700000000", "class at 5pm", 60);
s.remind("01800000000", "fee due", 10);
clock.advance(30 * 60000);
console.log("after 30 min:", spy.calls.length, "call(s)", JSON.stringify(spy.calls));
clock.advance(30 * 60000);
console.log("after 60 min:", spy.calls.length, "call(s), last at minute", spy.calls[1][2] / 60000);
console.log("pending timers:", clock.timers.length, "| clock now (min):", clock.now() / 60000);

আউটপুট:

after 30 min: 1 call(s) [["01800000000","fee due",600000]]
after 60 min: 2 call(s), last at minute 60
pending timers: 0 | clock now (min): 60

দুটি reminder দেওয়া হয়েছিল উল্টো ক্রমে — আগে ৬০ মিনিটের, পরে ১০ মিনিটের। advance(30 মিনিট)-এ fake clock সময় অনুযায়ী সাজিয়ে শুধু ১০ মিনিটেরটি চালাল; spy দেখাল ডাকটি এসেছে ঠিক 600000 ms-এ (১০ মিনিট)। আরও ৩০ মিনিট এগোতে ৬০ মিনিটেরটি এল। পুরো “এক ঘণ্টা” চলল মিলিসেকেন্ডের ভগ্নাংশে, প্রতিবার একই ফল — কোনো সত্যি অপেক্ষা নেই, কোনো flaky ব্যাপার নেই। এই খেলনা fake clock-এ একটি সীমা আছে: কোনো callback নিজে নতুন timer বসালে সেটি এই advance-এর ভেতরে সময়ের ক্রমে সাজানো হয় না; library-র fake timer এসব সামলায়।

সময়, random আর network — তিনটি চেনা উৎস

⏰ সময় — ঘড়ি থামান

কোডের ভেতরে datetime.now() বা Date.now() সরাসরি ডাকবেন না; একটি clock parameter নিন, test-এ স্থির সময়ের fake দিন, যেমন 2026-03-30T23:59 (বাংলাদেশ সময়)। মাসের শেষ দিন, অধিবর্ষ, time zone-এর সীমা — সব একটি সংখ্যা বদলে test. Python-এ কিছু third-party library global ঘড়িও থামিয়ে দেয়, কিন্তু injection-ই বেশি স্পষ্ট।

🎲 Random — seed বা inject

OTP, lottery বা shuffle-এর কোডে random সরাসরি নয়; একটি random-এর উৎস parameter হিসেবে নিন। Test-এ দিন স্থির seed-এর PRNG (যেমন random.Random(42)) বা একটি stub, যা ঠিক করা সংখ্যা দেয়। তাহলে “OTP সবসময় ৬ অঙ্কের” আর “একই seed-এ একই OTP” দুটোই test করা যায়।

🌐 Network — fake server

নিজের HTTP client-এর adapter-এর বদলে unit test-এ fake; adapter-এর নিজের test-এ একটি local fake server — যেমন test-এর ভেতরে চালু করা ছোট HTTP server, যা ঠিক করা JSON, ধীর উত্তর বা 500 error দেয়। আসল বাইরের service-এ শুধু অল্প কয়েকটি আলাদা integration test (পর্ব ৫১)।

৭ · কখন mock নয় — ভঙ্গুর test, নিজের নয় এমন API আর mock drift

Mock শক্তিশালী, আর শক্তিশালী জিনিসের অপব্যবহারও সহজ। তিনটি নিয়ম বহু দলে চলে:

  1. যা আপনার নয়, তা mock করবেন না — ইংরেজিতে “don't mock what you don't own”. Freeman ও Pryce-এর লেখায় বহুল পরিচিত এই উপদেশ: একটি payment SDK বা cloud SDK-র class সরাসরি mock করলে আপনি তার আচরণ অনুমান করে লিখছেন — আর SDK-র পরের version বদলালে আপনার mock জানবে না। এর বদলে নিজের ভাষায় একটি ছোট interface বানান (PaymentGateway.charge(order, amount)), তার পেছনে SDK-কে একটি adapter-এ মুড়ে দিন (পর্ব ৩৪) — পর্ব ৪০-এর port আর adapter. তারপর নিজের interface-কে mock বা fake করুন, আর adapter-টিকে আসল (বা sandbox) service-এর বিপরীতে অল্প কয়েকটি integration test-এ যাচাই করুন।
  2. Value mock করবেন না। টাকার পরিমাণ, তারিখ, একটি Item("rice", 80) — এগুলো value object, বানানো সহজ, আচরণ স্থির। এদের mock বানালে test পড়া কঠিন হয় আর আসল হিসাবের ভুল লুকায়। নিজের ছোট pure function বা হিসাবের class-ও সাধারণত আসলটিই ব্যবহার করুন।
  3. প্রতিটি ভেতরের ডাক যাচাই করবেন না। যত বেশি ডাক যাচাই, test তত বেশি implementation-এর সাথে বাঁধা। একে বলে over-mocking: আচরণ না বদলানো refactoring-এও test লাল, অথচ আসল bug-এ হয়তো সবুজ।

নিচের কোডে over-mocking-এর একটি পরিষ্কার প্রমাণ। একটি কাল্পনিক মুদি-দোকানের cart-এর মোট দাম আগে প্রতিটি পণ্যের দাম আলাদা খুঁজত (CartV1); refactoring-এর পর একবারে সব দাম আনে (CartV2) — ফল হুবহু এক। দুটি test দুই version-এর ওপর চালানো হলো।

from unittest import mock

class CartV1:                              # one price lookup per item
    def __init__(self, repo): self.repo = repo
    def total(self, ids): return sum(self.repo.get_price(i) for i in ids)

class CartV2:                              # refactor: one batch lookup, same result
    def __init__(self, repo): self.repo = repo
    def total(self, ids): return sum(self.repo.get_prices(ids).values())

class FakePriceRepo:                       # fake: in-memory, supports both methods
    def __init__(self, prices): self.prices = prices
    def get_price(self, i): return self.prices[i]
    def get_prices(self, ids): return {i: self.prices[i] for i in ids}

def test_mock_heavy(Cart):                 # pins HOW the code talks to repo
    repo = mock.Mock()
    repo.get_price.side_effect = {"rice": 80, "dal": 120}.get
    assert Cart(repo).total(["rice", "dal"]) == 200
    repo.get_price.assert_has_calls([mock.call("rice"), mock.call("dal")])

def test_state_based(Cart):                # checks only WHAT comes out
    repo = FakePriceRepo({"rice": 80, "dal": 120})
    assert Cart(repo).total(["rice", "dal"]) == 200

for Cart in (CartV1, CartV2):
    for test in (test_mock_heavy, test_state_based):
        try: test(Cart); result = "PASS"
        except Exception as e: result = f"FAIL ({type(e).__name__}: {e})"
        print(f"{Cart.__name__} {test.__name__:16} {result}")

আউটপুট:

CartV1 test_mock_heavy  PASS
CartV1 test_state_based PASS
CartV2 test_mock_heavy  FAIL (TypeError: 'Mock' object is not iterable)
CartV2 test_state_based PASS

পুরোনো কোডে দুটি test-ই সবুজ। Refactoring-এর পর mock-ভারী test লাল: get_prices কেউ সাজায়নি, তাই সাধারণ Mock একটি নতুন Mock ফেরত দিল, আর তার .values()-এর ফল যোগ করতে গিয়ে TypeError. কেউ mock-এ get_prices-ও সাজিয়ে দিলেও শেষ লাইনের assert_has_calls লাল থাকত, কারণ get_price আর ডাকা হয় না। অথচ ক্রেতার দিক থেকে কিছুই বদলায়নি — মোট ২০০। Fake-ভিত্তিক test শুধু ফল দেখে, তাই দুই version-এই সবুজ। এমন “মিথ্যা লাল” বারবার হলে দল refactoring-এ ভয় পায়, আর test-কে শত্রু ভাবতে শুরু করে — পর্ব ৪২-এ যে নিরাপদ ছোট ধাপের কথা বলা হয়েছিল, তা অসম্ভব হয়ে পড়ে।

Mock drift আর contract test

উল্টো বিপদও আছে। ধরুন, payment adapter-এর mock সবসময় "ok" ফেরত দেয়। ছয় মাস পরে আসল gateway-র দল উত্তর বদলে {"status": "SUCCESS"} করল। আপনার সব unit test সবুজ — কারণ mock এখনো পুরোনো পৃথিবীতে বাস করে — আর production-এ প্রতিটি order “declined”. একে বলে mock drift: আসল সহযোগী সরে গেছে, নকলটি সরেনি। প্রতিকার:

  • Contract test: একই test-এর সেট আসল adapter (sandbox-এ) আর fake দুটির ওপর চালান — দুটিই একই চুক্তি মানছে কি না। Fake তখন হয়ে ওঠে একটি “যাচাই করা fake”, অর্থাৎ বিশ্বাসযোগ্য। Consumer আর provider দুই দলের মধ্যে চুক্তি পরীক্ষার পদ্ধতি পর্ব ৫১-এ।
  • Autospec বা typed interface: নিজের interface-এ method-এর নাম বা argument বদলালে mock-ও সাথে সাথে ভাঙে (অংশ ৫)।
  • অল্প কিছু আসল integration test: double-ভিত্তিক হাজার test-এর পাশে আসল database বা sandbox-এর বিপরীতে কয়েকটি, যা চুক্তির বড় বদল ধরে।

আরও গভীরে: Meszaros-এর xUnit Test Patterns (২০০৭) প্রায় ৯০০ পৃষ্ঠার pattern-এর বই; test double ছাড়াও “test smell”-এর বিশদ তালিকা আছে, যেমন fragile test আর obscure test. Mockist ধারা বুঝতে Freeman ও Pryce-এর Growing Object-Oriented Software, Guided by Tests (২০০৯); classicist দৃষ্টিভঙ্গির জন্য Kent Beck-এর Test-Driven Development: By Example (২০০২)। Winters, Manshreck ও Wright সম্পাদিত Software Engineering at Google (২০২০) বইয়ের test double অধ্যায়ে আসল বাস্তবায়নকে প্রথম পছন্দ, তারপর fake, আর interaction testing-এর চেয়ে state testing-কে অগ্রাধিকার দেওয়ার পরামর্শ আছে বলে উল্লেখ করা হয়।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Mock আর stub-এর পার্থক্য? — stub SUT-কে ঠিক করা উত্তর দেয়, যাচাই হয় ফল দেখে; mock ডাকের প্রত্যাশা জানে আর ডাকটিই যাচাই করে (behaviour verification).
  • Fake কী, উদাহরণ? — সত্যি কাজ করে এমন সরল বাস্তবায়ন: in-memory repository, fake clock, local fake server.
  • Python-এ patch কোথায় করবেন? — যেখানে নামটি চালানোর সময় খোঁজা হয়; from x import f হলে ব্যবহারকারী module-এর f.
  • Over-mocking কেন খারাপ? — test implementation-এর প্রতিচ্ছবি হয়, refactoring-এ মিথ্যা লাল আর আসল bug-এ মিথ্যা সবুজ।
  • “Don't mock what you don't own” মানে? — third-party API নিজের adapter-এর পেছনে রাখুন, নিজের interface mock করুন, adapter-কে integration বা contract test দিয়ে যাচাই করুন।

৮ · AI-এর যুগে test double

AI-কে “এই class-এর unit test চাই” বললে সে প্রায়ই চোখের পলকে দশটি test দেয় — সবুজ, সুন্দর, আর অনেক সময় অকেজো। Test double-এর ক্ষেত্রে সাধারণ ভুলগুলো:

AI-এর সাধারণ ভুলকীভাবে ধরবেন
সব সহযোগী mock, আর প্রতিটি ভেতরের ডাক assert_called — implementation-এর হুবহু প্রতিচ্ছবিকোড refactor করুন (আচরণ একই রেখে) — test লাল হলে test ভঙ্গুর। আর কোডে ইচ্ছা করে bug ঢুকিয়ে দেখুন (mutation testing, পর্ব ৫০) — সবুজ থাকলে test অকেজো
Mock-এর return_value থেকেই প্রত্যাশিত ফল — “mock 150 দিল, assert 150” — আসল হিসাব কখনো চলেই নাপ্রতিটি assert-এর মান কোথা থেকে এল দেখুন; ফল যদি mock-এ বসানো সংখ্যাই হয়, test কিছুই পরীক্ষা করছে না
ভুল জায়গায় patch — test পাস করে, অথচ আসল network বা SMS-এ ডাক যায়assert_called দিয়ে নিশ্চিত হোন mock সত্যিই ডাক পেয়েছে; test চলার সময় network বন্ধ রেখে চালান
Autospec ছাড়া Mock(), ফলে অস্তিত্বহীন method-এর ডাকও পাসনিজের interface-এ create_autospec বা autospec=True চান
অস্তিত্বহীন API — ভুল matcher, অন্য library-র method, পুরোনো version-এর timer APIআপনার version-এর official documentation-এ প্রতিটি নাম মেলান; একটি throwaway branch-এ চালান
Third-party SDK-র class সরাসরি mock, তার আচরণ অনুমান করেAdapter-এর পেছনে রাখুন; SDK-র আসল আচরণ sandbox-এ একটি integration test দিয়ে যাচাই করুন
ভুল ইতিহাস: “mock শব্দ Meszaros-এর”, “Fowler test double আবিষ্কার করেছেন”মূল উৎস দেখুন: mock object ২০০০ সালের প্রবন্ধ থেকে; “test double” নাম ও পাঁচ ভাগ Meszaros-এর ২০০৭ সালের বই থেকে

AI-কে test-এর সমালোচক হিসেবে ব্যবহার করুন; কোড দেওয়ার আগে API key, token, ফোন নম্বর আর গ্রাহকের তথ্য মুছে নিন:

নিচে একটি কাল্পনিক অনলাইন বাজার app-এর OrderService আর তার unit test দিলাম (গোপন তথ্য মুছে দেওয়া)। ১. প্রতিটি test double-কে Meszaros-এর নাম দাও (dummy, stub, spy, mock, fake), আর বলো সেটি indirect input জোগাচ্ছে না indirect output ধরছে। ২. কোন test শুধু ভেতরের ডাকের ক্রম যাচাই করছে, অথচ ফল বা অবস্থা দেখলেই চলত — তালিকা দাও। SMS, email, payment-এর মতো বাইরের জগৎ বদলানো ডাক যাচাই করা ঠিক আছে, সেগুলো আলাদা চিহ্নিত করো। ৩. কোথাও কি value object বা নিজের pure হিসাবের class mock করা হয়েছে? আসলটি দিয়ে কীভাবে লেখা যায়, দেখাও। ৪. প্রতিটি patch-এর target কি সেই module, যেখানে নামটি ব্যবহার হয়? সন্দেহ হলে কারণ বলো। ৫. Third-party SDK সরাসরি mock হলে একটি adapter আর তার জন্য একটি contract test-এর রূপরেখা দাও। ৬. কোনো API-র নাম লিখলে বলো কোন library-র কোন version-এর; নিশ্চিত না হলে স্পষ্ট বলো। কোড পুরো নতুন করে লিখবে না।

AI-এর লেখা test হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১“Mock” শব্দ তো ২০০০ সাল থেকেই চলছিল। Meszaros ২০০৭ সালে “test double” নামে একটি নতুন সাধারণ শব্দ আনলেন কেন — নামের বিশৃঙ্খলা কি সত্যিই এত ক্ষতিকর?

২০০০ সালের XP সম্মেলনে Mackinnon, Freeman ও Craig-এর “Endo-Testing” প্রবন্ধে mock object-এর একটি নির্দিষ্ট অর্থ ছিল: এমন নকল, যা ভেতরে প্রত্যাশা রাখে আর SUT-এর ডাক যাচাই করে। কিন্তু শব্দটি জনপ্রিয় হতেই অর্থ ছড়িয়ে পড়ে। Java জগতে একের পর এক library আসে — EasyMock, jMock, পরে Mockito — যাদের প্রত্যেকে “mock” শব্দ ব্যবহার করে, কিন্তু আচরণ আলাদা: কোনোটিতে আগে প্রত্যাশা বলতে হয় আর অপ্রত্যাশিত ডাকে সাথে সাথে ব্যর্থতা (strict), কোনোটি সব ডাক মেনে নিয়ে পরে যাচাই করতে দেয়। একই সময়ে “stub”, “fake”, “dummy” শব্দগুলো দলভেদে আলাদা অর্থে চলতে থাকে।

এর ক্ষতি বাস্তব। Code review-তে একজন বলছেন “এখানে mock লাগবে”, আরেকজন বুঝছেন “একটি in-memory database”, তৃতীয়জন “প্রতিটি ডাক যাচাই”। বই আর প্রবন্ধে একই উপদেশ — যেমন “mock বেশি ব্যবহার করবেন না” — পাঠকভেদে ভিন্ন অর্থ পায়। আর সবচেয়ে বড় কথা, “mock” শব্দ সবকিছুর জন্য চললে যাচাইয়ের ধরন নিয়ে আলোচনাই হারিয়ে যায়: stub দিয়ে state দেখা আর mock দিয়ে behaviour দেখা — দুটি মৌলিকভাবে আলাদা নকশার সিদ্ধান্ত।

Meszaros-এর সমাধান ছিল দুই স্তরের নাম। ওপরে একটি নিরপেক্ষ সাধারণ শব্দ — test double, সিনেমার stunt double-এর অনুকরণে, যা কারও ঘাড়ে বিশেষ অর্থ চাপায় না। নিচে পাঁচটি ভূমিকা, প্রতিটির সংজ্ঞা কাজ দিয়ে: জায়গা ভরা, উত্তর দেওয়া, ডাক ধরা, প্রত্যাশা যাচাই, সরল বাস্তবায়ন। Fowler একই বছর তাঁর প্রবন্ধের হালনাগাদে এই নাম গ্রহণ করায় শব্দভাণ্ডারটি দ্রুত ছড়ায় বলে মনে করা হয়। তবু library-গুলো নিজেদের পুরোনো নাম রেখে দেয় — Python-এর Mock বা Jest-এর “mock function” আচরণে spy-র কাছাকাছি। Gang of Four-এর pattern-এর বইয়ের মতোই (পর্ব ৩২) এখানেও বড় অবদান ছিল একটি সাধারণ ভাষা — যা দিয়ে দুজন অপরিচিত developer এক মিনিটে একই জিনিস বোঝেন।

মূল উপলব্ধি: “Test double” নামটি এসেছিল একটি ঘোলা শব্দ (“mock”) থেকে পাঁচটি আলাদা ভূমিকা আলাদা করতে; library-র নাম যা-ই হোক, আলোচনায় ভূমিকা ধরে কথা বললে ভুল বোঝাবুঝি কমে।

প্র ০২Classicist আর mockist — কোন ধারা ঠিক? একই দলে দুই ধারার মানুষ থাকলে কীভাবে মিলিয়ে চলবেন?

প্রশ্নটি অনেকটা “ব্যাটসম্যান আক্রমণাত্মক খেলবেন, না রক্ষণাত্মক?” — উত্তর নির্ভর করে পিচ আর পরিস্থিতির ওপর। Mockist ধারার শক্তি নকশায়। Freeman ও Pryce দেখান, বাইরের স্তর থেকে শুরু করে (outside-in, পর্ব ৪৮) একটি class-এর test লিখতে গেলে তার সহযোগী এখনো নেই; mock দিয়ে সেই সহযোগীর দরকারি interface “আবিষ্কার” করা হয় — ছোট, ভূমিকাভিত্তিক, ঠিক যতটুকু লাগে। এতে পর্ব ২৮-এর interface segregation আপনা থেকে আসে। আর একটি ভুল হলে শুধু সেই class-এর test লাল হয়, তাই কারণ খুঁজতে হয় না।

Classicist ধারার শক্তি স্থায়িত্বে। Test ফল দেখে বলে ভেতরের নকশা বদলানো যায় নির্ভয়ে; একটি test অনেক class একসাথে চালায় বলে class-গুলোর মধ্যের জোড়াও পরীক্ষিত হয় — mock-ভারী suite-এ যে জোড়া কখনো চলেই না। দুর্বলতা: একটি মৌলিক class-এ bug হলে বহু test একসাথে লাল, আর আসল জায়গা খুঁজতে হয়; আবার নতুন নকশায় সহযোগী তৈরির আগে test লেখা কঠিন।

অধিকাংশ অভিজ্ঞ দল তাই মিশ্র পথে চলে। কয়েকটি ব্যবহারিক চুক্তি সাহায্য করে: (১) নিজের domain-এর ভেতরের object-গুলো আসল ব্যবহার করুন; (২) system-এর সীমানায় (database, network, ঘড়ি, বার্তা) double — আর সম্ভব হলে fake; (৩) query stub করুন, command-এর ডাক যাচাই করুন; (৪) outside-in নকশার সময় mock দিয়ে interface খুঁজে নেওয়ার পর, সহযোগী বানানো হয়ে গেলে কিছু test আসল সহযোগী দিয়ে চালান; (৫) “unit” শব্দের অর্থ দলে লিখে রাখুন — পর্ব ৪৬-এর solitary আর sociable test-এর পার্থক্য এখানেই কাজে লাগে।

বিতর্কটি অনেক সময় মানুষের মধ্যে আবেগী হয়ে ওঠে, কারণ দুই পক্ষই নিজেদের অভিজ্ঞতায় অন্যটির খারাপ দিক দেখেছে — একজন ভঙ্গুর mock-এর জঙ্গল, আরেকজন ধীর, জট-পাকানো integration test. Code review-তে ধারার নাম না টেনে নির্দিষ্ট প্রশ্ন করুন: “এই refactoring-এ এই test ভাঙবে কি?”, “এই bug ধরা পড়বে কি?”

মূল উপলব্ধি: দুই ধারাই একেক ধরনের ঝুঁকি কমায় — mockist নকশা আর ভুলের অবস্থান, classicist স্থায়িত্ব আর জোড়ার পরীক্ষা; সীমানায় double আর ভেতরে আসল object — এই মিশ্রণই বেশিরভাগ দলের বাস্তব উত্তর।

প্র ০৩“যা আপনার নয়, তা mock করবেন না” — কিন্তু একটি payment SDK তো test-এ চালানো যায় না। তাহলে third-party API-নির্ভর কোড test হবে কীভাবে?

উপদেশটি “third-party কোড test করবেন না” নয়; বরং “third-party-র আকৃতি-র ওপর আপনার test বানাবেন না”। ধরুন, একটি কাল্পনিক payment SDK-র class PayClient, যার create_charge(amount_paisa, currency, metadata, idempotency_key) একটি জটিল response object দেয়। আপনি যদি business কোডে সরাসরি এটি ডাকেন আর test-এ PayClient mock করেন, তবে mock-এ আপনি SDK-র আচরণ নিজের অনুমানে লিখছেন: কোন field-এ status, error কোন exception-এ, timeout কেমন। অনুমান ভুল হলে test সবুজ, production লাল। আর SDK-র নতুন version-এ method-এর নাম বা response বদলালে আপনার শত শত test-এর mock একসাথে বদলাতে হয়।

সমাধান তিন স্তরে। প্রথমত, নিজের ভাষায় একটি ছোট port: PaymentGateway.charge(order_id, amount) -> ChargeResult, যেখানে ChargeResult আপনার নিজের value object — “ok”, “declined”, “retry later”. দ্বিতীয়ত, একটি adapter (পর্ব ৩৪), যা এই port বাস্তবায়ন করে আর ভেতরে SDK ডাকে — টাকাকে পয়সায় রূপান্তর, SDK-র exception-কে আপনার ফলে রূপান্তর, idempotency key বানানো। তৃতীয়ত, test দুই ভাগে: business কোডের হাজারো unit test port-এর fake বা stub দিয়ে — দ্রুত, নিশ্চিত, আর আপনার নিজের চুক্তিতে লেখা; আর adapter-এর অল্প কয়েকটি integration test, SDK-র sandbox বা একটি local fake server-এর বিপরীতে।

এতে কয়েকটি লাভ। SDK বদলালে (বা অন্য provider-এ গেলে) শুধু adapter আর তার test বদলায়; business test অক্ষত (পর্ব ৪০-এর hexagonal ভাবনা)। Port-এর fake-এর জন্য একটি contract test লেখা যায় — একই test fake আর আসল adapter দুটিতে চালিয়ে fake-কে সৎ রাখা (পর্ব ৫১)। আর port ছোট বলে fake লেখা সহজ — SDK-র পঞ্চাশটি method নয়, আপনার দরকারি দুটি।

ব্যতিক্রমও আছে। খুব পাতলা, স্থিতিশীল, ভালোভাবে নথিভুক্ত standard library-র কিছু জিনিস (যেমন file system-এর একটি function) অনেক দল সরাসরি patch করে, আর তাতে বড় ক্ষতি হয় না। আবার কিছু SDK নিজেরাই test-এর জন্য fake বা local emulator দেয় — সেটি SDK-র মালিকের লেখা, তাই অনুমানের ঝুঁকি কম; তবু কতটা আসলের মতো, তা যাচাই করে নেওয়া ভালো।

মূল উপলব্ধি: নিজের নয় এমন API-কে নিজের ছোট port আর adapter-এর পেছনে রাখুন; business test port-এর double দিয়ে, adapter-এর test আসল বা sandbox-এর বিপরীতে — তাহলে অনুমান নয়, চুক্তি পরীক্ষিত হয়।

প্র ০৪দুই হাজার unit test সবুজ, অথচ deploy-এর পর প্রথম ঘণ্টায় checkout বন্ধ। Mock-ভিত্তিক suite কীভাবে এমন মিথ্যা নিশ্চয়তা দিতে পারে, আর কী করলে এড়ানো যায়?

এই গল্পটি অনেক দলে কোনো না কোনো রূপে ঘটে (এখানে বর্ণনা কাল্পনিক)। Test suite-এর প্রতিটি test সহযোগীদের mock দিয়ে চলে। Inventory service-এর দল একটি field-এর নাম qty থেকে quantity করল, আর ধরে নিল consumer-রা দেখে নেবে। Checkout-এর mock এখনো {"qty": 3} ফেরত দেয়, তাই সব test সবুজ; production-এ qty নেই, checkout ভাঙল। এটি mock drift-এর সবচেয়ে সাধারণ রূপ: double একটি পুরোনো ছবি, আর কেউ তাকে হালনাগাদ করার দায়িত্বে নেই।

আরও কয়েকটি পথে mock মিথ্যা নিশ্চয়তা দেয়। (১) ভুল জায়গায় patch — test পাস করে, কিন্তু SUT আসলে আসল function ডাকছে বা কিছুই ডাকছে না, আর কেউ assert_called দেখেনি। (২) Spec-ছাড়া Mock — SUT একটি অস্তিত্বহীন method ডাকে, mock খুশিমনে মেনে নেয়। (৩) Mock-এর return_value-ই প্রত্যাশিত মান — আসল হিসাব কখনো চলে না। (৪) দুই double-এর মাঝখানের জোড়া কখনো পরীক্ষিত নয় — A-র test-এ B mock, B-র test-এ A mock, কিন্তু A আর B একসাথে কখনো চলেনি। প্রতিটি অংশ একা ঠিক, জোড়া ভুল।

প্রতিকার একক নয়, স্তরে স্তরে। প্রথমত, double-কে চুক্তির সাথে বাঁধুন: নিজের interface-এ autospec বা static type, যাতে নাম বা signature বদলালে mock-ও ভাঙে। দ্বিতীয়ত, contract test: consumer যে আকৃতির উত্তর আশা করে, তা লিখিত চুক্তি হিসেবে provider-এর build-এ চলে; provider চুক্তি ভাঙলে তাদের build লাল — consumer-এর production-এ পৌঁছানোর আগেই (পর্ব ৫১)। তৃতীয়ত, fake-কে যাচাই করা fake বানান: একই test আসল adapter আর fake দুটিতে চলে। চতুর্থত, test pyramid-এর ওপরের স্তরে অল্প কয়েকটি আসল integration আর end-to-end test, যা জোড়াগুলো সত্যি চালায়। পঞ্চমত, mutation testing (পর্ব ৫০) দিয়ে দেখুন test সত্যিই ভুল ধরে কি না।

সংখ্যার ভ্রম থেকেও সাবধান থাকুন। “দুই হাজার test” বা “৯০% coverage” প্রতিবেদনে ভালো দেখায়, কিন্তু coverage শুধু বলে কোন লাইন চলেছে, তা ঠিক কি না নয়। Mock-ভারী suite উচ্চ coverage দিয়েও আসল জোড়ার একটি লাইনও পরীক্ষা না করতে পারে।

মূল উপলব্ধি: Double একটি অনুমান; অনুমান যাচাই না করলে সবুজ test শুধু নিজের সাথে কথা বলে। Autospec, contract test, যাচাই করা fake আর অল্প কিছু আসল integration test মিলে অনুমানকে বাস্তবের সাথে বেঁধে রাখে।

প্র ০৫সময় আর random-নির্ভর test কেন সবচেয়ে বেশি flaky হয়? “ঘড়ি থামানো” কি সব সমস্যার সমাধান, না নিজেই নতুন ফাঁদ?

সময় এমন একটি dependency, যা প্রায় সবাই ভুলে যায় যে dependency. datetime.now() দেখতে একটি নিরীহ function, কিন্তু প্রতিবার আলাদা উত্তর দেয় — অর্থাৎ এটি একটি অনিয়ন্ত্রিত indirect input. ফলে এমন test তৈরি হয়, যা দিনের বেলায় পাস করে আর রাত ১২টার কাছাকাছি ফেল করে (তারিখ বদলে যায়), মাসের ৩১ তারিখে ফেল করে, বা CI server অন্য time zone-এ থাকায় শুধু সেখানে ফেল করে — বাংলাদেশের সময় UTC+06:00, আর অনেক cloud server UTC-তে চলে, তাই “আজ” দুই জায়গায় সকাল ছয়টা পর্যন্ত আলাদা তারিখ। সময়-নির্ভর আরেকটি রূপ: sleep(2) দিয়ে “background কাজ শেষ হওয়া পর্যন্ত অপেক্ষা” — ধীর যন্ত্রে দুই সেকেন্ড যথেষ্ট নয়, দ্রুত যন্ত্রে সময়ের অপচয়।

Random-এর সমস্যা আরও সরল: ফল প্রতিবার আলাদা হলে test হয় দুর্বল (শুধু “৬ অঙ্ক” দেখে) নয়তো অনিশ্চিত। আর random-নির্ভর bug — যেমন যে shuffle-এ একটি বিশেষ ক্রমেই ভুল হয় — একবার দেখা দিয়ে আর ফিরে আসে না, কারণ কেউ seed লিখে রাখেনি।

সমাধানের মূল ভাবনা একটাই: অনিয়ন্ত্রিত input-কে নিয়ন্ত্রিত করা। ঘড়ি আর random-এর উৎস parameter হিসেবে নিন (পর্ব ৩৭), test-এ fake clock আর স্থির seed-এর PRNG দিন। Fake clock-এ সময় শুধু test এগোয়, তাই “৩০ দিন পরে membership শেষ” এক মিলিসেকেন্ডে পরীক্ষা হয়। Property-based testing-এর মতো random-ভিত্তিক কৌশল ব্যবহার করলে ব্যর্থতার seed ছাপান, যাতে ঠিক সেই ক্ষেত্রটি আবার চালানো যায়।

তবে ঘড়ি থামানোরও ফাঁদ আছে। প্রথমত, global ঘড়ি patch করার library (Python-এ এমন কিছু third-party library আছে) সুবিধাজনক, কিন্তু কোন কোন জায়গায় সময় পড়া হয় তা আড়াল করে — আর কিছু জায়গা (যেমন C extension বা অন্য thread) হয়তো থামানো ঘড়ি দেখে না। দ্বিতীয়ত, শুধু একটি স্থির মুহূর্তে test করলে সীমার ক্ষেত্রগুলো বাদ পড়ে; পর্ব ৪৭-এর boundary value এখানে সরাসরি খাটে — ২৩:৫৯:৫৯, ০০:০০:০০, ২৯ ফেব্রুয়ারি। তৃতীয়ত, fake clock সত্যিকারের concurrency-র ভুল (দুটি thread-এর race) ধরে না; সেগুলোর জন্য আলাদা কৌশল লাগে। আর সবশেষে, production-এর আসল ঘড়ি-সংযোগ — time zone-এর configuration — অন্তত একটি integration test-এ আসলটি দিয়ে যাচাই করুন।

মূল উপলব্ধি: সময় আর random লুকানো indirect input; তাদের inject করে fake clock আর স্থির seed দিলে test নিশ্চিত হয় — কিন্তু সীমার মুহূর্তগুলো আলাদা test করুন, আর global patch-এর সুবিধার আড়ালে নির্ভরতা লুকাতে দেবেন না।

অনুশীলন

আউটপুট অনুমান, double বাছাই, সমালোচনা আর হিসাব — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।

  1. আউটপুট অনুমান করুন (patch-এর path): clock_lib module-এ now_hour() সবসময় 10 ফেরত দেয়। notify module দুইভাবে এটি ব্যবহার করে। চারটি print-এর আউটপুট লিখুন।
    # notify.py
    import clock_lib
    from clock_lib import now_hour
    def greet_a(): return "night" if clock_lib.now_hour() >= 22 else "day"
    def greet_b(): return "night" if now_hour() >= 22 else "day"
    
    # test
    print(greet_a(), greet_b())
    with mock.patch("clock_lib.now_hour", return_value=23):
        print(greet_a(), greet_b())
    with mock.patch("notify.now_hour", return_value=23):
        print(greet_a(), greet_b())
    print(greet_a(), greet_b())

    আসল আউটপুট (দুটি module এক file-এ বানিয়ে Python ৩.১৪-এ চালিয়ে মেলানো):

    day day
    night day
    day night
    day day
    • লাইন ১: কোনো patch নেই; দুটিই আসল 10 পায় — day day.
    • লাইন ২: clock_lib.now_hour patch হলো। greet_a চালানোর সময় clock_lib-এর ভেতরে নামটি খোঁজে, তাই 23 পায় — night. greet_b খোঁজে notify-এর নিজের now_hour-এ, যা import-এর সময় আসল function-এর কপি — day.
    • লাইন ৩: এবার উল্টো। notify.now_hour patch হওয়ায় শুধু greet_b 23 পায়; greet_a আবার আসল clock_lib.now_hour দেখে।
    • লাইন ৪: with শেষে দুটি patch-ই ফেরত গেছে — আবার day day.

    শিক্ষা: একই function, দুই import-এর ধরন, দুই আলাদা patch-এর লক্ষ্য। সন্দেহ হলে SUT-এর কোডে দেখুন নামটি কীভাবে লেখা — clock_lib.now_hour() না শুধু now_hour(). আর এই ঝামেলা এড়ানোর সবচেয়ে ভালো উপায়: ঘড়িকে parameter হিসেবে নেওয়া।

  2. সঠিক double বেছে নিন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের কোড। প্রতিটি ক্ষেত্রে বলুন dummy, stub, spy, mock, fake, না কোনো double নয় (আসলটি) — আর এক লাইনে কারণ। (ক) ResultService-এর constructor একটি Printer চায়, কিন্তু calculate_gpa() কখনো print করে না। (খ) “বিভাগের server ‘unavailable’ দিলে পোর্টাল পুরোনো cache দেখাবে” — এটি test করতে হবে। (গ) ফল প্রকাশের পর প্রতিটি ছাত্রকে ঠিক একটি SMS যাবে, সঠিক নম্বরে। (ঘ) Enrollment-এর ৪০টি test ছাত্র যোগ করে, খোঁজে, মুছে। (ঙ) “ফল প্রকাশের ১৫ দিন পর পুনর্নিরীক্ষার আবেদন বন্ধ” — এটি test করতে হবে। (চ) GPA হিসাবের একটি pure function grade_point(marks). (ছ) Marks(subject, value) নামের একটি ছোট value object.
    • (ক) Dummy — Printer এই পথে ব্যবহারই হয় না; None বা object() দিন। ভুল করে ব্যবহার হলে error এসে সেটাই জানাবে।
    • (খ) Stub — server-এর “unavailable” উত্তর একটি indirect input; stub সেটি ঠিক করে দেয় (বা side_effect দিয়ে error ছোড়ে), test দেখে পোর্টাল cache থেকে কী দেখাল — state verification.
    • (গ) Spy বা mock — SMS বাইরের জগৎ বদলানো command, আর বার্তাটিই ফল; ডাক ধরে সংখ্যা, নম্বর আর লেখা যাচাই করুন। এখানে behaviour verification যথার্থ। “ঠিক একটি” যাচাই করতে assert_called_once_with বা spy-র তালিকার দৈর্ঘ্য।
    • (ঘ) Fake — বহুবার লেখা-পড়া-মোছা; stub দিয়ে প্রতিটি উত্তর সাজানো কষ্টকর আর ভঙ্গুর। একটি in-memory repository একবার লিখুন, ৪০টি test-এ ব্যবহার করুন; আসল database-এর বিপরীতে একটি contract test দিয়ে fake সৎ রাখুন।
    • (ঙ) Fake clock (স্থির সময়ের stub-ও চলে) — ফল প্রকাশ “২০২৬-০৩-০১”, তারপর clock ১৪ দিন ২৩ ঘণ্টা আর ১৫ দিন ১ মিনিটে — boundary value. ১৫ দিন অপেক্ষার প্রশ্নই নেই।
    • (চ) আসলটি — দ্রুত, নিশ্চিত, side effect নেই; mock করলে আসল হিসাবটাই পরীক্ষা হবে না।
    • (ছ) আসলটি — value mock করবেন না; Marks("Math", 78) বানানো এক লাইনের কাজ।
  3. এই test-এর সমালোচনা করুন: কাল্পনিক অনলাইন বাজারের Checkout.run() পণ্যের মোট দাম বের করে, তার ওপর VAT (এখানে কাল্পনিক ১৫%) যোগ করে order save করে। একজন developer লিখেছেন:
    def test_checkout():
        items = [Mock(price=100), Mock(price=50)]
        calc = Mock(); calc.subtotal.return_value = 150
        tax = Mock(); tax.vat.return_value = 22.5
        repo = Mock()
        Checkout(items, calc, tax, repo).run()
        calc.subtotal.assert_called_once_with(items)
        tax.vat.assert_called_once_with(150)
        repo.save.assert_called_once_with(ANY)
    Review-তে কী বলবেন, আর কীভাবে লিখবেন?
    • আসল হিসাব কখনো চলে না: subtotal 150 আর VAT 22.5 দুটিই mock-এ বসানো। Calc-এ যোগের বদলে গুণ থাকলে, বা VAT-এর হার ভুল হলে, test তবু সবুজ। Test নিজের দেওয়া সংখ্যাই নিজে মিলিয়ে দেখছে।
    • Value mock করা হয়েছে: Mock(price=100)-এর বদলে আসল Item("rice", 100) — সহজ, পড়তে স্পষ্ট।
    • নিজের pure class mock: Calc আর Tax দ্রুত, নিশ্চিত, side effect-হীন — আসলটি ব্যবহার করুন।
    • যাচাই implementation-এর প্রতিচ্ছবি: subtotal-এর পর vat — ঠিক এই ক্রম আর ভাগ। কেউ Tax-কে Calc-এর ভেতরে নিলে আচরণ একই, test লাল।
    • যা আসলে গুরুত্বপূর্ণ, সেটিই ঝাপসা: save-এ কী গেল, ANY দিয়ে লুকানো। Order-এর মোট দামই এই test-এর আসল ফল।
    def test_checkout_saves_total_with_vat():
        orders = InMemoryOrders()                         # fake: the only double
        items = [Item("rice", 100), Item("dal", 50)]      # real values
        Checkout(items, Calc(), Tax(0.15), orders).run()  # real calculators
        assert orders.saved == [{"total": 172.5}]         # 150 + 15% VAT

    এই রূপটি (ছোট class-গুলোসহ) চালিয়ে মেলানো হয়েছে: ফল ১৫০ + ২২.৫ = ১৭২.৫। এখন একটিমাত্র double — যেখানে আসলটি (database) সত্যিই সমস্যা। হিসাবে ভুল ঢোকালে test লাল; ভেতরের ভাগ বদলালে সবুজ।

  4. হিসাব (ধীর আর flaky suite): একটি দলের ৩০০টি unit test প্রতিটি আসল SMS sandbox-এ ডাক পাঠায়। প্রতিটি ডাকে গড়ে ০.৮ সেকেন্ড লাগে, আর network-এর কারণে প্রতিটি ডাক স্বাধীনভাবে ০.৫% সম্ভাবনায় ব্যর্থ হয়। (ক) পুরো suite-এ কত সময় লাগে? Fake দিলে প্রতিটি test ২ মিলিসেকেন্ডে চললে কত? (খ) কোডে কোনো bug না থাকলেও একবার চালানোয় পুরো suite সবুজ হওয়ার সম্ভাবনা কত? (গ) দিনে ২০ বার CI চললে গড়ে কতবার “মিথ্যা লাল” আসবে? (ঘ) ব্যর্থতার হার ০.১% হলে (খ)-এর উত্তর কত?

    (ক) ৩০০ × ০.৮ = ২৪০ সেকেন্ড = ৪ মিনিট। Fake দিলে ৩০০ × ০.০০২ = ০.৬ সেকেন্ড — ৪০০ গুণ দ্রুত। চার মিনিটের suite developer প্রতিটি ছোট বদলের পর চালান না; আধা সেকেন্ডেরটি চালান।

    (খ) প্রতিটি ডাক সফল হওয়ার সম্ভাবনা ০.৯৯৫, আর ডাকগুলো স্বাধীন, তাই সব সফল: ০.৯৯৫৩০০ ≈ ০.২২২ — মাত্র প্রায় ২২%! অর্থাৎ প্রায় ৭৮% চালানোয় অন্তত একটি test অকারণে লাল।

    (গ) ২০ × (১ − ০.২২২) ≈ ২০ × ০.৭৭৮ ≈ ১৫.৬ বার — দিনে গড়ে ১৫-১৬টি মিথ্যা লাল। এমন অবস্থায় দল লাল দেখলে আবার চালানোর অভ্যাস করে ফেলে, আর আসল ব্যর্থতাও উপেক্ষিত হয়।

    (ঘ) ০.৯৯৯৩০০ ≈ ০.৭৪১ — তবু প্রায় চারবারে একবার মিথ্যা লাল। শিক্ষা: প্রতিটি test-এর ছোট অনিশ্চয়তা suite-এর আকারের সাথে গুণিতক হারে বাড়ে। (সংখ্যাগুলো একটি ছোট script-এ হিসাব করে মেলানো।) সমাধান: unit test-এ SMS-এর জায়গায় spy বা fake; আসল sandbox-এ শুধু adapter-এর অল্প কয়েকটি আলাদা integration test, যার ব্যর্থতা আলাদা করে দেখা হয় (flaky test নিয়ে বিস্তারিত পর্ব ৫১-এ)।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPython-এর unittest.mock দিয়ে একটি SMS sender-এর double বসিয়ে test লেখার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧩 আসল SmsSender class দেখে create_autospec দিয়ে double বানানো
  2. 🎛️ দরকার হলে return_value বা side_effect দিয়ে উত্তর ঠিক করা
  3. 🚪 Double-টি constructor-এর মাধ্যমে SUT-কে দেওয়া
  4. ▶️ SUT-এর যে method পরীক্ষা হচ্ছে, তা চালানো
  5. ✅ ফল যাচাই করা, আর assert_called_once_with দিয়ে পাঠানো SMS যাচাই করা

একটি test-এ payment gateway-র জায়গায় এমন একটি object বসানো হলো, যার charge() সবসময় "declined" ফেরত দেয়; test শেষে দেখা হয় SUT "declined" ফেরত দিয়েছে কি না। Meszaros-এর শ্রেণিবিভাগে এই double কোনটি?

  • Dummy
  • Stub
  • Spy
  • Mock

orders.py-এ লেখা আছে from sms_lib import send_sms, আর notify() ফাংশন send_sms(...) ডাকে। Test-এ unittest.mock.patch দিয়ে send_sms বদলাতে হবে। কোন target ঠিক?

  • "sms_lib.send_sms" — কারণ function-টি সেখানে সংজ্ঞায়িত
  • "orders.notify"
  • "orders.send_sms" — কারণ notify() নামটি orders-এর ভেতরে খোঁজে
  • "builtins.send_sms"

SUT-এর কোডে ভুল বানানে gateway.chrage(100) লেখা। Test-এ gateway হিসেবে একটি সাধারণ unittest.mock.Mock() দেওয়া হয়েছে, আর test শুধু দেখে কোনো exception হয়নি। কী ঘটবে, আর কীভাবে ঠেকাবেন?

  • Mock সাথে সাথে AttributeError দেবে, তাই কিছু করার দরকার নেই
  • MagicMock ব্যবহার করলে বানান ভুল ধরা পড়বে
  • return_value ঠিক করলে বানান ভুল ধরা পড়বে
  • Test নীরবে পাস করবে; create_autospec দিয়ে আসল class থেকে double বানালে AttributeError ধরা পড়বে

একটি test-এ repository-কে Mock দিয়ে বদলে তিনটি ভেতরের ডাকের ক্রম assert_has_calls দিয়ে যাচাই করা হয়। কেউ আচরণ একই রেখে কোড refactor করলেন — তিনটি ডাকের বদলে একটি batch ডাক। Test লাল হলো। এর সবচেয়ে সঠিক ব্যাখ্যা কোনটি?

  • Test implementation-এর সাথে বাঁধা (over-mocking); fake দিয়ে ফল যাচাই করলে refactoring-এ টিকত
  • Refactoring-এ একটি bug ঢুকেছে, তাই test ঠিকই লাল
  • Mock-এ autospec না থাকায় এমন হয়েছে
  • Behaviour verification সবসময় ভুল, কখনো ব্যবহার করা উচিত নয়

একটি দল সরাসরি একটি third-party payment SDK-র class mock করে শত শত test লিখেছে। SDK-র নতুন version-এ response-এর গঠন বদলেছে, test সব সবুজ, production-এ payment ব্যর্থ। কোন প্রতিকার সবচেয়ে উপযুক্ত?

  • প্রতিটি test-এ আরও বেশি assert_called দিয়ে SDK-র ডাক যাচাই করা
  • SDK-কে নিজের interface-এর adapter-এ মুড়ে নিজের interface-এর double ব্যবহার, আর adapter-কে sandbox-এর বিপরীতে contract বা integration test দিয়ে যাচাই
  • সব double বাদ দিয়ে প্রতিটি unit test আসল payment service-এ চালানো
  • Mock-এর বদলে MagicMock ব্যবহার করা

একটি function আজকের তারিখ দেখে বলে membership শেষ কি না, আর test কখনো কখনো CI server-এ রাত ১২টার কাছাকাছি ফেল করে। সবচেয়ে ভালো নকশাগত সমাধান কোনটি?

  • CI-তে ব্যর্থ test স্বয়ংক্রিয়ভাবে তিনবার পর্যন্ত আবার চালানো
  • Test-এর শুরুতে কিছুক্ষণ sleep দেওয়া
  • Function-কে একটি clock বাইরে থেকে দেওয়া (inject), আর test-এ স্থির সময়ের fake clock দেওয়া
  • রাত ১১টা থেকে ১টা পর্যন্ত CI-তে এই test বন্ধ রাখা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. কেন double: আসল সহযোগী ধীর, অনিশ্চিত, অনুপলব্ধ বা বিপজ্জনক হলে, আর এমন অবস্থা তৈরি করতে যা আসলটি দিয়ে কঠিন (timeout, declined, রাত ১১টা ৫৯)। আসলটি দ্রুত, নিশ্চিত আর নিরাপদ হলে আসলটিই।
  2. পাঁচ ধরন (Meszaros, ২০০৭): dummy জায়গা ভরে; stub indirect input দেয়; spy indirect output লিখে রাখে; mock প্রত্যাশা জানে আর নিজে যাচাই করে; fake সরলভাবে সত্যি কাজ করে। Library-র নাম প্রায়ই এগুলো মানে না — ভূমিকা ধরে কথা বলুন।
  3. State বনাম behaviour: ফল দেখা refactoring-এ টেকে; ডাক দেখা দরকার যখন ডাকটিই ফল (SMS, payment). Classicist আর mockist দুই ধারারই যুক্তি আছে; query stub, command যাচাই — ব্যবহারিক মাঝামাঝি।
  4. unittest.mock: return_value, side_effect, assert_called_once_with; patch করুন যেখানে নামটি খোঁজা হয়; create_autospec বানান ভুল আর ভুল argument ধরে। JavaScript-এ হাতে লেখা spy আর fake clock সহজ; সময় আর random inject করুন।
  5. কখন mock নয়: নিজের নয় এমন API — adapter-এর পেছনে; value আর নিজের pure কোড — আসলটি; ভেতরের প্রতিটি ডাক — যাচাই নয় (over-mocking). Mock drift ঠেকাতে contract test, যাচাই করা fake আর অল্প কিছু আসল integration test.

পরের পর্বে (পর্ব ৫০): আপনার test-গুলো কোডের কতটা ছোঁয়, আর আসলে ভুল ধরে কি না — statement ও branch coverage, cyclomatic complexity, আর mutation testing, যা এই পর্বের “mock-এর দেওয়া সংখ্যাই assert” ধরনের অকেজো test খুঁজে বের করে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান