अध्याय 11

Mocking — बाहरी service को कैसे call किया गया, यह जाँचना

payment या email client की जगह Mock रखकर जाँचना कि उसे कैसे call किया गया। return_value, side_effect, autospec, patch, mocker — और कब mock की जगह fake।

50 मिनटPython 3.12
  1. 1समस्या
  2. 2समझें
  3. 3उदाहरण
  4. 4अनुमान
  5. 5स्वयं करें
  6. 6चुनौती

वह समस्या जिसे हम हल कर रहे हैं

यह एक छोटा checkout है। यह कार्ट का कुल दाम निकालता है, एक payment client के ज़रिए कार्ड से पैसे काटता है, और ग्राहक को ईमेल भेजता है।

payments.py:

python
class PaymentDeclined(Exception):
    pass


class PaymentGateway:
    """Talks to the real payment provider over HTTPS."""

    def __init__(self, api_key):
        self.api_key = api_key

    def charge(self, amount_cents, token):
        raise RuntimeError("real network call — never in a test")

    def refund(self, charge_id):
        raise RuntimeError("real network call — never in a test")

checkout.py:

python
from payments import PaymentDeclined


def cart_total(cart):
    return sum(item["price_cents"] * item["qty"] for item in cart)


def place_order(cart, email, token, gateway, mailer):
    total = cart_total(cart)
    if total == 0:
        raise ValueError("cart is empty")

    try:
        receipt = gateway.charge(total, token=token)
    except PaymentDeclined:
        mailer.send(email, "Your payment was declined")
        return {"status": "declined"}

    mailer.send(email, f"Order confirmed: {receipt['id']}")
    return {"status": "paid", "charge_id": receipt["id"], "total_cents": total}

असल ज़िंदगी में charge एक असली कार्ड से असली पैसे काटता है, इसलिए कोई भी टेस्ट वहाँ तक कभी नहीं पहुँचना चाहिए। पिछले अध्याय में monkeypatch से आपने किसी चीज़ को बदलना सीखा — लेकिन यहाँ बदल देना काम का सिर्फ़ आधा हिस्सा है। place_order की return value कहती है "paid", जबकि जो bug सच में मायने रखते हैं वे calls में छिपे हैं: कार्ड से 2500 कटे या 25? किस token से? एक बार, या दो बार? क्या ईमेल सही पते पर गया? इनका जवाब पाने के लिए बदले गए object को याद रखना होगा कि उसे कैसे call किया गया था।

इसके लिए आप हर dependency के लिए एक छोटी recording class लिख सकते थे। unittest.mock उसे पहले से आपके लिए लिख चुका है।

इस अध्याय के अंत में आप कर पाएंगे

  • किसी client की जगह Mock रखना, और assert_called_once_with, call_args, call_count व mock_calls से जाँचना कि उसे कैसे call किया गया
  • return_value और side_effect से mock से वैल्यू लौटवाना या exception उठवाना
  • समझाना कि खाली Mock ख़तरनाक क्यों है, और spec= व create_autospec से वह छेद बंद करना
  • patch से किसी नाम को बदलना — decorator और context manager दोनों तरह, सही जगह पर
  • pytest-mock का mocker.patch और mocker.spy इस्तेमाल करना
  • stub, mock और fake में फ़र्क़ बताना, और जानना कि कब fake वाला टेस्ट बेहतर है

ज़रूरी शर्तें: monkeypatch — एक टेस्ट के लिए चीज़ें बदलना।


टेस्ट लिखने से पहले

कोई भी Mock लिखने से पहले लिख लीजिए कि place_order असल में क्या वादा करता है। इसका एक input आपके हाथ में है (कार्ट, ईमेल और token), और दो side effects हैं जो return value में दिखते नहीं — एक charge और एक ईमेल। ऐसे कोड के टेस्ट को दोनों हिस्से जाँचने होते हैं।

अनुबंध (contract)। अगर कार्ट ख़ाली नहीं है, तो place_order ठीक कुल रक़म, एक बार, दिए गए token से charge करता है; पुष्टि का ईमेल भेजता है; और "paid" लौटाता है। अगर कार्ड declined हो, तो कुछ charge नहीं करता, माफ़ी का ईमेल भेजता है और "declined" लौटाता है। ख़ाली कार्ट पर कुछ भी बाहर जाने से पहले ValueError उठता है। नेटवर्क की विफलता को दबाया नहीं जाता।

