[{"data":1,"prerenderedAt":1894},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002F":3},{"id":4,"title":5,"body":6,"description":1853,"extension":1854,"meta":1855,"navigation":314,"path":1890,"seo":1891,"stem":1892,"__hash__":1893},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Findex.md","Spies, Fakes & Hand-Rolled Test Doubles",{"type":7,"value":8,"toc":1833},"minimark",[9,17,22,65,69,72,101,283,287,292,377,383,387,522,533,537,721,724,728,794,800,804,810,863,870,873,877,974,978,984,998,1016,1034,1037,1041,1044,1073,1095,1098,1229,1233,1236,1353,1367,1389,1393,1396,1443,1454,1467,1471,1474,1558,1569,1572,1575,1579,1582,1591,1600,1606,1612,1736,1740,1749,1755,1761,1777,1783,1787,1822,1829],[10,11,12,16],"p",{},[13,14,15],"code",{},"unittest.mock"," is powerful enough that it becomes the answer to every isolation problem, and the cost only shows up later: a test file where fifteen lines configure return values, a suite that passes after an interface changes because nothing checked the mock still matched reality, and failures that report \"expected call not found\" without saying what the collaborator was supposed to do. A hand-written double solves those by encoding behaviour once, in one place, in ordinary Python.",[18,19,21],"h2",{"id":20},"prerequisites","Prerequisites",[23,24,25,33,47,58],"ul",{},[26,27,28,29,32],"li",{},"Python 3.9+ and ",[13,30,31],{},"pytest >= 8.0",".",[26,34,35,38,39,42,43,46],{},[13,36,37],{},"typing.Protocol"," for structural interfaces, plus ",[13,40,41],{},"mypy"," or ",[13,44,45],{},"pyright"," if the protocol is to be enforced statically.",[26,48,49,50,52,53,32],{},"Familiarity with ",[13,51,15],{},"'s configuration API, since the comparison here assumes it: see ",[54,55,57],"a",{"href":56},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","deep dive into unittest.mock",[26,59,60,61,32],{},"Code that accepts its collaborators rather than importing them, which is the precondition for substituting anything — see ",[54,62,64],{"href":63},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","dependency injection for testability",[18,66,68],{"id":67},"core-concept-five-doubles-distinguished-by-what-they-do","Core concept: five doubles, distinguished by what they do",[10,70,71],{},"The vocabulary is worth being precise about, because the choice between them is a design decision rather than a stylistic one.",[10,73,74,75,79,80,83,84,87,88,92,93,96,97,100],{},"A ",[76,77,78],"strong",{},"dummy"," is a value passed to satisfy a signature and never used. A ",[76,81,82],{},"stub"," returns canned answers with no logic. A ",[76,85,86],{},"spy"," records how it was called; a ",[89,90,91],"em",{},"partial"," spy wraps a real object and delegates, so behaviour is preserved. A ",[76,94,95],{},"mock"," is a stub with expectations attached — it fails the test when the interaction does not match. A ",[76,98,99],{},"fake"," has a working implementation, simplified: an in-memory repository, a fake clock, a queue backed by a list.",[102,103,106,279],"figure",{"className":104},[105],"diagram",[107,108,115,116,115,120,115,124,115,132,115,142,115,151,115,154,115,159,115,164,115,168,115,172,115,176,115,180,115,182,115,185,115,189,115,192,115,195,115,198,115,203,115,205,115,208,115,212,115,215,115,218,115,222,115,226,115,228,115,231,115,235,115,238,115,241,115,245,115,250,115,252,115,255,115,259,115,262,115,265,115,268,115,275],"svg",{"viewBox":109,"role":110,"ariaLabelledBy":111,"xmlns":114},"0 0 840 286","img",[112,113],"dbl-t","dbl-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[117,118,119],"title",{"id":112},"Five kinds of test double by behaviour and assertion style",[121,122,123],"desc",{"id":113},"Five cards arranged from least to most behaviour. A dummy has none and is never called. A stub returns canned values. A spy records calls and may delegate to a real object. A mock adds expectations that fail the test. A fake has a working simplified implementation and supports assertions on resulting state.",[125,126],"rect",{"x":127,"y":127,"width":128,"height":129,"rx":130,"fill":131},"0","840","286","14","#fffdf8",[133,134,141],"text",{"x":135,"y":136,"textAnchor":137,"fontSize":138,"fontWeight":139,"fill":140},"420","28","middle","16","700","#3d405b","More behaviour to the right; different assertions at each step",[125,143],{"x":144,"y":145,"width":146,"height":147,"rx":148,"fill":131,"stroke":149,"strokeWidth":150},"22","52","152","164","11","rgba(61,64,91,0.35)","1.6",[125,152],{"x":144,"y":145,"width":146,"height":153,"rx":148,"fill":140},"30",[133,155,78],{"x":156,"y":157,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":131},"98","72","12",[133,160,163],{"x":161,"y":162,"fontSize":148,"fill":140},"36","104","never called",[133,165,167],{"x":161,"y":166,"fontSize":148,"fill":140},"126","fills a signature",[133,169,171],{"x":161,"y":170,"fontSize":148,"fontWeight":139,"fill":140},"156","assert: nothing",[133,173,175],{"x":161,"y":174,"fontSize":148,"fill":140},"186","object() will do",[125,177],{"x":174,"y":145,"width":146,"height":147,"rx":148,"fill":131,"stroke":178,"strokeWidth":179},"#f2cc8f","1.8",[125,181],{"x":174,"y":145,"width":146,"height":153,"rx":148,"fill":140},[133,183,82],{"x":184,"y":157,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":131},"262",[133,186,188],{"x":187,"y":162,"fontSize":148,"fill":140},"200","canned answers",[133,190,191],{"x":187,"y":166,"fontSize":148,"fill":140},"no logic",[133,193,194],{"x":187,"y":170,"fontSize":148,"fontWeight":139,"fill":140},"assert: on output",[133,196,197],{"x":187,"y":174,"fontSize":148,"fill":140},"Mock(return_value=…)",[125,199],{"x":200,"y":145,"width":146,"height":147,"rx":148,"fill":131,"stroke":201,"strokeWidth":202},"350","#81b29a","2",[125,204],{"x":200,"y":145,"width":146,"height":153,"rx":148,"fill":140},[133,206,86],{"x":207,"y":157,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":131},"426",[133,209,211],{"x":210,"y":162,"fontSize":148,"fill":140},"364","records calls",[133,213,214],{"x":210,"y":166,"fontSize":148,"fill":140},"may delegate",[133,216,217],{"x":210,"y":170,"fontSize":148,"fontWeight":139,"fill":140},"assert: on calls",[133,219,221],{"x":210,"y":174,"fontSize":148,"fill":220},"#2a5f49","Mock(wraps=real)",[125,223],{"x":224,"y":145,"width":146,"height":147,"rx":148,"fill":131,"stroke":225,"strokeWidth":202},"514","#e07a5f",[125,227],{"x":224,"y":145,"width":146,"height":153,"rx":148,"fill":140},[133,229,95],{"x":230,"y":157,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":131},"590",[133,232,234],{"x":233,"y":162,"fontSize":148,"fill":140},"528","stub + expectations",[133,236,237],{"x":233,"y":166,"fontSize":148,"fill":140},"fails on mismatch",[133,239,240],{"x":233,"y":170,"fontSize":148,"fontWeight":139,"fill":140},"assert: on protocol",[133,242,244],{"x":233,"y":174,"fontSize":148,"fill":243},"#8f3d22","couples to call shape",[125,246],{"x":247,"y":145,"width":248,"height":147,"rx":148,"fill":249,"stroke":201,"strokeWidth":202},"678","140","#e6f0ea",[125,251],{"x":247,"y":145,"width":248,"height":153,"rx":148,"fill":140},[133,253,99],{"x":254,"y":157,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":131},"748",[133,256,258],{"x":257,"y":162,"fontSize":148,"fill":140},"692","working, simplified",[133,260,261],{"x":257,"y":166,"fontSize":148,"fill":140},"in-memory state",[133,263,264],{"x":257,"y":170,"fontSize":148,"fontWeight":139,"fill":140},"assert: on state",[133,266,267],{"x":257,"y":174,"fontSize":148,"fill":220},"survives refactors",[125,269],{"x":144,"y":270,"width":271,"height":272,"rx":273,"fill":131,"stroke":149,"strokeWidth":274},"232","796","38","9","1.5",[133,276,278],{"x":135,"y":277,"textAnchor":137,"fontSize":158,"fill":140},"256","The further right, the more the test asserts on outcomes rather than on the sequence of calls that produced them.",[280,281,282],"figcaption",{},"Asserting on state rather than on interactions is what makes a test survive an internal refactor, which is the strongest practical argument for fakes over mocks.",[18,284,286],{"id":285},"step-by-step-implementation","Step-by-step implementation",[288,289,291],"h3",{"id":290},"_1-name-the-contract","1. Name the contract",[293,294,299],"pre",{"className":295,"code":296,"language":297,"meta":298,"style":298},"language-python shiki shiki-themes github-light github-dark","from typing import Protocol\n\nfrom myapp.models import Invoice\n\n\nclass InvoiceRepository(Protocol):\n    \"\"\"What the billing service actually depends on — nothing more.\"\"\"\n\n    def get(self, invoice_id: str) -> Invoice | None: ...\n\n    def save(self, invoice: Invoice) -> None: ...\n\n    def list_open(self, *, limit: int = 100) -> list[Invoice]: ...\n","python","",[13,300,301,309,316,322,327,332,338,344,349,355,360,366,371],{"__ignoreMap":298},[302,303,306],"span",{"class":304,"line":305},"line",1,[302,307,308],{},"from typing import Protocol\n",[302,310,312],{"class":304,"line":311},2,[302,313,315],{"emptyLinePlaceholder":314},true,"\n",[302,317,319],{"class":304,"line":318},3,[302,320,321],{},"from myapp.models import Invoice\n",[302,323,325],{"class":304,"line":324},4,[302,326,315],{"emptyLinePlaceholder":314},[302,328,330],{"class":304,"line":329},5,[302,331,315],{"emptyLinePlaceholder":314},[302,333,335],{"class":304,"line":334},6,[302,336,337],{},"class InvoiceRepository(Protocol):\n",[302,339,341],{"class":304,"line":340},7,[302,342,343],{},"    \"\"\"What the billing service actually depends on — nothing more.\"\"\"\n",[302,345,347],{"class":304,"line":346},8,[302,348,315],{"emptyLinePlaceholder":314},[302,350,352],{"class":304,"line":351},9,[302,353,354],{},"    def get(self, invoice_id: str) -> Invoice | None: ...\n",[302,356,358],{"class":304,"line":357},10,[302,359,315],{"emptyLinePlaceholder":314},[302,361,363],{"class":304,"line":362},11,[302,364,365],{},"    def save(self, invoice: Invoice) -> None: ...\n",[302,367,369],{"class":304,"line":368},12,[302,370,315],{"emptyLinePlaceholder":314},[302,372,374],{"class":304,"line":373},13,[302,375,376],{},"    def list_open(self, *, limit: int = 100) -> list[Invoice]: ...\n",[10,378,74,379,382],{},[13,380,381],{},"Protocol"," rather than an abstract base class keeps the real implementation free of an inheritance relationship it does not need, and gives the type checker enough to reject a fake that drifts from the interface.",[288,384,386],{"id":385},"_2-implement-the-fake","2. Implement the fake",[293,388,390],{"className":295,"code":389,"language":297,"meta":298,"style":298},"from myapp.models import Invoice\nfrom myapp.errors import DuplicateInvoice\n\n\nclass FakeInvoiceRepository:\n    \"\"\"In-memory implementation with the same observable behaviour.\"\"\"\n\n    def __init__(self) -> None:\n        self._items: dict[str, Invoice] = {}\n\n    def get(self, invoice_id: str) -> Invoice | None:\n        return self._items.get(invoice_id)\n\n    def save(self, invoice: Invoice) -> None:\n        existing = self._items.get(invoice.id)\n        # The real repository has a unique constraint; the fake must too,\n        # or tests will pass against behaviour production does not have.\n        if existing is not None and existing.version != invoice.version:\n            raise DuplicateInvoice(invoice.id)\n        self._items[invoice.id] = invoice\n\n    def list_open(self, *, limit: int = 100) -> list[Invoice]:\n        # Same ordering guarantee as the SQL implementation: newest first.\n        items = [i for i in self._items.values() if i.status == \"open\"]\n        return sorted(items, key=lambda i: i.created_at, reverse=True)[:limit]\n",[13,391,392,396,401,405,409,414,419,423,428,433,437,442,447,451,457,463,469,475,481,487,493,498,504,510,516],{"__ignoreMap":298},[302,393,394],{"class":304,"line":305},[302,395,321],{},[302,397,398],{"class":304,"line":311},[302,399,400],{},"from myapp.errors import DuplicateInvoice\n",[302,402,403],{"class":304,"line":318},[302,404,315],{"emptyLinePlaceholder":314},[302,406,407],{"class":304,"line":324},[302,408,315],{"emptyLinePlaceholder":314},[302,410,411],{"class":304,"line":329},[302,412,413],{},"class FakeInvoiceRepository:\n",[302,415,416],{"class":304,"line":334},[302,417,418],{},"    \"\"\"In-memory implementation with the same observable behaviour.\"\"\"\n",[302,420,421],{"class":304,"line":340},[302,422,315],{"emptyLinePlaceholder":314},[302,424,425],{"class":304,"line":346},[302,426,427],{},"    def __init__(self) -> None:\n",[302,429,430],{"class":304,"line":351},[302,431,432],{},"        self._items: dict[str, Invoice] = {}\n",[302,434,435],{"class":304,"line":357},[302,436,315],{"emptyLinePlaceholder":314},[302,438,439],{"class":304,"line":362},[302,440,441],{},"    def get(self, invoice_id: str) -> Invoice | None:\n",[302,443,444],{"class":304,"line":368},[302,445,446],{},"        return self._items.get(invoice_id)\n",[302,448,449],{"class":304,"line":373},[302,450,315],{"emptyLinePlaceholder":314},[302,452,454],{"class":304,"line":453},14,[302,455,456],{},"    def save(self, invoice: Invoice) -> None:\n",[302,458,460],{"class":304,"line":459},15,[302,461,462],{},"        existing = self._items.get(invoice.id)\n",[302,464,466],{"class":304,"line":465},16,[302,467,468],{},"        # The real repository has a unique constraint; the fake must too,\n",[302,470,472],{"class":304,"line":471},17,[302,473,474],{},"        # or tests will pass against behaviour production does not have.\n",[302,476,478],{"class":304,"line":477},18,[302,479,480],{},"        if existing is not None and existing.version != invoice.version:\n",[302,482,484],{"class":304,"line":483},19,[302,485,486],{},"            raise DuplicateInvoice(invoice.id)\n",[302,488,490],{"class":304,"line":489},20,[302,491,492],{},"        self._items[invoice.id] = invoice\n",[302,494,496],{"class":304,"line":495},21,[302,497,315],{"emptyLinePlaceholder":314},[302,499,501],{"class":304,"line":500},22,[302,502,503],{},"    def list_open(self, *, limit: int = 100) -> list[Invoice]:\n",[302,505,507],{"class":304,"line":506},23,[302,508,509],{},"        # Same ordering guarantee as the SQL implementation: newest first.\n",[302,511,513],{"class":304,"line":512},24,[302,514,515],{},"        items = [i for i in self._items.values() if i.status == \"open\"]\n",[302,517,519],{"class":304,"line":518},25,[302,520,521],{},"        return sorted(items, key=lambda i: i.created_at, reverse=True)[:limit]\n",[10,523,524,525,528,529,532],{},"Reproducing the ",[89,526,527],{},"error"," cases is the part most fakes skip and the part that matters most. A fake that never raises ",[13,530,531],{},"DuplicateInvoice"," means every test of the duplicate-handling path is testing nothing.",[288,534,536],{"id":535},"_3-prove-the-fake-agrees-with-the-real-thing","3. Prove the fake agrees with the real thing",[293,538,540],{"className":295,"code":539,"language":297,"meta":298,"style":298},"import pytest\n\n\nclass RepositoryContract:\n    \"\"\"Behaviours every implementation must satisfy. Subclassed per impl.\"\"\"\n\n    def test_get_returns_none_for_unknown_id(self, repo):\n        assert repo.get(\"nope\") is None\n\n    def test_saved_invoice_is_retrievable(self, repo, invoice):\n        repo.save(invoice)\n        assert repo.get(invoice.id) == invoice\n\n    def test_conflicting_version_raises(self, repo, invoice):\n        repo.save(invoice)\n        with pytest.raises(DuplicateInvoice):\n            repo.save(invoice.evolve(version=invoice.version + 1, id=invoice.id))\n\n    def test_list_open_is_newest_first(self, repo, invoice_factory):\n        older, newer = invoice_factory(days_ago=2), invoice_factory(days_ago=1)\n        repo.save(older)\n        repo.save(newer)\n        assert [i.id for i in repo.list_open()] == [newer.id, older.id]\n\n\nclass TestFakeRepository(RepositoryContract):\n    @pytest.fixture\n    def repo(self):\n        return FakeInvoiceRepository()\n\n\n@pytest.mark.integration          # only this half needs a database\nclass TestSqlRepository(RepositoryContract):\n    @pytest.fixture\n    def repo(self, db_session):\n        return SqlInvoiceRepository(db_session)\n",[13,541,542,547,551,555,560,565,569,574,579,583,588,593,598,602,607,611,616,621,625,630,635,640,645,650,654,658,664,670,676,682,687,692,698,704,709,715],{"__ignoreMap":298},[302,543,544],{"class":304,"line":305},[302,545,546],{},"import pytest\n",[302,548,549],{"class":304,"line":311},[302,550,315],{"emptyLinePlaceholder":314},[302,552,553],{"class":304,"line":318},[302,554,315],{"emptyLinePlaceholder":314},[302,556,557],{"class":304,"line":324},[302,558,559],{},"class RepositoryContract:\n",[302,561,562],{"class":304,"line":329},[302,563,564],{},"    \"\"\"Behaviours every implementation must satisfy. Subclassed per impl.\"\"\"\n",[302,566,567],{"class":304,"line":334},[302,568,315],{"emptyLinePlaceholder":314},[302,570,571],{"class":304,"line":340},[302,572,573],{},"    def test_get_returns_none_for_unknown_id(self, repo):\n",[302,575,576],{"class":304,"line":346},[302,577,578],{},"        assert repo.get(\"nope\") is None\n",[302,580,581],{"class":304,"line":351},[302,582,315],{"emptyLinePlaceholder":314},[302,584,585],{"class":304,"line":357},[302,586,587],{},"    def test_saved_invoice_is_retrievable(self, repo, invoice):\n",[302,589,590],{"class":304,"line":362},[302,591,592],{},"        repo.save(invoice)\n",[302,594,595],{"class":304,"line":368},[302,596,597],{},"        assert repo.get(invoice.id) == invoice\n",[302,599,600],{"class":304,"line":373},[302,601,315],{"emptyLinePlaceholder":314},[302,603,604],{"class":304,"line":453},[302,605,606],{},"    def test_conflicting_version_raises(self, repo, invoice):\n",[302,608,609],{"class":304,"line":459},[302,610,592],{},[302,612,613],{"class":304,"line":465},[302,614,615],{},"        with pytest.raises(DuplicateInvoice):\n",[302,617,618],{"class":304,"line":471},[302,619,620],{},"            repo.save(invoice.evolve(version=invoice.version + 1, id=invoice.id))\n",[302,622,623],{"class":304,"line":477},[302,624,315],{"emptyLinePlaceholder":314},[302,626,627],{"class":304,"line":483},[302,628,629],{},"    def test_list_open_is_newest_first(self, repo, invoice_factory):\n",[302,631,632],{"class":304,"line":489},[302,633,634],{},"        older, newer = invoice_factory(days_ago=2), invoice_factory(days_ago=1)\n",[302,636,637],{"class":304,"line":495},[302,638,639],{},"        repo.save(older)\n",[302,641,642],{"class":304,"line":500},[302,643,644],{},"        repo.save(newer)\n",[302,646,647],{"class":304,"line":506},[302,648,649],{},"        assert [i.id for i in repo.list_open()] == [newer.id, older.id]\n",[302,651,652],{"class":304,"line":512},[302,653,315],{"emptyLinePlaceholder":314},[302,655,656],{"class":304,"line":518},[302,657,315],{"emptyLinePlaceholder":314},[302,659,661],{"class":304,"line":660},26,[302,662,663],{},"class TestFakeRepository(RepositoryContract):\n",[302,665,667],{"class":304,"line":666},27,[302,668,669],{},"    @pytest.fixture\n",[302,671,673],{"class":304,"line":672},28,[302,674,675],{},"    def repo(self):\n",[302,677,679],{"class":304,"line":678},29,[302,680,681],{},"        return FakeInvoiceRepository()\n",[302,683,685],{"class":304,"line":684},30,[302,686,315],{"emptyLinePlaceholder":314},[302,688,690],{"class":304,"line":689},31,[302,691,315],{"emptyLinePlaceholder":314},[302,693,695],{"class":304,"line":694},32,[302,696,697],{},"@pytest.mark.integration          # only this half needs a database\n",[302,699,701],{"class":304,"line":700},33,[302,702,703],{},"class TestSqlRepository(RepositoryContract):\n",[302,705,707],{"class":304,"line":706},34,[302,708,669],{},[302,710,712],{"class":304,"line":711},35,[302,713,714],{},"    def repo(self, db_session):\n",[302,716,718],{"class":304,"line":717},36,[302,719,720],{},"        return SqlInvoiceRepository(db_session)\n",[10,722,723],{},"This is the mechanism that makes fakes safe. The contract suite runs twice: once in milliseconds against the fake on every push, once against the real repository in the integration stage. A divergence fails immediately and points at the exact behaviour, which is the guarantee a mock can never provide.",[288,725,727],{"id":726},"_4-use-a-spy-where-behaviour-must-be-preserved","4. Use a spy where behaviour must be preserved",[293,729,731],{"className":295,"code":730,"language":297,"meta":298,"style":298},"from unittest.mock import Mock\n\n\ndef test_cache_is_consulted_before_the_repository(billing_service, repo, invoice):\n    repo.save(invoice)\n    # wraps: calls still reach the real object, and are also recorded.\n    spy = Mock(wraps=repo)\n    billing_service.repository = spy\n\n    billing_service.get_invoice(invoice.id)\n    billing_service.get_invoice(invoice.id)      # second call should hit the cache\n\n    assert spy.get.call_count == 1               # behaviour unchanged, calls counted\n",[13,732,733,738,742,746,751,756,761,766,771,775,780,785,789],{"__ignoreMap":298},[302,734,735],{"class":304,"line":305},[302,736,737],{},"from unittest.mock import Mock\n",[302,739,740],{"class":304,"line":311},[302,741,315],{"emptyLinePlaceholder":314},[302,743,744],{"class":304,"line":318},[302,745,315],{"emptyLinePlaceholder":314},[302,747,748],{"class":304,"line":324},[302,749,750],{},"def test_cache_is_consulted_before_the_repository(billing_service, repo, invoice):\n",[302,752,753],{"class":304,"line":329},[302,754,755],{},"    repo.save(invoice)\n",[302,757,758],{"class":304,"line":334},[302,759,760],{},"    # wraps: calls still reach the real object, and are also recorded.\n",[302,762,763],{"class":304,"line":340},[302,764,765],{},"    spy = Mock(wraps=repo)\n",[302,767,768],{"class":304,"line":346},[302,769,770],{},"    billing_service.repository = spy\n",[302,772,773],{"class":304,"line":351},[302,774,315],{"emptyLinePlaceholder":314},[302,776,777],{"class":304,"line":357},[302,778,779],{},"    billing_service.get_invoice(invoice.id)\n",[302,781,782],{"class":304,"line":362},[302,783,784],{},"    billing_service.get_invoice(invoice.id)      # second call should hit the cache\n",[302,786,787],{"class":304,"line":368},[302,788,315],{"emptyLinePlaceholder":314},[302,790,791],{"class":304,"line":373},[302,792,793],{},"    assert spy.get.call_count == 1               # behaviour unchanged, calls counted\n",[10,795,796,799],{},[13,797,798],{},"wraps"," is the least intrusive double available: nothing about the system's behaviour changes, so any failure is about the interaction rather than about a stub returning the wrong thing.",[18,801,803],{"id":802},"verification","Verification",[10,805,806,807,809],{},"A fake earns trust by failing when it should. Break it deliberately once — remove the ",[13,808,531],{}," check — and confirm the contract suite goes red for the fake and stays green for the real repository. If both stay green, the contract test is not covering the behaviour the code depends on, which is a more valuable discovery than the fake itself.",[293,811,815],{"className":812,"code":813,"language":814,"meta":298,"style":298},"language-bash shiki shiki-themes github-light github-dark","pytest tests\u002Fcontracts -q -k Fake              # milliseconds\npytest tests\u002Fcontracts -q -k Sql -m integration # seconds, needs the container\n","bash",[13,816,817,841],{"__ignoreMap":298},[302,818,819,823,827,831,834,837],{"class":304,"line":305},[302,820,822],{"class":821},"sScJk","pytest",[302,824,826],{"class":825},"sZZnC"," tests\u002Fcontracts",[302,828,830],{"class":829},"sj4cs"," -q",[302,832,833],{"class":829}," -k",[302,835,836],{"class":825}," Fake",[302,838,840],{"class":839},"sJ8bj","              # milliseconds\n",[302,842,843,845,847,849,851,854,857,860],{"class":304,"line":311},[302,844,822],{"class":821},[302,846,826],{"class":825},[302,848,830],{"class":829},[302,850,833],{"class":829},[302,852,853],{"class":825}," Sql",[302,855,856],{"class":829}," -m",[302,858,859],{"class":825}," integration",[302,861,862],{"class":839}," # seconds, needs the container\n",[293,864,868],{"className":865,"code":867,"language":133,"meta":298},[866],"language-text","tests\u002Fcontracts\u002Ftest_repository.py::TestFakeRepository::test_conflicting_version_raises FAILED\ntests\u002Fcontracts\u002Ftest_repository.py::TestSqlRepository::test_conflicting_version_raises PASSED\n",[13,869,867],{"__ignoreMap":298},[10,871,872],{},"One red and one green is exactly the signal the arrangement exists to produce.",[18,874,876],{"id":875},"troubleshooting","Troubleshooting",[878,879,880,896],"table",{},[881,882,883],"thead",{},[884,885,886,890,893],"tr",{},[887,888,889],"th",{},"Symptom",[887,891,892],{},"Root cause",[887,894,895],{},"Fix",[897,898,899,911,922,936,947,963],"tbody",{},[884,900,901,905,908],{},[902,903,904],"td",{},"Tests pass; production breaks on the same path",[902,906,907],{},"Fake lacks an error case the real one has",[902,909,910],{},"Add it to the contract suite, then to the fake",[884,912,913,916,919],{},[902,914,915],{},"Fake and real disagree on ordering",[902,917,918],{},"Implicit SQL ordering never specified",[902,920,921],{},"Make ordering part of the contract and assert it",[884,923,924,927,933],{},[902,925,926],{},"Type checker accepts a drifted fake",[902,928,929,930,932],{},"No ",[13,931,381],{},", or fake not annotated",[902,934,935],{},"Annotate the fake's use sites with the protocol type",[884,937,938,941,944],{},[902,939,940],{},"Contract suite duplicated per implementation",[902,942,943],{},"Copy-paste instead of a shared base class",[902,945,946],{},"One base class, one fixture per implementation",[884,948,949,952,960],{},[902,950,951],{},"Spy changes behaviour",[902,953,954,957,958],{},[13,955,956],{},"Mock()"," used instead of ",[13,959,221],{},[902,961,962],{},"Wrap rather than replace",[884,964,965,968,971],{},[902,966,967],{},"Fake grows a query language",[902,969,970],{},"It is reimplementing the database",[902,972,973],{},"Move those tests to the integration layer",[18,975,977],{"id":976},"when-mocks-are-still-the-right-answer","When mocks are still the right answer",[10,979,980,981,983],{},"None of this is an argument against ",[13,982,15],{},". Three situations favour it clearly.",[10,985,986,989,990,993,994,997],{},[76,987,988],{},"One-off collaborators."," A single test needs a function to raise ",[13,991,992],{},"ConnectionError","; a ",[13,995,996],{},"Mock(side_effect=ConnectionError)"," says that in one line and nothing else in the suite cares. Writing a fake for it would be ceremony.",[10,999,1000,1007,1008,1011,1012,1015],{},[76,1001,1002,1003,1006],{},"Asserting that something did ",[89,1004,1005],{},"not"," happen."," ",[13,1009,1010],{},"mock.assert_not_called()"," has no clean fake equivalent, and \"no email was sent\" is a real requirement. A fake can grow a ",[13,1013,1014],{},"sent"," list to support it, but for a single assertion the mock is simpler.",[10,1017,1018,1021,1022,1025,1026,1029,1030,32],{},[76,1019,1020],{},"Third-party interfaces you do not control and barely use."," Faking a cloud SDK's client is a large job with little payoff if the code touches two methods. ",[13,1023,1024],{},"create_autospec"," on those two methods gives signature checking without the implementation — and the strictness argument for autospec over a bare ",[13,1027,1028],{},"MagicMock"," is made in ",[54,1031,1033],{"href":1032},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002F","autospec and strict mocking",[10,1035,1036],{},"The dividing line is repetition. The first test that needs a collaborator gets a mock; the fourth test that needs the same collaborator configured the same way is telling you to write a fake.",[18,1038,1040],{"id":1039},"fakes-that-other-people-can-use","Fakes that other people can use",[10,1042,1043],{},"A fake shipped alongside the real implementation is one of the highest-leverage things a library can provide, and the pattern is worth copying for internal packages too.",[293,1045,1047],{"className":295,"code":1046,"language":297,"meta":298,"style":298},"# myapp\u002Fclients\u002F__init__.py\nfrom myapp.clients.http import HttpBillingClient\nfrom myapp.clients.fake import FakeBillingClient   # shipped, tested, versioned\n\n__all__ = [\"HttpBillingClient\", \"FakeBillingClient\"]\n",[13,1048,1049,1054,1059,1064,1068],{"__ignoreMap":298},[302,1050,1051],{"class":304,"line":305},[302,1052,1053],{},"# myapp\u002Fclients\u002F__init__.py\n",[302,1055,1056],{"class":304,"line":311},[302,1057,1058],{},"from myapp.clients.http import HttpBillingClient\n",[302,1060,1061],{"class":304,"line":318},[302,1062,1063],{},"from myapp.clients.fake import FakeBillingClient   # shipped, tested, versioned\n",[302,1065,1066],{"class":304,"line":324},[302,1067,315],{"emptyLinePlaceholder":314},[302,1069,1070],{"class":304,"line":329},[302,1071,1072],{},"__all__ = [\"HttpBillingClient\", \"FakeBillingClient\"]\n",[10,1074,1075,1076,1079,1080,1079,1083,1086,1087,1090,1091,32],{},"Shipping the fake means every consumer of the package gets a fast, correct double without writing one, and it means the fake is covered by the package's own contract tests rather than by each consumer's guesses. ",[13,1077,1078],{},"freezegun",", ",[13,1081,1082],{},"moto",[13,1084,1085],{},"fakeredis"," and ",[13,1088,1089],{},"pyfakefs"," are all this idea applied at ecosystem scale — the last of which is covered in ",[54,1092,1094],{"href":1093},"\u002Fadvanced-mocking-test-doubles-in-python\u002Ffaking-the-filesystem-and-environment\u002Ffaking-a-whole-filesystem-with-pyfakefs\u002F","faking a whole filesystem with pyfakefs",[10,1096,1097],{},"The obligation that comes with shipping a fake is versioning it in step. A behaviour added to the real client and not to the fake is a silent divergence for every downstream consumer, which is why the contract suite belongs in the package's own CI rather than in a consumer's.",[102,1099,1101,1226],{"className":1100},[105],[107,1102,115,1107,115,1110,115,1113,115,1129,115,1133,115,1137,115,1143,115,1148,115,1151,115,1154,115,1158,115,1162,115,1169,115,1173,115,1179,115,1184,115,1188,115,1192,115,1198,115,1204,115,1207,115,1211,115,1214,115,1217,115,1222],{"viewBox":1103,"role":110,"ariaLabelledBy":1104,"xmlns":114},"0 0 820 260",[1105,1106],"ship-t","ship-d",[117,1108,1109],{"id":1105},"One contract suite, two implementations, many consumers",[121,1111,1112],{"id":1106},"A package publishes both a real HTTP client and a fake client, with a single contract suite verifying both in the package's own pipeline. Three downstream consumers import the fake for their fast tests and the real client for production, so a divergence is caught once rather than by each consumer.",[1114,1115,1116,1117,115],"defs",{},"\n    ",[1118,1119,1125],"marker",{"id":1120,"viewBox":1121,"refX":273,"refY":1122,"markerWidth":1123,"markerHeight":1123,"orient":1124},"ship-a","0 0 10 10","5","7","auto-start-reverse",[1126,1127],"path",{"d":1128,"fill":140},"M0 0 L10 5 L0 10 z",[125,1130],{"x":127,"y":127,"width":1131,"height":1132,"rx":130,"fill":131},"820","260",[133,1134,1136],{"x":1135,"y":136,"textAnchor":137,"fontSize":138,"fontWeight":139,"fill":140},"410","The fake is part of the package's contract",[125,1138],{"x":1139,"y":1140,"width":1141,"height":1142,"rx":148,"fill":249,"stroke":201,"strokeWidth":202},"26","54","250","60",[133,1144,1147],{"x":1145,"y":1146,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"151","78","HttpBillingClient",[133,1149,1150],{"x":1145,"y":156,"textAnchor":137,"fontSize":148,"fill":140},"the real implementation",[125,1152],{"x":1139,"y":1153,"width":1141,"height":1142,"rx":148,"fill":249,"stroke":201,"strokeWidth":202},"130",[133,1155,1157],{"x":1145,"y":1156,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"154","FakeBillingClient",[133,1159,1161],{"x":1145,"y":1160,"textAnchor":137,"fontSize":148,"fill":140},"174","shipped in the same wheel",[304,1163],{"x1":1164,"y1":1165,"x2":1166,"y2":1167,"stroke":140,"strokeWidth":150,"markerEnd":1168},"280","84","320","112","url(#ship-a)",[304,1170],{"x1":1164,"y1":1171,"x2":1166,"y2":1172,"stroke":140,"strokeWidth":150,"markerEnd":1168},"160","132",[125,1174],{"x":1175,"y":1176,"width":1177,"height":1142,"rx":148,"fill":1178,"stroke":178,"strokeWidth":202},"326","96","196","#f7f0da",[133,1180,1183],{"x":1181,"y":1182,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"424","120","one contract suite",[133,1185,1187],{"x":1181,"y":248,"textAnchor":137,"fontSize":148,"fill":1186},"#8a5a00","runs in the package's CI",[304,1189],{"x1":1190,"y1":166,"x2":1191,"y2":166,"stroke":140,"strokeWidth":150,"markerEnd":1168},"526","566",[125,1193],{"x":1194,"y":1140,"width":1195,"height":1196,"rx":1197,"fill":131,"stroke":149,"strokeWidth":150},"572","222","46","10",[133,1199,1203],{"x":1200,"y":1201,"textAnchor":137,"fontSize":1202,"fill":140},"683","82","11.5","checkout service tests",[125,1205],{"x":1194,"y":1206,"width":1195,"height":1196,"rx":1197,"fill":131,"stroke":149,"strokeWidth":150},"106",[133,1208,1210],{"x":1200,"y":1209,"textAnchor":137,"fontSize":1202,"fill":140},"134","reporting job tests",[125,1212],{"x":1194,"y":1213,"width":1195,"height":1196,"rx":1197,"fill":131,"stroke":149,"strokeWidth":150},"158",[133,1215,1216],{"x":1200,"y":174,"textAnchor":137,"fontSize":1202,"fill":140},"admin tooling tests",[125,1218],{"x":1139,"y":1219,"width":1220,"height":1221,"rx":273,"fill":131,"stroke":149,"strokeWidth":274},"212","768","34",[133,1223,1225],{"x":1135,"y":1224,"textAnchor":137,"fontSize":1202,"fill":140},"234","Three consumers, zero hand-written doubles, one place where divergence is caught.",[280,1227,1228],{},"Each consumer would otherwise write its own approximation of the client, and each approximation would be wrong in a different way.",[18,1230,1232],{"id":1231},"fakes-for-time-randomness-and-identity","Fakes for time, randomness and identity",[10,1234,1235],{},"Three collaborators appear in almost every codebase and are almost always handled badly: the clock, the random number generator, and whatever produces identifiers. Patching them globally works and has a long tail of surprises; faking them is simpler and composes better.",[293,1237,1239],{"className":295,"code":1238,"language":297,"meta":298,"style":298},"from dataclasses import dataclass, field\nfrom datetime import datetime, timedelta, timezone\n\n\nclass FakeClock:\n    \"\"\"A clock the test drives, rather than a patched module.\"\"\"\n\n    def __init__(self, start: datetime | None = None) -> None:\n        self._now = start or datetime(2026, 1, 1, tzinfo=timezone.utc)\n\n    def now(self) -> datetime:\n        return self._now\n\n    def advance(self, **kwargs) -> None:\n        # Explicit: the test says when time passes, so nothing depends on duration.\n        self._now += timedelta(**kwargs)\n\n\ndef test_token_expires_after_an_hour(clock=FakeClock()):\n    token = issue_token(clock=clock, ttl=timedelta(hours=1))\n    assert token.is_valid(clock.now())\n\n    clock.advance(hours=1, seconds=1)\n    assert not token.is_valid(clock.now())\n",[13,1240,1241,1246,1251,1255,1259,1264,1269,1273,1278,1283,1287,1292,1297,1301,1306,1311,1316,1320,1324,1329,1334,1339,1343,1348],{"__ignoreMap":298},[302,1242,1243],{"class":304,"line":305},[302,1244,1245],{},"from dataclasses import dataclass, field\n",[302,1247,1248],{"class":304,"line":311},[302,1249,1250],{},"from datetime import datetime, timedelta, timezone\n",[302,1252,1253],{"class":304,"line":318},[302,1254,315],{"emptyLinePlaceholder":314},[302,1256,1257],{"class":304,"line":324},[302,1258,315],{"emptyLinePlaceholder":314},[302,1260,1261],{"class":304,"line":329},[302,1262,1263],{},"class FakeClock:\n",[302,1265,1266],{"class":304,"line":334},[302,1267,1268],{},"    \"\"\"A clock the test drives, rather than a patched module.\"\"\"\n",[302,1270,1271],{"class":304,"line":340},[302,1272,315],{"emptyLinePlaceholder":314},[302,1274,1275],{"class":304,"line":346},[302,1276,1277],{},"    def __init__(self, start: datetime | None = None) -> None:\n",[302,1279,1280],{"class":304,"line":351},[302,1281,1282],{},"        self._now = start or datetime(2026, 1, 1, tzinfo=timezone.utc)\n",[302,1284,1285],{"class":304,"line":357},[302,1286,315],{"emptyLinePlaceholder":314},[302,1288,1289],{"class":304,"line":362},[302,1290,1291],{},"    def now(self) -> datetime:\n",[302,1293,1294],{"class":304,"line":368},[302,1295,1296],{},"        return self._now\n",[302,1298,1299],{"class":304,"line":373},[302,1300,315],{"emptyLinePlaceholder":314},[302,1302,1303],{"class":304,"line":453},[302,1304,1305],{},"    def advance(self, **kwargs) -> None:\n",[302,1307,1308],{"class":304,"line":459},[302,1309,1310],{},"        # Explicit: the test says when time passes, so nothing depends on duration.\n",[302,1312,1313],{"class":304,"line":465},[302,1314,1315],{},"        self._now += timedelta(**kwargs)\n",[302,1317,1318],{"class":304,"line":471},[302,1319,315],{"emptyLinePlaceholder":314},[302,1321,1322],{"class":304,"line":477},[302,1323,315],{"emptyLinePlaceholder":314},[302,1325,1326],{"class":304,"line":483},[302,1327,1328],{},"def test_token_expires_after_an_hour(clock=FakeClock()):\n",[302,1330,1331],{"class":304,"line":489},[302,1332,1333],{},"    token = issue_token(clock=clock, ttl=timedelta(hours=1))\n",[302,1335,1336],{"class":304,"line":495},[302,1337,1338],{},"    assert token.is_valid(clock.now())\n",[302,1340,1341],{"class":304,"line":500},[302,1342,315],{"emptyLinePlaceholder":314},[302,1344,1345],{"class":304,"line":506},[302,1346,1347],{},"    clock.advance(hours=1, seconds=1)\n",[302,1349,1350],{"class":304,"line":512},[302,1351,1352],{},"    assert not token.is_valid(clock.now())\n",[10,1354,1355,1356,1358,1359,1362,1363,32],{},"The test above runs in microseconds, states the passage of time explicitly, and cannot be affected by the machine's timezone, a leap second or a slow CI runner. The equivalent with ",[13,1357,1078],{}," is shorter to write and patches ",[13,1360,1361],{},"datetime"," process-wide, which affects logging timestamps, database defaults and any library that samples the clock — a trade-off examined in ",[54,1364,1366],{"href":1365},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch\u002F","freezing time with freezegun vs monkeypatch",[10,1368,1369,1370,1373,1374,1079,1377,1380,1381,1384,1385,1388],{},"Identifiers follow the same pattern. A ",[13,1371,1372],{},"FakeIdGenerator"," yielding ",[13,1375,1376],{},"\"id-1\"",[13,1378,1379],{},"\"id-2\""," makes assertions readable and failures reproducible, where a patched ",[13,1382,1383],{},"uuid4"," returning a fixed value breaks the moment two objects are created in one test. Randomness is the one case where seeding the real generator is usually enough, because the standard library's ",[13,1386,1387],{},"Random"," is already injectable — pass an instance rather than calling the module-level functions.",[18,1390,1392],{"id":1391},"keeping-doubles-out-of-the-assertions","Keeping doubles out of the assertions",[10,1394,1395],{},"A last discipline decides whether a suite of fakes reads well: the double should appear in the arrangement and in the final assertion, never in between.",[293,1397,1399],{"className":295,"code":1398,"language":297,"meta":298,"style":298},"def test_publishing_an_invoice_notifies_the_customer(clock, repo, notifier):\n    invoice = InvoiceFactory(status=\"draft\")\n    repo.save(invoice)\n    service = BillingService(repository=repo, notifier=notifier, clock=clock)\n\n    service.publish(invoice.id)                      # the action under test\n\n    assert repo.get(invoice.id).status == \"published\"    # state, from the fake\n    assert notifier.sent == [(\"customer@example.test\", \"invoice_published\")]\n",[13,1400,1401,1406,1411,1415,1420,1424,1429,1433,1438],{"__ignoreMap":298},[302,1402,1403],{"class":304,"line":305},[302,1404,1405],{},"def test_publishing_an_invoice_notifies_the_customer(clock, repo, notifier):\n",[302,1407,1408],{"class":304,"line":311},[302,1409,1410],{},"    invoice = InvoiceFactory(status=\"draft\")\n",[302,1412,1413],{"class":304,"line":318},[302,1414,755],{},[302,1416,1417],{"class":304,"line":324},[302,1418,1419],{},"    service = BillingService(repository=repo, notifier=notifier, clock=clock)\n",[302,1421,1422],{"class":304,"line":329},[302,1423,315],{"emptyLinePlaceholder":314},[302,1425,1426],{"class":304,"line":334},[302,1427,1428],{},"    service.publish(invoice.id)                      # the action under test\n",[302,1430,1431],{"class":304,"line":340},[302,1432,315],{"emptyLinePlaceholder":314},[302,1434,1435],{"class":304,"line":346},[302,1436,1437],{},"    assert repo.get(invoice.id).status == \"published\"    # state, from the fake\n",[302,1439,1440],{"class":304,"line":351},[302,1441,1442],{},"    assert notifier.sent == [(\"customer@example.test\", \"invoice_published\")]\n",[10,1444,1445,1446,1449,1450,1453],{},"Both assertions read state the fakes accumulated, not calls they recorded. That difference is what lets ",[13,1447,1448],{},"BillingService"," be refactored — the notification moved behind an outbox, the save reordered — without touching the test, as long as the observable outcome is unchanged. A test asserting ",[13,1451,1452],{},"notifier.send.assert_called_once_with(...)"," is coupled to the call signature and breaks on a refactor that changed nothing a user could see.",[10,1455,1456,1457,1459,1460,1459,1463,1466],{},"When a fake needs an accumulator to support this, give it one deliberately: a ",[13,1458,1014],{}," list, a ",[13,1461,1462],{},"published",[13,1464,1465],{},"deleted"," set. These are part of the fake's test-facing API and should be as carefully designed as the contract itself, because every test in the suite will read them.",[18,1468,1470],{"id":1469},"reading-a-suites-double-usage","Reading a suite's double usage",[10,1472,1473],{},"An existing suite can be assessed quickly, and the numbers usually tell a clear story.",[293,1475,1477],{"className":812,"code":1476,"language":814,"meta":298,"style":298},"# How much configuration is going into doubles?\ngrep -rc \"return_value\\|side_effect\\|assert_called\" tests\u002F | sort -t: -k2 -rn | head\n# Which collaborators are mocked most often?\ngrep -rho 'patch(\"[^\"]*\"' tests\u002F | sort | uniq -c | sort -rn | head\n",[13,1478,1479,1484,1519,1524],{"__ignoreMap":298},[302,1480,1481],{"class":304,"line":305},[302,1482,1483],{"class":839},"# How much configuration is going into doubles?\n",[302,1485,1486,1489,1492,1495,1498,1502,1505,1508,1511,1514,1516],{"class":304,"line":311},[302,1487,1488],{"class":821},"grep",[302,1490,1491],{"class":829}," -rc",[302,1493,1494],{"class":825}," \"return_value\\|side_effect\\|assert_called\"",[302,1496,1497],{"class":825}," tests\u002F",[302,1499,1501],{"class":1500},"szBVR"," |",[302,1503,1504],{"class":821}," sort",[302,1506,1507],{"class":829}," -t:",[302,1509,1510],{"class":829}," -k2",[302,1512,1513],{"class":829}," -rn",[302,1515,1501],{"class":1500},[302,1517,1518],{"class":821}," head\n",[302,1520,1521],{"class":304,"line":318},[302,1522,1523],{"class":839},"# Which collaborators are mocked most often?\n",[302,1525,1526,1528,1531,1534,1536,1538,1540,1542,1545,1548,1550,1552,1554,1556],{"class":304,"line":324},[302,1527,1488],{"class":821},[302,1529,1530],{"class":829}," -rho",[302,1532,1533],{"class":825}," 'patch(\"[^\"]*\"'",[302,1535,1497],{"class":825},[302,1537,1501],{"class":1500},[302,1539,1504],{"class":821},[302,1541,1501],{"class":1500},[302,1543,1544],{"class":821}," uniq",[302,1546,1547],{"class":829}," -c",[302,1549,1501],{"class":1500},[302,1551,1504],{"class":821},[302,1553,1513],{"class":829},[302,1555,1501],{"class":1500},[302,1557,1518],{"class":821},[10,1559,1560,1561,1564,1565,1568],{},"A collaborator patched in forty tests is a fake waiting to be written. A single test file with sixty ",[13,1562,1563],{},"return_value"," assignments is usually one where the double has become a second implementation, configured inline, with no name. And a high ratio of ",[13,1566,1567],{},"assert_called_with"," to ordinary assertions suggests the suite is testing call sequences rather than outcomes, which is the pattern that makes refactoring expensive.",[10,1570,1571],{},"A fourth signal is worth watching: how often a mock's configuration is copied between test files. Duplicated setup is the cheapest possible evidence that a shared double is missing, and unlike the counts above it points at the exact behaviour to encode, because the duplicated lines are the contract.",[10,1573,1574],{},"None of these numbers is a target. They are a way of finding the three or four places where the effort of writing a proper double pays back immediately, rather than converting a suite wholesale on principle.",[18,1576,1578],{"id":1577},"migrating-a-mock-heavy-suite-incrementally","Migrating a mock-heavy suite, incrementally",[10,1580,1581],{},"Replacing mocks with fakes wholesale is a bad trade; doing it where the pain is concentrated is a good one. The sequence that works has four steps and can stop after any of them.",[10,1583,1584,1587,1588,1590],{},[76,1585,1586],{},"Find the collaborator that is mocked most."," The ",[13,1589,1488],{}," above gives it in a second. It is usually a repository, an HTTP client or a message publisher.",[10,1592,1593,1596,1597,1599],{},[76,1594,1595],{},"Write the contract suite before the fake."," Derive the behaviours from what the existing mocks were configured to do — every ",[13,1598,1563],{}," in the suite is somebody's belief about the real object, and collecting them is the fastest way to discover what the contract actually is. Run that suite against the real implementation first; it will fail in one or two places, and those are genuine bugs in the tests.",[10,1601,1602,1605],{},[76,1603,1604],{},"Write the fake, and switch one module's tests to it."," Not the whole suite. One module is enough to reveal whether the fake's shape is right, and cheap to throw away if it is not.",[10,1607,1608,1611],{},[76,1609,1610],{},"Delete the mocks as tests are touched."," A migration with a deadline becomes a large, risky change nobody reviews properly; one that happens as files are edited anyway finishes in a few months with no dedicated effort.",[102,1613,1615,1733],{"className":1614},[105],[107,1616,115,1621,115,1624,115,1627,115,1634,115,1638,115,1642,115,1648,115,1651,115,1655,115,1658,115,1661,115,1667,115,1669,115,1673,115,1677,115,1680,115,1683,115,1687,115,1690,115,1693,115,1697,115,1700,115,1703,115,1707,115,1710,115,1714,115,1718,115,1721,115,1724,115,1729],{"viewBox":1617,"role":110,"ariaLabelledBy":1618,"xmlns":114},"0 0 800 244",[1619,1620],"mig-t","mig-d",[117,1622,1623],{"id":1619},"Four steps from a mock-heavy suite to a fake",[121,1625,1626],{"id":1620},"A left-to-right sequence. First find the most-mocked collaborator. Second, derive a contract suite from the existing mock configurations and run it against the real implementation. Third, write the fake and switch one module. Fourth, remove remaining mocks opportunistically as files are edited. Each step is independently valuable.",[1114,1628,1116,1629,115],{},[1118,1630,1632],{"id":1631,"viewBox":1121,"refX":273,"refY":1122,"markerWidth":1123,"markerHeight":1123,"orient":1124},"mig-a",[1126,1633],{"d":1128,"fill":140},[125,1635],{"x":127,"y":127,"width":1636,"height":1637,"rx":130,"fill":131},"800","244",[133,1639,1641],{"x":1640,"y":136,"textAnchor":137,"fontSize":138,"fontWeight":139,"fill":140},"400","Each step is useful on its own",[125,1643],{"x":144,"y":1644,"width":1645,"height":1646,"rx":148,"fill":1647,"stroke":140,"strokeWidth":150},"56","176","110","#f4f1de",[133,1649,1650],{"x":1646,"y":1201,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"1 · find",[133,1652,1654],{"x":161,"y":1653,"fontSize":148,"fill":140},"108","most-patched target",[133,1656,1657],{"x":161,"y":1153,"fontSize":148,"fill":140},"one grep",[133,1659,1660],{"x":161,"y":146,"fontSize":148,"fill":220},"tells you where",[304,1662],{"x1":1663,"y1":1664,"x2":1665,"y2":1664,"stroke":140,"strokeWidth":150,"markerEnd":1666},"202","111","228","url(#mig-a)",[125,1668],{"x":1224,"y":1644,"width":1645,"height":1646,"rx":148,"fill":1178,"stroke":178,"strokeWidth":202},[133,1670,1672],{"x":1671,"y":1201,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"322","2 · contract",[133,1674,1676],{"x":1675,"y":1653,"fontSize":148,"fill":140},"248","derive from existing",[133,1678,1679],{"x":1675,"y":1153,"fontSize":148,"fill":140},"return_value settings",[133,1681,1682],{"x":1675,"y":146,"fontSize":148,"fill":1186},"finds wrong beliefs",[304,1684],{"x1":1685,"y1":1664,"x2":1686,"y2":1664,"stroke":140,"strokeWidth":150,"markerEnd":1666},"414","440",[125,1688],{"x":1689,"y":1644,"width":147,"height":1646,"rx":148,"fill":249,"stroke":201,"strokeWidth":202},"446",[133,1691,1692],{"x":233,"y":1201,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"3 · one module",[133,1694,1696],{"x":1695,"y":1653,"fontSize":148,"fill":140},"460","write the fake",[133,1698,1699],{"x":1695,"y":1153,"fontSize":148,"fill":140},"switch a single file",[133,1701,1702],{"x":1695,"y":146,"fontSize":148,"fill":220},"cheap to revert",[304,1704],{"x1":1705,"y1":1664,"x2":1706,"y2":1664,"stroke":140,"strokeWidth":150,"markerEnd":1666},"614","640",[125,1708],{"x":1709,"y":1644,"width":1172,"height":1646,"rx":148,"fill":249,"stroke":201,"strokeWidth":202},"646",[133,1711,1713],{"x":1712,"y":1201,"textAnchor":137,"fontSize":158,"fontWeight":139,"fill":140},"712","4 · drift",[133,1715,1717],{"x":1716,"y":1653,"fontSize":148,"fill":140},"660","replace as",[133,1719,1720],{"x":1716,"y":1153,"fontSize":148,"fill":140},"files are edited",[133,1722,1723],{"x":1716,"y":146,"fontSize":148,"fill":220},"no deadline",[125,1725],{"x":144,"y":1726,"width":1727,"height":1728,"rx":273,"fill":131,"stroke":149,"strokeWidth":274},"188","756","40",[133,1730,1732],{"x":1640,"y":1731,"textAnchor":137,"fontSize":158,"fill":140},"213","Stopping after step two still leaves the suite better: the contract is now written down and verified.",[280,1734,1735],{},"Step two is the surprising one. Collecting what the mocks were configured to return usually reveals two or three places where the tests believed something the real implementation never did.",[18,1737,1739],{"id":1738},"frequently-asked-questions","Frequently Asked Questions",[10,1741,1742,1745,1746,1748],{},[76,1743,1744],{},"When is a hand-written fake better than MagicMock?","\nWhenever more than two or three tests need the same collaborator to behave consistently. A fake encodes the collaborator's contract once, so a change to that contract fails in one place; the equivalent ",[13,1747,1028],{}," configuration is repeated per test and drifts silently. Fakes also make tests readable, because the setup says what the world is rather than which methods return what.",[10,1750,1751,1754],{},[76,1752,1753],{},"What is the difference between a spy and a mock?","\nA spy wraps a real object and records calls while still delegating to it, so behaviour is unchanged and the recording is additive. A mock replaces the object entirely and returns configured values. Use a spy when the real behaviour is wanted and only the interaction needs asserting.",[10,1756,1757,1760],{},[76,1758,1759],{},"How do I keep a fake honest as the real implementation changes?","\nRun the same test suite against both. A shared contract test, parametrised over the real object and the fake, proves they agree on the behaviours the code depends on. Without it a fake drifts and the tests keep passing against a collaborator that no longer exists.",[10,1762,1763,1766,1767,1079,1770,1086,1773,1776],{},[76,1764,1765],{},"Is it acceptable for a fake to be simpler than the real thing?","\nYes, and it should be. A fake reproduces the behaviours the code under test depends on, not the implementation. An in-memory repository needs ",[13,1768,1769],{},"get",[13,1771,1772],{},"save",[13,1774,1775],{},"list"," with the same semantics; it does not need connection pooling, retries or SQL. The contract test defines where the line is.",[10,1778,1779,1782],{},[76,1780,1781],{},"Should fakes live with the tests or with the production code?","\nWith the production code, next to the interface they implement, when other packages' tests also need them. A fake shipped beside the real client is a documented, versioned part of the contract, which is how most well-designed SDKs provide one. Keep it in the test tree only while it is used by a single package.",[18,1784,1786],{"id":1785},"related-guides","Related guides",[23,1788,1789,1796,1803,1810,1815],{},[26,1790,1791,1792,32],{},"Build one end to end in ",[54,1793,1795],{"href":1794},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fwriting-an-in-memory-fake-repository\u002F","writing an in-memory fake repository",[26,1797,1798,1799,32],{},"Observe without replacing using ",[54,1800,1802],{"href":1801},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps\u002F","spying on a real object with wraps",[26,1804,1805,1806,32],{},"Get the substitution seam in place first via ",[54,1807,1809],{"href":1808},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Finjecting-fakes-vs-mocks-in-constructors\u002F","injecting fakes vs mocks in constructors",[26,1811,1812,1813,32],{},"Keep mocks strict where you do use them with ",[54,1814,1033],{"href":1032},[26,1816,1817,1818,32],{},"Run the contract suite's other half against a real service using ",[54,1819,1821],{"href":1820},"\u002Fintegration-database-and-service-testing\u002F","integration, database and service testing",[10,1823,1824,1825],{},"← Back to ",[54,1826,1828],{"href":1827},"\u002Fadvanced-mocking-test-doubles-in-python\u002F","Advanced Mocking & Test Doubles in Python",[1830,1831,1832],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":298,"searchDepth":311,"depth":311,"links":1834},[1835,1836,1837,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852],{"id":20,"depth":311,"text":21},{"id":67,"depth":311,"text":68},{"id":285,"depth":311,"text":286,"children":1838},[1839,1840,1841,1842],{"id":290,"depth":318,"text":291},{"id":385,"depth":318,"text":386},{"id":535,"depth":318,"text":536},{"id":726,"depth":318,"text":727},{"id":802,"depth":311,"text":803},{"id":875,"depth":311,"text":876},{"id":976,"depth":311,"text":977},{"id":1039,"depth":311,"text":1040},{"id":1231,"depth":311,"text":1232},{"id":1391,"depth":311,"text":1392},{"id":1469,"depth":311,"text":1470},{"id":1577,"depth":311,"text":1578},{"id":1738,"depth":311,"text":1739},{"id":1785,"depth":311,"text":1786},"Choose the right double: fakes with real behaviour, spies that record without replacing, stubs, and when a hand-written class beats MagicMock in a Python test suite.","md",{"slug":1856,"type":1857,"breadcrumb":1858,"datePublished":1859,"dateModified":1859,"faq":1860,"howto":1871},"spies-fakes-and-hand-rolled-test-doubles","topic","Spies & Fakes","2026-09-18",[1861,1863,1865,1867,1869],{"q":1744,"a":1862},"Whenever more than two or three tests need the same collaborator to behave consistently. A fake encodes the collaborator's contract once, so a change to that contract fails in one place; the equivalent MagicMock configuration is repeated per test and drifts silently. Fakes also make tests readable, because the setup says what the world is rather than which methods return what.",{"q":1753,"a":1864},"A spy wraps a real object and records calls while still delegating to it, so behaviour is unchanged and the recording is additive. A mock replaces the object entirely and returns configured values. Use a spy when the real behaviour is wanted and only the interaction needs asserting.",{"q":1759,"a":1866},"Run the same test suite against both. A shared contract test, parametrised over the real object and the fake, proves they agree on the behaviours the code depends on. Without it a fake drifts and the tests keep passing against a collaborator that no longer exists.",{"q":1765,"a":1868},"Yes, and it should be. A fake reproduces the behaviours the code under test depends on, not the implementation. An in-memory repository needs get, save and list with the same semantics; it does not need connection pooling, retries or SQL. The contract test defines where the line is.",{"q":1781,"a":1870},"With the production code, next to the interface they implement, when other packages' tests also need them. A fake shipped beside the real client is a documented, versioned part of the contract, which is how most well-designed SDKs provide one. Keep it in the test tree only while it is used by a single package.",{"name":1872,"description":1873,"steps":1874},"How to build a fake that stays trustworthy","Implement the collaborator's contract in memory, then prove the fake and the real implementation agree with a shared contract test.",[1875,1878,1881,1884,1887],{"name":1876,"text":1877},"Define the collaborator as a Protocol","Write the interface the code under test depends on as a typing.Protocol so both implementations are checked against it.",{"name":1879,"text":1880},"Implement the fake in memory","Back the fake with a dict or a list and reproduce only the behaviours the contract promises, including its error cases.",{"name":1882,"text":1883},"Write one contract test suite","Parametrise a suite of behavioural tests over both implementations so any divergence fails immediately.",{"name":1885,"text":1886},"Run the fake everywhere and the real one at the boundary","Use the fake in the fast suite and the real implementation in the small number of integration tests that exercise the boundary itself.",{"name":1888,"text":1889},"Add a spy only when behaviour must be preserved","Where the real object's behaviour is needed and only the calls are in question, wrap rather than replace.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles",{"title":5,"description":1853},"advanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Findex","OR3myAcR15-bULmMps0AozY0ggBY9vmS_cXUzFCsdy4",1789718765720]