Mocking — बाहरी service को कैसे call किया गया, यह जाँचना
payment या email client की जगह Mock रखकर जाँचना कि उसे कैसे call किया गया। return_value, side_effect, autospec, patch, mocker — और कब mock की जगह fake।
- 1समस्या
- 2समझें
- 3उदाहरण
- 4अनुमान
- 5स्वयं करें
- 6चुनौती
वह समस्या जिसे हम हल कर रहे हैं
यह एक छोटा checkout है। यह कार्ट का कुल दाम निकालता है, एक payment client के ज़रिए कार्ड से पैसे काटता है, और ग्राहक को ईमेल भेजता है।
payments.py:
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:
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
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)<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 होने पर ख़ुद समझाते हैं:
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")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"] करता है उसे असली जवाब चाहिए, तो उसे दीजिए:
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")){'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, फिर सफल" जैसी स्थिति के लिए अच्छा:
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")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 का नाम ग़लत लिखिए, कोई शिकायत नहीं करेगा:
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)no error — the typo was recorded as a new method
[call.chrage(2500, token='tok_visa')]अगर यह typo checkout.py में होता, तो खाली Mock वाला टेस्ट pass हो सकता था, और पहले असली ग्राहक को AttributeError मिलता। ग़लत arguments भी इसी तरह निकल जाते हैं। यहाँ ग़लत तरीक़ा और सही तरीक़ा साथ-साथ हैं — टेस्ट हो रहा कोड token देना भूल गया है:
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()$ 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 ख़ुद लाता है:
# 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 की तरह:
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")$ pytest -q test_signup.py
.. [100%]
2 passed in 0.08starget है "signup.send_email", न कि "emailer.send_email"। यही पिछले अध्याय का नियम है: नाम को वहाँ patch कीजिए जहाँ उसे ढूँढा जाता है, वहाँ नहीं जहाँ वह define हुआ है। from emailer import send_email ने नाम को signup में copy कर दिया, इसलिए register वहीं ढूँढता है। ग़लत तरीक़ा — मूल module को patch करना — copy को अछूता छोड़ देता है, और असली function चल जाता है:
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()$ 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.08serror 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 सिर्फ़ नज़र रखता है।
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")]$ pytest -q test_signup_mocker.py
.. [100%]
2 passed in 0.08sspy ने 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 से बना टेस्ट जाँचता है कि कोड ने अपना काम कैसे किया। ज़्यादती कीजिए तो टेस्ट यह जाँचना बंद कर देता है कि उसने क्या किया। पहले ग़लत तरीक़ा:
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()$ 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 से बेहतर होता है:
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:
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 == []$ 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।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
side_effect में तीन items की list है, जिसका बीच वाला एक exception है। ठीक क्या छपेगा?
from unittest.mock import Mock
client = Mock()
client.fetch.side_effect = [1, ValueError("boom"), 3]
print(client.fetch())
try:
client.fetch()
except ValueError as exc:
print("error:", exc)
print(client.fetch(), client.fetch.call_count)- A1 error: boom 3 2
- B1 ValueError('boom') 3 3
- C1 error: boom 3 3
- D[1, ValueError('boom'), 3] error: boom 3 1
टेस्ट RuntimeError: real SMTP call के साथ fail होता है — यानी असली send चल गया। कौन-सा बदलाव इसे ठीक करेगा?
# mailer.py
def send(to, subject):
raise RuntimeError("real SMTP call")
# orders.py
from mailer import send
def confirm(order_id, email):
send(email, f"Order {order_id} confirmed")
# test_orders.py
from orders import confirm
def test_confirm_sends_one_email(mocker):
send = mocker.patch("mailer.send")
confirm(7, "asha@example.com")
send.assert_called_once_with("asha@example.com", "Order 7 confirmed")- A`mocker.patch("mailer.send", autospec=True)` लिखिए
- B`mocker.patch("orders.send")` लिखिए — जहाँ `orders` नाम ढूँढता है
- C`mocker.patch` की जगह `mocker.spy(mailer, "send")` इस्तेमाल कीजिए
- D`orders.py` में `send` call करने से पहले `import mailer` जोड़िए
pay token देना भूल गया है। किस stand-in वाला टेस्ट यह bug पकड़ेगा?
class PaymentGateway:
def charge(self, amount_cents, token):
...
def pay(gateway, total):
return gateway.charge(total) # the token is forgotten- AMock()
- BMagicMock()
- CMock(spec=PaymentGateway)
- Dcreate_autospec(PaymentGateway, instance=True)
उत्तर देने के लिए अकाउंट आवश्यक है
अपने उत्तर जाँचने के लिए साइन इन करें
प्रश्न ऊपर दिए गए हैं, और मन में उत्तर सोचना ही मुख्य कार्य है। सही उत्तर, व्याख्या और तीन-स्तरीय संकेत देखने के लिए साइन इन करें।
आपकी बारी
weather.py लिखिए, जिसमें WeatherClient नाम की class हो जिसका method get_forecast(self, city) RuntimeError उठाए — यह असली HTTP client का प्रतिनिधि है — और एक function umbrella_advice(city, client)। function client.get_forecast(city) को call करता है, जो {"rain_chance": 70} जैसा dict लौटाता है, और संभावना 50 या उससे ज़्यादा हो तो "take an umbrella", वरना "leave it at home" लौटाता है। अगर client TimeoutError उठाए, तो यह "forecast unavailable" लौटाता है।
एक टेस्ट-योजना (स्थिति | इनपुट | अपेक्षित) से शुरू कीजिए, फिर test_weather.py लिखिए:
create_autospec(WeatherClient, instance=True)औरreturn_valueसे दोनों सलाहें टेस्ट कीजिए — ठीक 50 की सीमा समेत — औरassert_called_once_withसे जाँचिए कि शहर का नाम आगे पहुँचा।side_effect = TimeoutError(...)से"forecast unavailable"वाला रास्ता टेस्ट कीजिए।side_effectमें एक list देकर एक ही mock से दो अलग जवाब पाइए।- एक
FakeWeatherClientलिखिए जो शहर → बारिश की संभावना का dict रखे, और उससे टेस्ट कीजिए।
फिर एक प्रयोग: मान लीजिए library upgrade हुई, और असली method को बदलकर get_forecast(self, city, units) कर दीजिए। एक टेस्ट चलाइए जो Mock() इस्तेमाल करे, और एक जो create_autospec। कौन-सा पकड़ता है?
समाधान
weather.py:
class WeatherClient:
"""Stands for the real HTTP client."""
def get_forecast(self, city):
raise RuntimeError("real HTTP call — never in a test")
def umbrella_advice(city, client):
try:
forecast = client.get_forecast(city)
except TimeoutError:
return "forecast unavailable"
if forecast["rain_chance"] >= 50:
return "take an umbrella"
return "leave it at home"योजना:
| स्थिति | इनपुट | अपेक्षित | |---|---|---| | बारिश | संभावना 70 | "take an umbrella" | | सीमा | संभावना 50 | "take an umbrella" | | ठीक नीचे | संभावना 49 | "leave it at home" | | नेटवर्क विफलता | client TimeoutError उठाता है | "forecast unavailable" | | अनुबंध | कोई भी शहर | get_forecast उसी शहर से एक बार call होता है |
test_weather.py:
from unittest.mock import create_autospec
import pytest
from weather import WeatherClient, umbrella_advice
@pytest.fixture
def client():
return create_autospec(WeatherClient, instance=True)
@pytest.mark.parametrize(
"chance, advice",
[(70, "take an umbrella"), (50, "take an umbrella"), (49, "leave it at home")],
)
def test_advice_follows_the_rain_chance(client, chance, advice):
client.get_forecast.return_value = {"rain_chance": chance}
assert umbrella_advice("Dhaka", client) == advice
client.get_forecast.assert_called_once_with("Dhaka")
def test_timeout_gives_a_polite_answer(client):
client.get_forecast.side_effect = TimeoutError("no answer in 5s")
assert umbrella_advice("Dhaka", client) == "forecast unavailable"
def test_one_mock_two_answers(client):
client.get_forecast.side_effect = [{"rain_chance": 90}, {"rain_chance": 10}]
assert umbrella_advice("Dhaka", client) == "take an umbrella"
assert umbrella_advice("Dhaka", client) == "leave it at home"
assert client.get_forecast.call_count == 2
class FakeWeatherClient:
def __init__(self, chances):
self.chances = chances
def get_forecast(self, city):
return {"rain_chance": self.chances[city]}
def test_with_a_fake():
client = FakeWeatherClient({"Dhaka": 80, "Cairo": 5})
assert umbrella_advice("Dhaka", client) == "take an umbrella"
assert umbrella_advice("Cairo", client) == "leave it at home"$ pytest -v test_weather.py
============================= test session starts ==============================
collected 6 items
test_weather.py::test_advice_follows_the_rain_chance[70-take an umbrella] PASSED [ 16%]
test_weather.py::test_advice_follows_the_rain_chance[50-take an umbrella] PASSED [ 33%]
test_weather.py::test_advice_follows_the_rain_chance[49-leave it at home] PASSED [ 50%]
test_weather.py::test_timeout_gives_a_polite_answer PASSED [ 66%]
test_weather.py::test_one_mock_two_answers PASSED [ 83%]
test_weather.py::test_with_a_fake PASSED [100%]
============================== 6 passed in 0.08s ===============================प्रयोग, WeatherClient में get_forecast(self, city, units) और इस test_drift.py के साथ:
from unittest.mock import Mock, create_autospec
from weather import WeatherClient, umbrella_advice
def test_plain_mock():
client = Mock()
client.get_forecast.return_value = {"rain_chance": 70}
assert umbrella_advice("Dhaka", client) == "take an umbrella"
def test_autospec():
client = create_autospec(WeatherClient, instance=True)
client.get_forecast.return_value = {"rain_chance": 70}
assert umbrella_advice("Dhaka", client) == "take an umbrella"$ pytest -q --tb=line test_drift.py
.F [100%]
=================================== FAILURES ===================================
E TypeError: missing a required argument: 'units'
/usr/lib/python3.12/inspect.py:3157: TypeError: missing a required argument: 'units'
=========================== short test summary info ============================
FAILED test_drift.py::test_autospec - TypeError: missing a required argument:...
1 failed, 1 passed in 0.07sहर फ़ैसले की वजह:
- एक fixture autospec mock बनाता है। हर टेस्ट को नया मिलता है, इसलिए call की गिनती एक टेस्ट से दूसरे में नहीं रिसती, और सख़्त संस्करण ही default है — याद रखने वाली अलग चीज़ नहीं।
parametrize70, 50 और 49 को कवर करता है।>=वाली लाइन में ग़लती की संभावना सबसे ज़्यादा है; 50 और 49 उसके दोनों तरफ़ हैं। सिर्फ़ एक "बारिश" वाला case>के साथ भी pass हो जाता।assert_called_once_with("Dhaka")parametrized टेस्ट में ही है, क्योंकि शहर को आगे पहुँचाना client के साथ contract का हिस्सा है — सीमा पार करने वाली एकमात्र call।- timeout वाला टेस्ट सिर्फ़ return value जाँचता है। मायने यह रखता है कि user क्या देखता है; client से कितनी बार पूछा गया, यह वादे का हिस्सा नहीं।
- fake को हर टेस्ट में कोई setup नहीं चाहिए: आप दुनिया का वर्णन करते हैं (
{"Dhaka": 80, "Cairo": 5}) और उससे सवाल पूछते हैं। अपने ख़ुद के client के लिए आमतौर पर यही ज़्यादा पढ़ने लायक़ टेस्ट है। - प्रयोग खाली
Mockकी क़ीमत दिखाता है: upgrade के बाद असली client हर call पर fail होता, फिर भी plain-mock वाला टेस्ट हरा रहता है। autospec वाला टेस्ट उसी दिन fail होता है, ग़ायब argument का नाम बताते हुए।
Step 6 of 6
चुनौती — the chapter quiz
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
Sign in to take the quiz