तैयारी। आपके virtual environment में pytest, जहाँ से आप pytest चलाते हैं वहाँ से payments.py और checkout.py import हो सकें, और mocker fixture के लिए pytest-mock plugin (pip install pytest-mock)। unittest.mock पायथन के साथ ही आता है। कोई API key नहीं, कोई नेटवर्क नहीं — अगर किसी टेस्ट को इनमें से कुछ भी चाहिए, तो वह unit test नहीं है।

योजना।

| स्थिति | इनपुट | अपेक्षित | |---|---|---| | सामान्य रास्ता | 2 × 1000 + 1 × 500, कार्ड स्वीकार | tok_visa से 2500 का एक charge; पुष्टि ईमेल; "paid" | | कार्ड declined | वही कार्ट, gateway PaymentDeclined उठाता है | कोई charge दर्ज नहीं; माफ़ी का ईमेल; "declined" | | ख़ाली कार्ट | [] | ValueError("cart is empty"); gateway और mailer अछूते | | नेटवर्क विफलता | gateway TimeoutError उठाता है | TimeoutError बाहर आता है; कोई ईमेल नहीं | | अनुबंध | कोई भी ऑर्डर | charge असली signature (amount_cents, token) के साथ call होता है |

क्या टेस्ट न करें। ख़ुद payment provider को — वह उसके लेखकों का काम है, और आपका टेस्ट वहाँ पहुँच भी नहीं सकता। पायथन का sum। और place_order के अंदरूनी क़दम: वह cart_total को एक बार call करता है या हिसाब ख़ुद करता है, यह उसका अपना मामला है। योजना में सिर्फ़ नतीजे हैं और सीमा पार करने वाली एक call — और कुछ नहीं। यह याद रखिए; उपयोगी mock और नुक़सानदेह mock का फ़र्क़ यहीं है, और अध्याय के अंत में हम इस पर लौटेंगे।


Mock — हर call लिख लेने वाला object

python
from unittest.mock import Mock

gateway = Mock()

result = gateway.charge(2500, token="tok_visa")
gateway.refund("ch_1")

print(result)
print(gateway.charge.called, gateway.charge.call_count)
print(gateway.charge.call_args)
print(gateway.mock_calls)
text
<Mock name='mock.charge()' id='132252166523936'>
True 1
call(2500, token='tok_visa')
[call.charge(2500, token='tok_visa'), call.refund('ch_1')]

gateway में कोई charge method नहीं है; कुछ भी define नहीं किया गया। आप जो attribute छूते हैं, Mock उसे बना लेता है, और हर attribute ख़ुद एक Mock है जिसे call किया जा सकता है। हर call दर्ज होती है: called बताता है कि call हुई या नहीं, call_count कितनी बार, call_args ठीक किन arguments से। parent पर mock_calls में हर method की हर call क्रम से रहती है — जब क्रम मायने रखता हो तब काम आता है।

इन attributes को साधारण assert में पढ़ना भी चलता है, लेकिन assertion methods वही बात एक लाइन में कहते हैं, और fail होने पर ख़ुद समझाते हैं:

python
from unittest.mock import Mock

gateway = Mock()
gateway.charge(2500, token="tok_visa")

gateway.charge.assert_called_once_with(2500, token="tok_visa")
print("first check passed")

gateway.charge.assert_called_once_with(2500, token="tok_amex")
text
first check passed
Traceback (most recent call last):
  ...
AssertionError: expected call not found.
Expected: charge(2500, token='tok_amex')
  Actual: charge(2500, token='tok_visa')

assert_called_once_with दो चीज़ें जाँचता है: ठीक एक call हुई, और वह ठीक इन्हीं arguments से हुई। इसके रिश्तेदार हैं assert_called_once() (एक बार, कोई भी arguments), assert_called_with(...) (सिर्फ़ आख़िरी call) और assert_not_called()। (इस अध्याय में जहाँ सिर्फ़ आख़िरी लाइन मायने रखती है, वहाँ traceback को ... तक छोटा किया गया है।)

"एक बार" क्यों? क्योंकि किसी कार्ड से दो बार पैसे काटना असली bug है, और अकेला assert_called_with दूसरी call को नहीं पकड़ता।

return_value और side_effect — mock क्या लौटाएगा

