[{"data":1,"prerenderedAt":899},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps\u002F":3},{"id":4,"title":5,"body":6,"description":862,"extension":863,"meta":864,"navigation":94,"path":895,"seo":896,"stem":897,"__hash__":898},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps\u002Findex.md","Spying on a Real Object with wraps",{"type":7,"value":8,"toc":851},"minimark",[9,13,24,32,37,65,69,72,154,215,355,359,392,395,401,405,468,472,475,481,487,497,500,585,589,602,659,681,743,747,750,767,771,789,799,808,812,842,847],[10,11,12],"p",{},"Sometimes a test needs the real collaborator — its real computation, its real side effects — and also needs to know how it was used. A cache test wants the real cache but must confirm that the second lookup did not reach the backing store. A retry test wants the real HTTP client against a local server but must count the attempts. Replacing the collaborator with a stub would lose the behaviour; not replacing it would lose the observation. A spy does both: it wraps the real object, forwards every call, and records each one.",[10,14,15,19,20,23],{},[16,17,18],"code",{},"unittest.mock"," supports spies directly through the ",[16,21,22],{},"wraps"," argument, and they are 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 something unrealistic.",[10,25,26,27,31],{},"That property makes spies particularly well suited to a category of test that is otherwise awkward to write: assertions about efficiency. Caching, batching, deduplication, retry limits and connection reuse are all claims about how ",[28,29,30],"em",{},"often"," something happens, and none of them can be checked by looking at return values alone. A spy lets the test observe the frequency while every value in the system remains real, which is why the examples below are all of that shape.",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,47,53],"ul",{},[41,42,43,44,46],"li",{},"Python 3.8+; ",[16,45,18],{}," in the standard library.",[41,48,49,52],{},[16,50,51],{},"pytest >= 8.0",".",[41,54,55,56,59,60,52],{},"The target resolution rules for ",[16,57,58],{},"patch"," in ",[61,62,64],"a",{"href":63},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fwhere-to-patch-understanding-mock-patch-targets\u002F","where to patch",[33,66,68],{"id":67},"solution","Solution",[10,70,71],{},"Wrap the one method whose use must be observed, and let it keep running.",[73,74,79],"pre",{"className":75,"code":76,"language":77,"meta":78,"style":78},"language-python shiki shiki-themes github-light github-dark","from unittest.mock import patch\n\n\ndef test_second_lookup_is_served_from_the_cache(cached_prices, backing_store):\n    # Spy on the backing store's fetch: real behaviour, recorded calls.\n    with patch.object(backing_store, \"fetch\", wraps=backing_store.fetch) as fetch:\n        first = cached_prices.get(\"SKU-1\")\n        second = cached_prices.get(\"SKU-1\")\n\n    assert first == second                 # real values from the real store\n    assert fetch.call_count == 1           # the cache served the second lookup\n    fetch.assert_called_once_with(\"SKU-1\")\n","python","",[16,80,81,89,96,101,107,113,119,125,131,136,142,148],{"__ignoreMap":78},[82,83,86],"span",{"class":84,"line":85},"line",1,[82,87,88],{},"from unittest.mock import patch\n",[82,90,92],{"class":84,"line":91},2,[82,93,95],{"emptyLinePlaceholder":94},true,"\n",[82,97,99],{"class":84,"line":98},3,[82,100,95],{"emptyLinePlaceholder":94},[82,102,104],{"class":84,"line":103},4,[82,105,106],{},"def test_second_lookup_is_served_from_the_cache(cached_prices, backing_store):\n",[82,108,110],{"class":84,"line":109},5,[82,111,112],{},"    # Spy on the backing store's fetch: real behaviour, recorded calls.\n",[82,114,116],{"class":84,"line":115},6,[82,117,118],{},"    with patch.object(backing_store, \"fetch\", wraps=backing_store.fetch) as fetch:\n",[82,120,122],{"class":84,"line":121},7,[82,123,124],{},"        first = cached_prices.get(\"SKU-1\")\n",[82,126,128],{"class":84,"line":127},8,[82,129,130],{},"        second = cached_prices.get(\"SKU-1\")\n",[82,132,134],{"class":84,"line":133},9,[82,135,95],{"emptyLinePlaceholder":94},[82,137,139],{"class":84,"line":138},10,[82,140,141],{},"    assert first == second                 # real values from the real store\n",[82,143,145],{"class":84,"line":144},11,[82,146,147],{},"    assert fetch.call_count == 1           # the cache served the second lookup\n",[82,149,151],{"class":84,"line":150},12,[82,152,153],{},"    fetch.assert_called_once_with(\"SKU-1\")\n",[73,155,157],{"className":75,"code":156,"language":77,"meta":78,"style":78},"from unittest.mock import Mock\n\n\ndef test_client_retries_three_times_against_a_flaky_server(local_server, client_factory):\n    local_server.fail_next(2)                       # real server, scripted failures\n    real = client_factory(local_server.url)\n    spy = Mock(wraps=real)                          # whole-object spy\n\n    result = retrying_fetch(spy, \"\u002Fprices\")\n\n    assert result.status_code == 200\n    assert spy.get.call_count == 3                  # two failures, one success\n",[16,158,159,164,168,172,177,182,187,192,196,201,205,210],{"__ignoreMap":78},[82,160,161],{"class":84,"line":85},[82,162,163],{},"from unittest.mock import Mock\n",[82,165,166],{"class":84,"line":91},[82,167,95],{"emptyLinePlaceholder":94},[82,169,170],{"class":84,"line":98},[82,171,95],{"emptyLinePlaceholder":94},[82,173,174],{"class":84,"line":103},[82,175,176],{},"def test_client_retries_three_times_against_a_flaky_server(local_server, client_factory):\n",[82,178,179],{"class":84,"line":109},[82,180,181],{},"    local_server.fail_next(2)                       # real server, scripted failures\n",[82,183,184],{"class":84,"line":115},[82,185,186],{},"    real = client_factory(local_server.url)\n",[82,188,189],{"class":84,"line":121},[82,190,191],{},"    spy = Mock(wraps=real)                          # whole-object spy\n",[82,193,194],{"class":84,"line":127},[82,195,95],{"emptyLinePlaceholder":94},[82,197,198],{"class":84,"line":133},[82,199,200],{},"    result = retrying_fetch(spy, \"\u002Fprices\")\n",[82,202,203],{"class":84,"line":138},[82,204,95],{"emptyLinePlaceholder":94},[82,206,207],{"class":84,"line":144},[82,208,209],{},"    assert result.status_code == 200\n",[82,211,212],{"class":84,"line":150},[82,213,214],{},"    assert spy.get.call_count == 3                  # two failures, one success\n",[216,217,220,351],"figure",{"className":218},[219],"diagram",[221,222,229,230,229,234,229,238,229,256,229,264,229,273,229,282,229,288,229,292,229,297,229,306,229,311,229,316,229,321,229,325,229,331,229,335,229,339,229,346],"svg",{"viewBox":223,"role":224,"ariaLabelledBy":225,"xmlns":228},"0 0 820 262","img",[226,227],"wr-t","wr-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[231,232,233],"title",{"id":226},"A spy between the caller and the real object",[235,236,237],"desc",{"id":227},"The code under test calls what it believes is the backing store. The spy records the call and its arguments in call_args_list, then forwards the call to the real fetch method and returns the real result unchanged. The test later asserts on the recorded calls while the returned values came from real behaviour.",[239,240,241,242,229],"defs",{},"\n    ",[243,244,251],"marker",{"id":245,"viewBox":246,"refX":247,"refY":248,"markerWidth":249,"markerHeight":249,"orient":250},"wr-a","0 0 10 10","9","5","7","auto-start-reverse",[252,253],"path",{"d":254,"fill":255},"M0 0 L10 5 L0 10 z","#3d405b",[257,258],"rect",{"x":259,"y":259,"width":260,"height":261,"rx":262,"fill":263},"0","820","262","14","#fffdf8",[265,266,272],"text",{"x":267,"y":268,"textAnchor":269,"fontSize":270,"fontWeight":271,"fill":255},"410","28","middle","16","700","Record on the way in, forward unchanged",[257,274],{"x":275,"y":276,"width":277,"height":278,"rx":279,"fill":280,"stroke":255,"strokeWidth":281},"26","96","180","70","11","#f4f1de","1.6",[265,283,287],{"x":284,"y":285,"textAnchor":269,"fontSize":286,"fontWeight":271,"fill":255},"116","124","12","cache.get()",[265,289,291],{"x":284,"y":290,"textAnchor":269,"fontSize":279,"fill":255},"146","code under test",[84,293],{"x1":294,"y1":295,"x2":261,"y2":295,"stroke":255,"strokeWidth":281,"markerEnd":296},"210","131","url(#wr-a)",[257,298],{"x":299,"y":300,"width":301,"height":302,"rx":279,"fill":303,"stroke":304,"strokeWidth":305},"268","80","230","102","#f7f0da","#f2cc8f","2",[265,307,310],{"x":308,"y":309,"textAnchor":269,"fontSize":286,"fontWeight":271,"fill":255},"383","106","spy (wraps=real)",[265,312,315],{"x":313,"y":314,"fontSize":279,"fill":255},"284","132","call_args_list.append(…)",[265,317,320],{"x":313,"y":318,"fontSize":279,"fill":319},"154","#8a5a00","then forward",[84,322],{"x1":323,"y1":295,"x2":324,"y2":295,"stroke":255,"strokeWidth":281,"markerEnd":296},"502","554",[257,326],{"x":327,"y":276,"width":328,"height":278,"rx":279,"fill":329,"stroke":330,"strokeWidth":305},"560","234","#e6f0ea","#81b29a",[265,332,334],{"x":333,"y":285,"textAnchor":269,"fontSize":286,"fontWeight":271,"fill":255},"677","backing_store.fetch",[265,336,338],{"x":333,"y":290,"textAnchor":269,"fontSize":279,"fill":337},"#2a5f49","real behaviour, real result",[257,340],{"x":275,"y":341,"width":342,"height":343,"rx":247,"fill":263,"stroke":344,"strokeWidth":345},"204","768","38","rgba(61,64,91,0.35)","1.4",[265,347,350],{"x":267,"y":348,"textAnchor":269,"fontSize":349,"fill":255},"228","11.5","The result flows back untouched; only the record of the call is new.",[352,353,354],"figcaption",{},"Because the result is real, any assertion on values tests the actual system. The spy adds observation and nothing else.",[33,356,358],{"id":357},"why-this-works","Why this works",[10,360,361,362,365,366,368,369,372,373,376,377,380,381,384,385,388,389,52],{},"A ",[16,363,364],{},"Mock"," with ",[16,367,22],{}," set does two things on every call: it records the call in ",[16,370,371],{},"mock_calls"," and ",[16,374,375],{},"call_args_list"," exactly as any mock would, and then — because no ",[16,378,379],{},"return_value"," or ",[16,382,383],{},"side_effect"," is configured — it calls the wrapped object with the same arguments and returns whatever that returns. Attribute access is wrapped recursively, so ",[16,386,387],{},"spy.get"," is itself a spy around ",[16,390,391],{},"real.get",[10,393,394],{},"The forwarding is literal: arguments, keyword arguments and the return value pass through unchanged, and an exception raised by the real method propagates through the spy to the caller exactly as it would without it. That makes a spy safe to insert into any test that already passes — the only possible change in outcome is a new assertion on the recorded calls.",[10,396,397,400],{},[16,398,399],{},"patch.object(target, name, wraps=original)"," applies the same idea to one attribute of a live object, which is usually preferable. It leaves the rest of the object untouched, it restores the original when the context exits, and it makes the test's intent explicit: only this method's use is under observation.",[33,402,404],{"id":403},"edge-cases-and-failure-modes","Edge cases and failure modes",[38,406,407,417,427,445,458],{},[41,408,409,416],{},[410,411,412,413,415],"strong",{},"Configuring ",[16,414,379],{}," on a spy."," It silently stops forwarding for that attribute, turning the spy into a stub. If the real result is needed, leave it unset.",[41,418,419,422,423,426],{},[410,420,421],{},"Wrapping a property."," Properties are resolved on the class, so wrapping the instance attribute does nothing. Patch the property on the class with ",[16,424,425],{},"new_callable=PropertyMock"," and a wrapped getter.",[41,428,429,432,433,436,437,440,441,444],{},[410,430,431],{},"Identity checks."," Code that checks ",[16,434,435],{},"isinstance(obj, RealClient)"," fails against a ",[16,438,439],{},"Mock(wraps=...)",". Spy on a method with ",[16,442,443],{},"patch.object"," instead of replacing the object.",[41,446,447,450,451,453,454,457],{},[410,448,449],{},"Async methods."," Wrapping an async method with a plain ",[16,452,364],{}," returns the coroutine without recording the await. Use ",[16,455,456],{},"AsyncMock(wraps=...)"," so awaits are recorded too.",[41,459,460,463,464,467],{},[410,461,462],{},"Spies that outlive the test."," A spy assigned without a context manager or ",[16,465,466],{},"monkeypatch"," stays in place for later tests. Always patch through a mechanism that restores.",[33,469,471],{"id":470},"when-a-spy-is-the-right-double","When a spy is the right double",[10,473,474],{},"A spy is the right choice in a narrow but common set of situations, and recognising them avoids reaching for a stub out of habit.",[10,476,477,480],{},[410,478,479],{},"The claim is about efficiency, not correctness."," \"The second lookup is served from cache\", \"the batch makes one request rather than ten\", \"the retry stops after three attempts\" — these are statements about how often something happens, and the only way to test them without changing the behaviour is to count calls to the real thing. A stub would make the count trivially correct and the values meaningless.",[10,482,483,486],{},[410,484,485],{},"The real collaborator is cheap and deterministic."," A local server, an in-process cache, a pure computation — spying on these costs nothing and keeps the test honest. Spying on a slow or non-deterministic collaborator inherits its slowness and flakiness, and there a fake is usually better.",[10,488,489,492,493,496],{},[410,490,491],{},"The behaviour under test depends on the collaborator's real output."," A parser that feeds its result to a validator, where the test must confirm the validator was consulted ",[28,494,495],{},"and"," that the parser's real output passed validation, needs both halves. A stubbed validator would pass anything.",[10,498,499],{},"Outside those situations, a spy tends to be a hesitation between two better options: if the interaction does not matter, use the real object without a spy; if the behaviour does not matter, use a stub or a fake. Being explicit about which of the three the test actually needs usually makes the test shorter as well as clearer, because the double that fits requires the least configuration.",[216,501,503,582],{"className":502},[219],[221,504,229,509,229,512,229,515,229,519,229,524,229,530,229,535,229,540,229,543,229,546,229,549,229,552,229,556,229,559,229,562,229,567,229,571,229,575,229,578],{"viewBox":505,"role":224,"ariaLabelledBy":506,"xmlns":228},"0 0 800 236",[507,508],"sp2-t","sp2-d",[231,510,511],{"id":507},"Choosing between real object, spy and stub",[235,513,514],{"id":508},"A decision by two questions. If neither the collaborator's behaviour nor the interaction with it matters to the test, use the real object. If only the interaction matters, a stub or mock suffices. If both the real behaviour and the interaction matter, use a spy, provided the real collaborator is cheap and deterministic.",[257,516],{"x":259,"y":259,"width":517,"height":518,"rx":262,"fill":263},"800","236",[265,520,523],{"x":521,"y":268,"textAnchor":269,"fontSize":522,"fontWeight":271,"fill":255},"400","15.5","Does the test need the real behaviour, the interaction, or both?",[257,525],{"x":526,"y":527,"width":528,"height":529,"rx":286,"fill":329,"stroke":330,"strokeWidth":305},"24","52","240","160",[265,531,534],{"x":532,"y":300,"textAnchor":269,"fontSize":533,"fontWeight":271,"fill":255},"144","12.5","behaviour only",[265,536,539],{"x":537,"y":538,"fontSize":279,"fill":255},"40","110","use the real object",[265,541,542],{"x":537,"y":314,"fontSize":279,"fill":255},"no double at all",[265,544,545],{"x":537,"y":277,"fontSize":279,"fill":337},"simplest and most honest",[257,547],{"x":548,"y":527,"width":528,"height":529,"rx":286,"fill":303,"stroke":304,"strokeWidth":305},"280",[265,550,551],{"x":521,"y":300,"textAnchor":269,"fontSize":533,"fontWeight":271,"fill":255},"interaction only",[265,553,555],{"x":554,"y":538,"fontSize":279,"fill":255},"296","stub, mock or fake",[265,557,558],{"x":554,"y":314,"fontSize":279,"fill":255},"real behaviour irrelevant",[265,560,561],{"x":554,"y":277,"fontSize":279,"fill":319},"fast, isolated",[257,563],{"x":564,"y":527,"width":528,"height":529,"rx":286,"fill":565,"stroke":566,"strokeWidth":305},"536","#fbe9e3","#e07a5f",[265,568,570],{"x":569,"y":300,"textAnchor":269,"fontSize":533,"fontWeight":271,"fill":255},"656","both",[265,572,574],{"x":573,"y":538,"fontSize":279,"fill":255},"552","spy with wraps=",[265,576,577],{"x":573,"y":314,"fontSize":279,"fill":255},"if the real thing is cheap",[265,579,581],{"x":573,"y":277,"fontSize":279,"fill":580},"#8f3d22","counts, caching, retries",[352,583,584],{},"The right-hand column is smaller than it looks: many tests that reach for a spy actually only need one of the other two.",[33,586,588],{"id":587},"spying-on-async-collaborators","Spying on async collaborators",[10,590,591,592,595,596,598,599,601],{},"Async code needs ",[16,593,594],{},"AsyncMock"," for the same reason it needs it everywhere else: a plain ",[16,597,364],{}," wrapping a coroutine function returns the coroutine without recording that it was awaited, so a test can confirm a call happened while the await never did. ",[16,600,456],{}," records both and forwards the await to the real coroutine.",[73,603,605],{"className":75,"code":604,"language":77,"meta":78,"style":78},"from unittest.mock import AsyncMock, patch\n\n\nasync def test_prefetch_warms_the_cache_once(prices, backing_store):\n    with patch.object(backing_store, \"fetch\",\n                      new=AsyncMock(wraps=backing_store.fetch)) as fetch:\n        await prices.prefetch([\"SKU-1\", \"SKU-2\"])\n        await prices.get(\"SKU-1\")                  # served from the warmed cache\n\n    assert fetch.await_count == 2                  # one per SKU, none for the get\n    assert [c.args[0] for c in fetch.await_args_list] == [\"SKU-1\", \"SKU-2\"]\n",[16,606,607,612,616,620,625,630,635,640,645,649,654],{"__ignoreMap":78},[82,608,609],{"class":84,"line":85},[82,610,611],{},"from unittest.mock import AsyncMock, patch\n",[82,613,614],{"class":84,"line":91},[82,615,95],{"emptyLinePlaceholder":94},[82,617,618],{"class":84,"line":98},[82,619,95],{"emptyLinePlaceholder":94},[82,621,622],{"class":84,"line":103},[82,623,624],{},"async def test_prefetch_warms_the_cache_once(prices, backing_store):\n",[82,626,627],{"class":84,"line":109},[82,628,629],{},"    with patch.object(backing_store, \"fetch\",\n",[82,631,632],{"class":84,"line":115},[82,633,634],{},"                      new=AsyncMock(wraps=backing_store.fetch)) as fetch:\n",[82,636,637],{"class":84,"line":121},[82,638,639],{},"        await prices.prefetch([\"SKU-1\", \"SKU-2\"])\n",[82,641,642],{"class":84,"line":127},[82,643,644],{},"        await prices.get(\"SKU-1\")                  # served from the warmed cache\n",[82,646,647],{"class":84,"line":133},[82,648,95],{"emptyLinePlaceholder":94},[82,650,651],{"class":84,"line":138},[82,652,653],{},"    assert fetch.await_count == 2                  # one per SKU, none for the get\n",[82,655,656],{"class":84,"line":144},[82,657,658],{},"    assert [c.args[0] for c in fetch.await_args_list] == [\"SKU-1\", \"SKU-2\"]\n",[10,660,661,662,665,666,669,670,673,674,676,677,52],{},"The assertion on the argument order also earns its place: prefetching in a different order than requested is harmless here, but in code where order carries meaning — a queue, a ledger, a sequence of writes — the same ",[16,663,664],{},"await_args_list"," check is how that meaning gets tested. The assertion on ",[16,667,668],{},"await_count"," rather than ",[16,671,672],{},"call_count"," is the one that matters most. A coroutine that was created but never awaited would still increment ",[16,675,672],{},", and the test would pass while the fetch never ran — the silent failure mode described in ",[61,678,680],{"href":679},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002F","patching async code and coroutines",[216,682,684,740],{"className":683},[219],[221,685,229,690,229,693,229,696,229,699,229,702,229,707,229,711,229,716,229,720,229,724,229,727,229,730,229,734,229,737],{"viewBox":686,"role":224,"ariaLabelledBy":687,"xmlns":228},"0 0 800 226",[688,689],"asp-t","asp-d",[231,691,692],{"id":688},"call_count versus await_count on an async spy",[235,694,695],{"id":689},"Two counters on an AsyncMock spy. call_count increments when the coroutine function is called, even if the coroutine is never awaited. await_count increments only when the coroutine is actually awaited, which is when the real work runs. Asserting on await_count proves the real call happened.",[257,697],{"x":259,"y":259,"width":517,"height":698,"rx":262,"fill":263},"226",[265,700,701],{"x":521,"y":268,"textAnchor":269,"fontSize":522,"fontWeight":271,"fill":255},"Called is not the same as awaited",[257,703],{"x":275,"y":704,"width":705,"height":706,"rx":286,"fill":303,"stroke":304,"strokeWidth":305},"50","360","156",[265,708,672],{"x":709,"y":710,"textAnchor":269,"fontSize":533,"fontWeight":271,"fill":255},"206","76",[265,712,715],{"x":713,"y":714,"fontSize":279,"fill":255},"44","104","increments when fetch(…) is called",[265,717,719],{"x":713,"y":718,"fontSize":279,"fill":255},"126","even if the coroutine is dropped",[265,721,723],{"x":713,"y":722,"fontSize":279,"fontWeight":271,"fill":319},"170","can be 2 while no fetch ran",[257,725],{"x":726,"y":704,"width":705,"height":706,"rx":286,"fill":329,"stroke":330,"strokeWidth":305},"414",[265,728,668],{"x":729,"y":710,"textAnchor":269,"fontSize":533,"fontWeight":271,"fill":255},"594",[265,731,733],{"x":732,"y":714,"fontSize":279,"fill":255},"432","increments when it is awaited",[265,735,736],{"x":732,"y":718,"fontSize":279,"fill":255},"which is when the real work runs",[265,738,739],{"x":732,"y":722,"fontSize":279,"fontWeight":271,"fill":337},"proves the fetch happened",[352,741,742],{},"For async spies, every count assertion should use the await-based attribute. The call-based ones are true but not sufficient.",[33,744,746],{"id":745},"spies-and-autospec-together","Spies and autospec together",[10,748,749],{},"A plain spy checks nothing about how it is called until the real method runs, at which point a wrong argument raises from inside the real code — correct, but with a traceback pointing into the collaborator rather than at the caller's mistake. Combining the spy with autospec moves that check to the boundary.",[10,751,752,755,756,759,760,762,763,766],{},[16,753,754],{},"patch.object(target, \"fetch\", autospec=True, side_effect=target.fetch)"," produces a spy whose signature matches the real method exactly. A call with a misspelt keyword fails at the spy with a clear ",[16,757,758],{},"TypeError"," naming the method, and a correct call is forwarded to the real implementation through ",[16,761,383],{},". The small cost is that ",[16,764,765],{},"autospec"," on a bound method receives the instance as its first argument in some configurations, which is worth one quick check the first time; the benefit is that the spy now catches interface drift as well as recording calls, which is exactly the property the autospec guides on this site argue every mock should have.",[33,768,770],{"id":769},"frequently-asked-questions","Frequently Asked Questions",[10,772,773,776,777,779,780,783,784,380,786,788],{},[410,774,775],{},"Does wraps change what the real object returns?","\nNo. A ",[16,778,364],{}," created with ",[16,781,782],{},"wraps=real"," forwards every call to the real object and returns its real result, while recording the call. Setting ",[16,785,379],{},[16,787,383],{}," on the spy overrides that forwarding for the configured attribute only.",[10,790,791,794,795,798],{},[410,792,793],{},"Can a spy wrap a single method rather than a whole object?","\nYes, and it is usually cleaner. ",[16,796,797],{},"patch.object(target, \"method\", wraps=target.method)"," replaces just that method with a recording wrapper, leaving the rest of the object untouched.",[10,800,801,804,805,807],{},[410,802,803],{},"Do spies enforce the real signature?","\nA plain ",[16,806,439],{}," does not check arguments until the real method is called, at which point a wrong signature fails naturally. Combining autospec with wraps checks arguments at the spy itself, which gives a clearer failure.",[33,809,811],{"id":810},"related","Related",[38,813,814,821,828,835],{},[41,815,816,820],{},[61,817,819],{"href":818},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002F","Spies, Fakes & Hand-Rolled Test Doubles"," — where spies sit among the other doubles.",[41,822,823,827],{},[61,824,826],{"href":825},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002Fpatching-class-attributes-with-patch-object\u002F","Patching Class Attributes with patch.object"," — the mechanism used to install a method spy.",[41,829,830,834],{},[61,831,833],{"href":832},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002Fassert-called-with-vs-call-args-list\u002F","assert_called_with vs call_args_list"," — reading what the spy recorded.",[41,836,837,841],{},[61,838,840],{"href":839},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-async-code-and-coroutines\u002Fasserting-await-order-with-asyncmock\u002F","Asserting Await Order with AsyncMock"," — the async counterpart.",[10,843,844,845],{},"← Back to ",[61,846,819],{"href":818},[848,849,850],"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);}",{"title":78,"searchDepth":91,"depth":91,"links":852},[853,854,855,856,857,858,859,860,861],{"id":35,"depth":91,"text":36},{"id":67,"depth":91,"text":68},{"id":357,"depth":91,"text":358},{"id":403,"depth":91,"text":404},{"id":470,"depth":91,"text":471},{"id":587,"depth":91,"text":588},{"id":745,"depth":91,"text":746},{"id":769,"depth":91,"text":770},{"id":810,"depth":91,"text":811},"Record calls to a real collaborator without changing its behaviour using Mock(wraps=...), patch.object(wraps=...) and autospec, and know when a spy beats a stub.","md",{"slug":865,"type":866,"breadcrumb":867,"datePublished":868,"dateModified":868,"faq":869,"howto":876},"spying-on-a-real-object-with-wraps","article","Spies with wraps","2026-09-18",[870,872,874],{"q":775,"a":871},"No. A Mock created with wraps=real forwards every call to the real object and returns its real result, while recording the call. Setting return_value or side_effect on the spy overrides that forwarding for the configured attribute only.",{"q":793,"a":873},"Yes, and it is usually cleaner. patch.object(target, 'method', wraps=target.method) replaces just that method with a recording wrapper, leaving the rest of the object untouched.",{"q":803,"a":875},"A plain Mock(wraps=...) does not check arguments until the real method is called, at which point a wrong signature fails naturally. Combining autospec with wraps checks arguments at the spy itself, which gives a clearer failure.",{"name":877,"description":878,"steps":879},"How to spy on a real object in a test","Wrap the real collaborator or method so calls are recorded and forwarded, then assert on the calls while the real behaviour runs.",[880,883,886,889,892],{"name":881,"text":882},"Decide what to observe","Identify the interaction the test must confirm — a call count, an argument, an ordering — while keeping real behaviour.",{"name":884,"text":885},"Wrap the narrowest target","Prefer patch.object on the one method over wrapping the whole object.",{"name":887,"text":888},"Keep the real behaviour","Leave return_value and side_effect unset so the real method runs.",{"name":890,"text":891},"Assert on the recorded calls","Use call_count, assert_called_with or call_args_list on the spy.",{"name":893,"text":894},"Add autospec where signatures matter","Combine autospec with wraps so a wrong call fails at the spy with a clear message.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps",{"title":5,"description":862},"advanced-mocking-test-doubles-in-python\u002Fspies-fakes-and-hand-rolled-test-doubles\u002Fspying-on-a-real-object-with-wraps\u002Findex","v1QPPouN4HSwEZ_c_k489cn_owHG96iD4ocVXpdnjVQ",1789718768938]