default रूप से एक call दूसरा Mock लौटाती है। जो कोड receipt["id"] करता है उसे असली जवाब चाहिए, तो उसे दीजिए:

python
from unittest.mock import Mock

gateway = Mock()
gateway.charge.return_value = {"id": "ch_1", "status": "succeeded"}

print(gateway.charge(2500, token="tok_visa"))
print(gateway.charge(999, token="tok_other"))
text
{'id': 'ch_1', 'status': 'succeeded'}
{'id': 'ch_1', 'status': 'succeeded'}

arguments कुछ भी हों, जवाब एक ही। जब इतना काफ़ी न हो, तो side_effect set कीजिए। इसके तीन रूप हैं।

एक exception — gateway.charge.side_effect = PaymentDeclined("card declined") से हर call वही exception उठाती है। असल में जिन बुरे रास्तों को पैदा करना मुश्किल है, उन तक ऐसे ही पहुँचा जाता है: declined कार्ड, timeout, सर्वर से 500।

एक list — हर call पर एक item, क्रम से। list में exception हो तो उठाया जाता है; बाक़ी सब लौटाया जाता है। "एक बार fail, फिर सफल" जैसी स्थिति के लिए अच्छा:

python
from unittest.mock import Mock

gateway = Mock()
gateway.charge.side_effect = [
    TimeoutError("gateway timed out"),
    {"id": "ch_2", "status": "succeeded"},
]

try:
    gateway.charge(2500, token="tok_visa")
except TimeoutError as exc:
    print("first call:", exc)

print("second call:", gateway.charge(2500, token="tok_visa"))
gateway.charge(2500, token="tok_visa")
text
first call: gateway timed out
second call: {'id': 'ch_2', 'status': 'succeeded'}
Traceback (most recent call last):
  ...
StopIteration

तीसरी call को list ख़ाली मिली। mock से StopIteration का मतलब है कि कोड ने उसे आपकी योजना से ज़्यादा बार call किया — और यह जानना भी अपने आप में क़ीमती है।

एक function — mock को जो arguments मिलते हैं उन्हीं से इसे call किया जाता है, और यह जो लौटाए वही जवाब है। gateway.charge.side_effect = lambda amount_cents, token: {"status": "declined"} if amount_cents > 100_000 else {"id": "ch_1"} input पर निर्भर जवाब देता है — और calls फिर भी दर्ज होती और गिनी जाती हैं।

MagicMock — जब कोड len, with या for इस्तेमाल करे

साधारण Mock पायथन के special methods को support नहीं करता: len(Mock()) से TypeError: object of type 'Mock' has no len() आता है, और with Mock(): भी fail होता है। MagicMock इन्हें समझदार defaults के साथ support करता है — len(MagicMock()) 0 है, यह with block में चलता है, और m.__len__.return_value = 3 से जवाब बदल जाता है।

जब बदला गया object context manager, container या iterator की तरह इस्तेमाल हो — जैसे with block में एक database connection — तब MagicMock लीजिए। नीचे वाले patch और mocker.patch default रूप से MagicMock ही बनाते हैं।

ख़तरा: Mock हर चीज़ पर हाँ कहता है

इस आज्ञाकारी स्वभाव की एक क़ीमत है। किसी method का नाम ग़लत लिखिए, कोई शिकायत नहीं करेगा:

python
from unittest.mock import Mock

gateway = Mock()
gateway.chrage(2500, token="tok_visa")   # typo in the method name

print("no error — the typo was recorded as a new method")
print(gateway.mock_calls)
text
no error — the typo was recorded as a new method
[call.chrage(2500, token='tok_visa')]

अगर यह typo checkout.py में होता, तो खाली Mock वाला टेस्ट pass हो सकता था, और पहले असली ग्राहक को AttributeError मिलता। ग़लत arguments भी इसी तरह निकल जाते हैं। यहाँ ग़लत तरीक़ा और सही तरीक़ा साथ-साथ हैं — टेस्ट हो रहा कोड token देना भूल गया है:

python
from unittest.mock import Mock, create_autospec

from payments import PaymentGateway


def charge_order(gateway, total, token):
    return gateway.charge(total)   # bug: the token is never passed


def test_with_a_plain_mock():
    gateway = Mock()
    charge_order(gateway, 2500, "tok_visa")
    gateway.charge.assert_called_once()


def test_with_an_autospec():
    gateway = create_autospec(PaymentGateway, instance=True)
    charge_order(gateway, 2500, "tok_visa")
    gateway.charge.assert_called_once()
text
$ pytest -q --tb=line test_typo.py
.F                                                                       [100%]
=================================== FAILURES ===================================
E   TypeError: missing a required argument: 'token'
/usr/lib/python3.12/inspect.py:3157: TypeError: missing a required argument: 'token'
=========================== short test summary info ============================
FAILED test_typo.py::test_with_an_autospec - TypeError: missing a required ar...
1 failed, 1 passed in 0.07s

साधारण Mock ने एक टूटी हुई call को जाने दिया — एक हरा टेस्ट जो किसी चीज़ की रक्षा नहीं करता। create_autospec असली PaymentGateway से mock बनाता है: उसमें सिर्फ़ उसी class के attributes होते हैं, और हर method अपने arguments को असली signature से मिलाकर देखता है। instance=True का मतलब है "class का एक instance", ख़ुद class नहीं।

सख़्ती के तीन स्तर:

  • Mock() — कोई भी attribute, कोई भी arguments।
  • Mock(spec=PaymentGateway) — सिर्फ़ असली attribute नाम (gateway.chrage से AttributeError: Mock object has no attribute 'chrage'. Did you mean: 'charge'? आता है), लेकिन arguments नहीं जाँचे जाते।
  • create_autospec(PaymentGateway, instance=True), या patch पर autospec=True — असली नाम और असली signature।

typo के अलावा यह क्यों मायने रखता है? क्योंकि असली client बदलता है। जब library upgrade में किसी method का नाम बदलता है या कोई ज़रूरी argument जुड़ता है, autospec mock उसी दिन fail होता है; खाली Mock ऐसे कोड से सहमत होता रहता है जो अब काम ही नहीं करता। जो चीज़ किसी असली class की जगह खड़ी है, उसके लिए सबसे सख़्त स्तर इस्तेमाल कीजिए।

एक तरह की typo को पायथन ख़ुद पकड़ता है: 3.12 से assert_called_once_wiht जैसी ग़लत लिखी assertion चुपचाप pass होने के बजाय AttributeError: 'assert_called_once_wiht' is not a valid assertion. उठाती है। लेकिन यह पहरा सिर्फ़ mock के अपने assertion नाम जानता है। उसे नहीं पता कि आपके gateway में charge है, chrage नहीं — यह सिर्फ़ spec जान सकता है।

patch — वह नाम बदलना जिसे कोड ख़ुद ढूँढता है

place_order अपना gateway argument के रूप में पाता है, इसलिए mock देना आसान है। लेकिन अक्सर कोड अपनी dependency ख़ुद लाता है:

python
# emailer.py
def send_email(to, subject):
    raise RuntimeError("real SMTP call — never in a test")


# signup.py
from emailer import send_email


def register(email):
    send_email(email, "Welcome aboard")
    return {"email": email, "active": True}

unittest.mock.patch एक टेस्ट के दौरान किसी नाम को MagicMock से बदल देता है, फिर असली को वापस रख देता है। यह decorator की तरह काम करता है — mock argument बनकर आता है — या context manager की तरह:

python
from unittest.mock import patch

from signup import register


@patch("signup.send_email")
def test_register_as_decorator(send_email):
    user = register("asha@example.com")

    assert user["active"] is True
    send_email.assert_called_once_with("asha@example.com", "Welcome aboard")


def test_register_as_context_manager():
    with patch("signup.send_email") as send_email:
        register("ravi@example.com")

    send_email.assert_called_once_with("ravi@example.com", "Welcome aboard")
text
$ pytest -q test_signup.py
..                                                                       [100%]
2 passed in 0.08s

target है "signup.send_email", न कि "emailer.send_email"। यही पिछले अध्याय का नियम है: नाम को वहाँ patch कीजिए जहाँ उसे ढूँढा जाता है, वहाँ नहीं जहाँ वह define हुआ है। from emailer import send_email ने नाम को signup में copy कर दिया, इसलिए register वहीं ढूँढता है। ग़लत तरीक़ा — मूल module को patch करना — copy को अछूता छोड़ देता है, और असली function चल जाता है:

python
from unittest.mock import patch

from signup import register


def test_patched_in_the_wrong_place():
    with patch("emailer.send_email") as send_email:
        register("asha@example.com")

    send_email.assert_called_once()
text
$ pytest -q --tb=line test_signup_wrong.py
F                                                                        [100%]
=================================== FAILURES ===================================
E   RuntimeError: real SMTP call — never in a test
/home/you/shop/emailer.py:2: RuntimeError: real SMTP call — never in a test
=========================== short test summary info ============================
FAILED test_signup_wrong.py::test_patched_in_the_wrong_place - RuntimeError: ...
1 failed in 0.08s

error emailer.py से आ रहा है — असली वाले से। यहाँ stand-in ज़ोर से चिल्लाता है; असली ईमेल client होता तो चुपचाप ईमेल भेज ही देता।

patch भी autospec=True लेता है, उसी कारण से: patch("signup.send_email", autospec=True) ऐसा mock बनाता है जो सिर्फ़ (to, subject) स्वीकार करता है।

mocker — pytest-mock का fixture

pytest-mock plugin यह सब mocker नाम के fixture में लपेट देता है। mocker.patch वही arguments लेता है जो patch, उसे decorator या with नहीं चाहिए, और टेस्ट ख़त्म होने पर अपने आप वापस हो जाता है — बिल्कुल monkeypatch की तरह। mocker.spy इनमें अलग है: वह कुछ भी नहीं बदलता। असली method चलता है; spy सिर्फ़ नज़र रखता है।

python
from checkout import place_order
from fakes import FakeGateway, FakeMailer
from signup import register


def test_register_with_mocker(mocker):
    send_email = mocker.patch("signup.send_email", autospec=True)

    register("asha@example.com")

    send_email.assert_called_once_with("asha@example.com", "Welcome aboard")


def test_spy_watches_a_real_object(mocker):
    gateway = FakeGateway()
    spy = mocker.spy(gateway, "charge")

    place_order([{"price_cents": 700, "qty": 1}], "a@example.com", "tok", gateway, FakeMailer())

    spy.assert_called_once_with(700, token="tok")
    assert spy.spy_return == {"id": "ch_1"}
    assert gateway.charges == [("ch_1", 700, "tok")]
text
$ pytest -q test_signup_mocker.py
..                                                                       [100%]
2 passed in 0.08s

spy ने mock की तरह call दर्ज की, असली नतीजा spy_return में रखा, और असली charge ने फिर भी अपना काम किया — gateway.charges भर गया। (FakeGateway अगले हिस्से में define है।) mocker में mocker.Mock, mocker.MagicMock और mocker.create_autospec भी हैं, ताकि टेस्ट को सब कुछ एक ही fixture से मिल जाए।

Stub, mock, fake — और कब mock न करें

तीन शब्द आपस में उलझ जाते हैं, और यही फ़र्क़ तय करता है कि आपका टेस्ट क्या पकड़ पाएगा:

  • stub सिर्फ़ जवाब देता है। gateway.charge.return_value = {"id": "ch_1"}, और आप कभी नहीं जाँचते कि उसे कैसे call किया गया। वह टेस्ट हो रहे कोड को खिलाता है।
  • mock वह stub है जिससे आप बाद में पूछताछ करते हैं: assert_called_once_with(...)। टेस्ट ख़ुद call के बारे में है।
  • fake एक असली, काम करने वाला, छोटा implementation है — एक in-memory gateway जो बैंक से बात करने के बजाय एक list रखता है।

mocks से बना टेस्ट जाँचता है कि कोड ने अपना काम कैसे किया। ज़्यादती कीजिए तो टेस्ट यह जाँचना बंद कर देता है कि उसने क्या किया। पहले ग़लत तरीक़ा:

python
import checkout
from checkout import place_order


def test_place_order_over_mocked(mocker):
    mocker.patch("checkout.cart_total", return_value=2500)
    gateway = mocker.Mock()
    gateway.charge.return_value = {"id": "ch_1"}
    mailer = mocker.Mock()

    place_order([{"price_cents": 1, "qty": 1}], "a@example.com", "tok", gateway, mailer)

    checkout.cart_total.assert_called_once()
    gateway.charge.assert_called_once_with(2500, token="tok")
    mailer.send.assert_called_once()
text
$ pytest -q test_overmock.py
.                                                                        [100%]
1 passed in 0.07s

यह pass होता है। कार्ट की क़ीमत एक cent है, और टेस्ट 2500 के charge की "पुष्टि" करता है — क्योंकि हिसाब को टेस्ट ने ख़ुद बदल दिया। cart_total को तोड़ दीजिए, यह टेस्ट फिर भी pass होगा। बिना behaviour बदले cart_total का नाम बदलिए, यह fail होगा। यही over-mocking की बदबू है: ऐसा टेस्ट जो implementation की लाइन-दर-लाइन नक़ल करता है, अपने ही mocks को टेस्ट करता है, और हर refactor पर टूटता है।

सही तरीक़ा अध्याय की शुरुआत वाली योजना पर चलता है: सिर्फ़ अपने system की सीमा पर mock कीजिए, और कहीं नहीं। payment provider और SMTP सर्वर सीमा हैं। cart_total आपका अपना कोड है — उसे चलने दीजिए। और छोटे interface वाले सहयोगियों के लिए आमतौर पर fake, mock से बेहतर होता है:

python
from payments import PaymentDeclined


class FakeGateway:
    """An in-memory stand-in for PaymentGateway: same methods, no network."""

    def __init__(self, declines=False):
        self.declines = declines
        self.charges = []

    def charge(self, amount_cents, token):
        if self.declines:
            raise PaymentDeclined("card declined")
        charge_id = f"ch_{len(self.charges) + 1}"
        self.charges.append((charge_id, amount_cents, token))
        return {"id": charge_id}

    def refund(self, charge_id):
        self.charges = [c for c in self.charges if c[0] != charge_id]


class FakeMailer:
    def __init__(self):
        self.sent = []

    def send(self, to, subject):
        self.sent.append((to, subject))

fakes वाला टेस्ट state पर assert करता है — list में आख़िर में क्या पहुँचा — calls के क्रम पर नहीं। यह behaviour के वर्णन की तरह पढ़ा जाता है, और जब तक behaviour ठीक है, हर refactor में टिका रहता है। fake एक बार लिखिए, fakes.py में या किसी conftest.py fixture में, और हर टेस्ट उसे दोबारा इस्तेमाल करता है। इसकी एक कमज़ोरी है कि fake असली client से भटक सकता है — इसीलिए पूर्ण उदाहरण में contract के लिए एक autospec टेस्ट रखा गया है।


पूर्ण उदाहरण

test_checkout.py टेस्ट-योजना को पंक्ति-दर-पंक्ति लागू करता है, हर औज़ार अपनी जगह पर — behaviour के लिए fakes, contract के लिए autospec mock, विफलता के लिए side_effect:

python
from unittest.mock import create_autospec

import pytest

from checkout import place_order
from fakes import FakeGateway, FakeMailer
from payments import PaymentGateway

CART = [{"price_cents": 1000, "qty": 2}, {"price_cents": 500, "qty": 1}]
EMAIL = "asha@example.com"


# Behaviour, checked with fakes: what happened, not which calls were made.
def test_paid_order_charges_once_and_sends_a_receipt():
    gateway, mailer = FakeGateway(), FakeMailer()

    result = place_order(CART, EMAIL, "tok_visa", gateway, mailer)

    assert result == {"status": "paid", "charge_id": "ch_1", "total_cents": 2500}
    assert gateway.charges == [("ch_1", 2500, "tok_visa")]
    assert mailer.sent == [(EMAIL, "Order confirmed: ch_1")]


def test_declined_card_sends_an_apology_and_charges_nothing():
    gateway, mailer = FakeGateway(declines=True), FakeMailer()

    result = place_order(CART, EMAIL, "tok_visa", gateway, mailer)

    assert result == {"status": "declined"}
    assert gateway.charges == []
    assert mailer.sent == [(EMAIL, "Your payment was declined")]


def test_empty_cart_never_reaches_the_gateway():
    gateway, mailer = FakeGateway(), FakeMailer()

    with pytest.raises(ValueError, match="cart is empty"):
        place_order([], EMAIL, "tok_visa", gateway, mailer)

    assert gateway.charges == []
    assert mailer.sent == []


# The contract with the real client, checked with an autospec mock.
def test_gateway_is_called_with_the_real_signature():
    gateway = create_autospec(PaymentGateway, instance=True)
    gateway.charge.return_value = {"id": "ch_9"}

    place_order(CART, EMAIL, "tok_visa", gateway, FakeMailer())

    gateway.charge.assert_called_once_with(2500, token="tok_visa")
    gateway.refund.assert_not_called()


def test_a_timeout_propagates_and_sends_nothing():
    gateway = create_autospec(PaymentGateway, instance=True)
    gateway.charge.side_effect = TimeoutError("gateway timed out")
    mailer = FakeMailer()

    with pytest.raises(TimeoutError):
        place_order(CART, EMAIL, "tok_visa", gateway, mailer)

    assert mailer.sent == []
text
$ pytest -v test_checkout.py
============================= test session starts ==============================
collected 5 items

test_checkout.py::test_paid_order_charges_once_and_sends_a_receipt PASSED [ 20%]
test_checkout.py::test_declined_card_sends_an_apology_and_charges_nothing PASSED [ 40%]
test_checkout.py::test_empty_cart_never_reaches_the_gateway PASSED       [ 60%]
test_checkout.py::test_gateway_is_called_with_the_real_signature PASSED  [ 80%]
test_checkout.py::test_a_timeout_propagates_and_sends_nothing PASSED     [100%]

============================== 5 passed in 0.08s ===============================

दो बातें ध्यान देने लायक़ हैं।

पहली, behaviour वाले तीनों टेस्ट में कोई assert_called... नहीं है। वे नतीजा जाँचते हैं — return value, दर्ज charges, भेजे गए ईमेल — इसलिए place_order को शुरू से दोबारा लिख दिया जाए तब भी वे टिके रहेंगे।

दूसरी, call जाँचने वाला अकेला टेस्ट PaymentGateway के साथ contract है, और वह create_autospec इस्तेमाल करता है। कोई charge का नाम बदले या ज़रूरी argument जोड़े, तो यह टेस्ट चुपचाप सहमत होने के बजाय fail होगा। योजना में पाँच पंक्तियाँ, पाँच टेस्ट, place_order के अंदर को टेस्ट करने वाला कुछ नहीं।


जब यह काम न करे

AssertionError: expected call not found. mock call हुआ, लेकिन इन arguments से नहीं। संदेश Expected: और Actual: को एक के नीचे एक छापता है — अक्षर-दर-अक्षर मिलाइए। positional argument बनाम keyword argument (charge(2500, "tok") बनाम charge(2500, token="tok")) अलग call गिनी जाती है।

AssertionError: Expected 'charge' to be called once. Called 2 times. कोड ने उसे एक से ज़्यादा बार call किया, और Calls: लाइन हर call की सूची देती है। अक्सर कारण एक retry loop होता है, या टेस्ट में दो कामों के लिए एक ही mock का इस्तेमाल।

AttributeError: Mock object has no attribute 'chrage'. Did you mean: 'charge'? spec अपना काम कर रहा है: कोड ऐसा नाम इस्तेमाल कर रहा है जो असली class में नहीं है। कोड ठीक कीजिए, टेस्ट नहीं।

TypeError: missing a required argument: 'token' autospec mock ने ऐसी call पकड़ी जो असली signature से मेल नहीं खाती। असली client के सामने कोड इसी तरह fail होता।

AttributeError: <module 'signup' from '...'> does not have the attribute 'send_mail' patch का target ऐसी चीज़ का नाम है जो मौजूद नहीं — typo, या ग़लत module। patch नए attributes बनाने से इनकार करता है, और यह अच्छी बात है।

patch ने "कुछ नहीं किया" और असली service call हो गई आपने नाम को वहाँ patch किया जहाँ वह define है, वहाँ नहीं जहाँ उसे ढूँढा जाता है। अगर module में from emailer import send_email है, तो "signup.send_email" patch कीजिए।

mock call से StopIteration side_effect एक list थी, और कोड ने list के items से ज़्यादा calls कीं।

नतीजे में डेटा की जगह <MagicMock name='mock.charge().__getitem__()' ...> है आपने return_value set नहीं किया, और MagicMock ने ख़ुशी से receipt["id"] का जवाब एक और mock से दे दिया। साधारण Mock के साथ यही कोड TypeError: 'Mock' object is not subscriptable से fail होता। return_value में वही आकार दीजिए जो असली client लौटाता है।

fixture 'mocker' not found pytest जिस environment से चल रहा है उसमें pytest-mock install नहीं है। pip install pytest-mock।