[{"data":1,"prerenderedAt":927},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fautospeccing-clients-that-use-getattr\u002F":3},{"id":4,"title":5,"body":6,"description":890,"extension":891,"meta":892,"navigation":96,"path":923,"seo":924,"stem":925,"__hash__":926},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fautospeccing-clients-that-use-getattr\u002Findex.md","Autospeccing Clients That Use __getattr__",{"type":7,"value":8,"toc":880},"minimark",[9,21,32,35,40,67,71,74,110,113,271,380,384,400,422,426,478,482,485,552,567,578,703,707,710,720,723,810,814,823,832,838,842,871,876],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"create_autospec"," is the strongest defence against mocks that drift from reality: it copies the real object's attributes and signatures, and rejects calls that the real object would reject. It depends on one assumption — that the real object's methods exist as attributes on its class. Many SDKs break that assumption. AWS's boto3, several generated API clients, and most ORMs' dynamic managers build their methods at runtime through ",[13,18,19],{},"__getattr__"," or from a service description, and autospec, inspecting the class, finds nothing to copy.",[10,22,23,24,27,28,31],{},"The result is a mock that looks strict and accepts anything. ",[13,25,26],{},"client.put_objct(Bucket=\"b\")"," — a misspelling — passes, as does a call with an argument the real API does not take. The fix is to give autospec something concrete to inspect, and the best candidates are a ",[13,29,30],{},"Protocol"," declaring the operations your code depends on, or a thin adapter that wraps the SDK behind methods you own.",[10,33,34],{},"The problem is easy to miss because nothing fails. A team adopts autospec across the suite, sees it catch signature drift in its own code, and reasonably assumes the SDK mocks are equally protected. They are not, and the first sign is usually a production error on a call the tests exercised a hundred times — against a mock that would have accepted anything. Checking which of a suite's autospecced targets are dynamic, and fixing those specifically, is a small audit with an unusually high return.",[36,37,39],"h2",{"id":38},"prerequisites","Prerequisites",[41,42,43,55,64],"ul",{},[44,45,46,47,50,51,54],"li",{},"Python 3.8+ for ",[13,48,49],{},"typing.Protocol","; ",[13,52,53],{},"unittest.mock"," in the standard library.",[44,56,57,58,63],{},"The autospec fundamentals from ",[59,60,62],"a",{"href":61},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002F","autospec and strict mocking",".",[44,65,66],{},"A dynamic client to double — boto3 is used as the example, but the pattern is general.",[36,68,70],{"id":69},"solution","Solution",[10,72,73],{},"First, confirm the problem: the method you call is not a real attribute.",[75,76,81],"pre",{"className":77,"code":78,"language":79,"meta":80,"style":80},"language-python shiki shiki-themes github-light github-dark","import boto3\n\nclient = boto3.client(\"s3\")\nprint(\"put_object\" in dir(type(client)))      # False: generated at runtime\n","python","",[13,82,83,91,98,104],{"__ignoreMap":80},[84,85,88],"span",{"class":86,"line":87},"line",1,[84,89,90],{},"import boto3\n",[84,92,94],{"class":86,"line":93},2,[84,95,97],{"emptyLinePlaceholder":96},true,"\n",[84,99,101],{"class":86,"line":100},3,[84,102,103],{},"client = boto3.client(\"s3\")\n",[84,105,107],{"class":86,"line":106},4,[84,108,109],{},"print(\"put_object\" in dir(type(client)))      # False: generated at runtime\n",[10,111,112],{},"Then give autospec a concrete specification of what your code actually uses.",[75,114,116],{"className":77,"code":115,"language":79,"meta":80,"style":80},"from typing import Any, Protocol\nfrom unittest.mock import create_autospec\n\n\nclass ObjectStore(Protocol):\n    \"\"\"The slice of the S3 client this service depends on.\"\"\"\n\n    def put_object(self, *, Bucket: str, Key: str, Body: bytes,\n                   ContentType: str = ...) -> dict[str, Any]: ...\n\n    def get_object(self, *, Bucket: str, Key: str) -> dict[str, Any]: ...\n\n\ndef test_upload_uses_the_right_key():\n    store = create_autospec(ObjectStore, instance=True)\n\n    upload_invoice(store, invoice_id=\"inv_1\", pdf=b\"%PDF\")\n\n    store.put_object.assert_called_once_with(\n        Bucket=\"invoices\", Key=\"inv_1.pdf\", Body=b\"%PDF\", ContentType=\"application\u002Fpdf\"\n    )\n\n\ndef test_misspelt_method_now_fails():\n    store = create_autospec(ObjectStore, instance=True)\n    import pytest\n    with pytest.raises(AttributeError):\n        store.put_objct(Bucket=\"b\", Key=\"k\", Body=b\"\")   # caught by the spec\n",[13,117,118,123,128,132,136,142,148,153,159,165,170,176,181,186,192,198,203,209,214,220,226,232,237,242,248,253,259,265],{"__ignoreMap":80},[84,119,120],{"class":86,"line":87},[84,121,122],{},"from typing import Any, Protocol\n",[84,124,125],{"class":86,"line":93},[84,126,127],{},"from unittest.mock import create_autospec\n",[84,129,130],{"class":86,"line":100},[84,131,97],{"emptyLinePlaceholder":96},[84,133,134],{"class":86,"line":106},[84,135,97],{"emptyLinePlaceholder":96},[84,137,139],{"class":86,"line":138},5,[84,140,141],{},"class ObjectStore(Protocol):\n",[84,143,145],{"class":86,"line":144},6,[84,146,147],{},"    \"\"\"The slice of the S3 client this service depends on.\"\"\"\n",[84,149,151],{"class":86,"line":150},7,[84,152,97],{"emptyLinePlaceholder":96},[84,154,156],{"class":86,"line":155},8,[84,157,158],{},"    def put_object(self, *, Bucket: str, Key: str, Body: bytes,\n",[84,160,162],{"class":86,"line":161},9,[84,163,164],{},"                   ContentType: str = ...) -> dict[str, Any]: ...\n",[84,166,168],{"class":86,"line":167},10,[84,169,97],{"emptyLinePlaceholder":96},[84,171,173],{"class":86,"line":172},11,[84,174,175],{},"    def get_object(self, *, Bucket: str, Key: str) -> dict[str, Any]: ...\n",[84,177,179],{"class":86,"line":178},12,[84,180,97],{"emptyLinePlaceholder":96},[84,182,184],{"class":86,"line":183},13,[84,185,97],{"emptyLinePlaceholder":96},[84,187,189],{"class":86,"line":188},14,[84,190,191],{},"def test_upload_uses_the_right_key():\n",[84,193,195],{"class":86,"line":194},15,[84,196,197],{},"    store = create_autospec(ObjectStore, instance=True)\n",[84,199,201],{"class":86,"line":200},16,[84,202,97],{"emptyLinePlaceholder":96},[84,204,206],{"class":86,"line":205},17,[84,207,208],{},"    upload_invoice(store, invoice_id=\"inv_1\", pdf=b\"%PDF\")\n",[84,210,212],{"class":86,"line":211},18,[84,213,97],{"emptyLinePlaceholder":96},[84,215,217],{"class":86,"line":216},19,[84,218,219],{},"    store.put_object.assert_called_once_with(\n",[84,221,223],{"class":86,"line":222},20,[84,224,225],{},"        Bucket=\"invoices\", Key=\"inv_1.pdf\", Body=b\"%PDF\", ContentType=\"application\u002Fpdf\"\n",[84,227,229],{"class":86,"line":228},21,[84,230,231],{},"    )\n",[84,233,235],{"class":86,"line":234},22,[84,236,97],{"emptyLinePlaceholder":96},[84,238,240],{"class":86,"line":239},23,[84,241,97],{"emptyLinePlaceholder":96},[84,243,245],{"class":86,"line":244},24,[84,246,247],{},"def test_misspelt_method_now_fails():\n",[84,249,251],{"class":86,"line":250},25,[84,252,197],{},[84,254,256],{"class":86,"line":255},26,[84,257,258],{},"    import pytest\n",[84,260,262],{"class":86,"line":261},27,[84,263,264],{},"    with pytest.raises(AttributeError):\n",[84,266,268],{"class":86,"line":267},28,[84,269,270],{},"        store.put_objct(Bucket=\"b\", Key=\"k\", Body=b\"\")   # caught by the spec\n",[272,273,276,376],"figure",{"className":274},[275],"diagram",[277,278,285,286,285,290,285,294,285,302,285,312,285,322,285,328,285,334,285,338,285,343,285,346,285,350,285,355,285,359,285,363,285,366,285,370,285,373],"svg",{"viewBox":279,"role":280,"ariaLabelledBy":281,"xmlns":284},"0 0 820 262","img",[282,283],"ga-t","ga-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[287,288,289],"title",{"id":282},"What autospec sees on a dynamic client versus a Protocol",[291,292,293],"desc",{"id":283},"Two inspections. On the dynamic SDK client class, autospec finds only a __getattr__ hook and no concrete methods, so the resulting mock accepts any attribute and any arguments. On a Protocol listing put_object and get_object with signatures, autospec finds two concrete methods and enforces both names and arguments.",[295,296],"rect",{"x":297,"y":297,"width":298,"height":299,"rx":300,"fill":301},"0","820","262","14","#fffdf8",[303,304,311],"text",{"x":305,"y":306,"textAnchor":307,"fontSize":308,"fontWeight":309,"fill":310},"410","28","middle","16","700","#3d405b","Autospec can only enforce what it can see",[295,313],{"x":314,"y":315,"width":316,"height":317,"rx":318,"fill":319,"stroke":320,"strokeWidth":321},"26","52","368","186","12","#fbe9e3","#e07a5f","2",[303,323,327],{"x":324,"y":325,"textAnchor":307,"fontSize":326,"fontWeight":309,"fill":310},"210","78","12.5","autospec(boto3 client class)",[303,329,333],{"x":330,"y":331,"fontSize":332,"fill":310},"44","106","11","class attributes: __getattr__",[303,335,337],{"x":330,"y":336,"fontSize":332,"fill":310},"128","no put_object, no signatures",[303,339,342],{"x":330,"y":340,"fontSize":332,"fontWeight":309,"fill":341},"164","#8f3d22","put_objct(…) accepted",[303,344,345],{"x":330,"y":317,"fontSize":332,"fill":341},"wrong kwargs accepted",[303,347,349],{"x":330,"y":348,"fontSize":332,"fill":310},"216","looks strict, enforces nothing",[295,351],{"x":352,"y":315,"width":316,"height":317,"rx":318,"fill":353,"stroke":354,"strokeWidth":321},"426","#e6f0ea","#81b29a",[303,356,358],{"x":357,"y":325,"textAnchor":307,"fontSize":326,"fontWeight":309,"fill":310},"610","autospec(ObjectStore Protocol)",[303,360,362],{"x":361,"y":331,"fontSize":332,"fill":310},"444","put_object(*, Bucket, Key, Body, …)",[303,364,365],{"x":361,"y":336,"fontSize":332,"fill":310},"get_object(*, Bucket, Key)",[303,367,369],{"x":361,"y":340,"fontSize":332,"fontWeight":309,"fill":368},"#2a5f49","put_objct → AttributeError",[303,371,372],{"x":361,"y":317,"fontSize":332,"fill":368},"wrong kwargs → TypeError",[303,374,375],{"x":361,"y":348,"fontSize":332,"fill":310},"strict, and documents the dependency",[377,378,379],"figcaption",{},"The Protocol also narrows the mock to what the code actually uses — a few methods out of the SDK's hundreds — which makes it easier to read and harder to misuse.",[36,381,383],{"id":382},"why-this-works","Why this works",[10,385,386,388,389,392,393,396,397,399],{},[13,387,15],{}," walks the specification object with ",[13,390,391],{},"dir()"," and ",[13,394,395],{},"inspect.signature",", creating a child mock for each attribute it finds and binding each callable child to the real signature. A class whose methods come from ",[13,398,19],{}," has no such attributes to find, so autospec produces a mock with no constraints at all — it cannot enforce a name it never saw.",[10,401,402,403,405,406,409,410,413,414,417,418,421],{},"A ",[13,404,30],{}," is an ordinary class whose methods are declared with real signatures, so autospec finds them exactly as it would on a concrete class. The mock then rejects unknown attribute names with ",[13,407,408],{},"AttributeError"," and wrong arguments with ",[13,411,412],{},"TypeError",", which restores every guarantee autospec normally gives. The same Protocol serves as a type annotation in the production code, so ",[13,415,416],{},"mypy"," or ",[13,419,420],{},"pyright"," checks the real call sites against the same declaration the tests use. That double use is what makes the Protocol worth writing: one declaration constrains both the tests and the code, so the two cannot disagree about what the dependency looks like without a type error or a test failure pointing it out.",[36,423,425],{"id":424},"edge-cases-and-failure-modes","Edge cases and failure modes",[41,427,428,435,445,455,472],{},[44,429,430,434],{},[431,432,433],"strong",{},"Protocol drifting from the SDK."," If the SDK renames an argument and the Protocol does not follow, tests pass against the old signature. Verify the real client against the Protocol in an integration test.",[44,436,437,440,441,444],{},[431,438,439],{},"Keyword-only parameters."," Many SDKs take only keyword arguments. Declare them after ",[13,442,443],{},"*"," in the Protocol so positional calls fail as they would against the real API.",[44,446,447,450,451,454],{},[431,448,449],{},"Return shapes."," The Protocol's return annotation is not enforced by autospec. Configure ",[13,452,453],{},"return_value"," with a realistic payload, ideally built from a recorded response.",[44,456,457,464,465,468,469,471],{},[431,458,459,460,463],{},"Using ",[13,461,462],{},"spec="," instead of autospec."," ",[13,466,467],{},"Mock(spec=ObjectStore)"," restricts attribute names but not arguments. Use ",[13,470,15],{}," for both.",[44,473,474,477],{},[431,475,476],{},"Too broad a Protocol."," Copying the SDK's whole surface into a Protocol recreates the problem of a huge double. Declare only what the code calls.",[36,479,481],{"id":480},"an-adapter-as-the-better-boundary","An adapter as the better boundary",[10,483,484],{},"The Protocol approach keeps the SDK in the production code and narrows the test double. An adapter goes one step further and narrows the production dependency too: a small class that exposes the operations the service needs, in its own vocabulary, and hides the SDK entirely.",[75,486,488],{"className":77,"code":487,"language":79,"meta":80,"style":80},"class InvoiceStorage:\n    \"\"\"Everything the billing service needs from object storage.\"\"\"\n\n    def __init__(self, client, bucket: str) -> None:\n        self._client, self._bucket = client, bucket\n\n    def save_pdf(self, invoice_id: str, pdf: bytes) -> None:\n        self._client.put_object(Bucket=self._bucket, Key=f\"{invoice_id}.pdf\",\n                                Body=pdf, ContentType=\"application\u002Fpdf\")\n\n    def load_pdf(self, invoice_id: str) -> bytes:\n        response = self._client.get_object(Bucket=self._bucket, Key=f\"{invoice_id}.pdf\")\n        return response[\"Body\"].read()\n",[13,489,490,495,500,504,509,514,518,523,528,533,537,542,547],{"__ignoreMap":80},[84,491,492],{"class":86,"line":87},[84,493,494],{},"class InvoiceStorage:\n",[84,496,497],{"class":86,"line":93},[84,498,499],{},"    \"\"\"Everything the billing service needs from object storage.\"\"\"\n",[84,501,502],{"class":86,"line":100},[84,503,97],{"emptyLinePlaceholder":96},[84,505,506],{"class":86,"line":106},[84,507,508],{},"    def __init__(self, client, bucket: str) -> None:\n",[84,510,511],{"class":86,"line":138},[84,512,513],{},"        self._client, self._bucket = client, bucket\n",[84,515,516],{"class":86,"line":144},[84,517,97],{"emptyLinePlaceholder":96},[84,519,520],{"class":86,"line":150},[84,521,522],{},"    def save_pdf(self, invoice_id: str, pdf: bytes) -> None:\n",[84,524,525],{"class":86,"line":155},[84,526,527],{},"        self._client.put_object(Bucket=self._bucket, Key=f\"{invoice_id}.pdf\",\n",[84,529,530],{"class":86,"line":161},[84,531,532],{},"                                Body=pdf, ContentType=\"application\u002Fpdf\")\n",[84,534,535],{"class":86,"line":167},[84,536,97],{"emptyLinePlaceholder":96},[84,538,539],{"class":86,"line":172},[84,540,541],{},"    def load_pdf(self, invoice_id: str) -> bytes:\n",[84,543,544],{"class":86,"line":178},[84,545,546],{},"        response = self._client.get_object(Bucket=self._bucket, Key=f\"{invoice_id}.pdf\")\n",[84,548,549],{"class":86,"line":183},[84,550,551],{},"        return response[\"Body\"].read()\n",[10,553,554,555,558,559,562,563,566],{},"Business-logic tests now mock ",[13,556,557],{},"InvoiceStorage"," — two methods with plain signatures, fully autospecced — and never mention S3 at all. The adapter itself gets a handful of integration tests against a real or emulated bucket, which is where the SDK's behaviour genuinely needs checking. An SDK upgrade that changes argument names touches one file and its tests, instead of every test that mocked the client. The adapter's method names also describe the domain rather than the vendor — ",[13,560,561],{},"save_pdf"," rather than ",[13,564,565],{},"put_object"," — which makes the business-logic tests read as statements about invoices instead of statements about a storage API, and makes a later change of storage provider a matter of writing a second adapter rather than touching the service.",[10,568,569,570,573,574,63],{},"The adapter is also where a fake belongs, if one is wanted: an ",[13,571,572],{},"InMemoryInvoiceStorage"," implementing the same two methods over a dictionary is a few lines, needs no mocking library, and can share a contract suite with the real adapter — the approach described in ",[59,575,577],{"href":576},"\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",[272,579,581,700],{"className":580},[275],[277,582,285,587,285,590,285,593,285,610,285,614,285,619,285,627,285,633,285,637,285,641,285,645,285,649,285,653,285,657,285,661,285,664,285,670,285,675,285,679,285,683,285,689,285,693,285,696],{"viewBox":583,"role":280,"ariaLabelledBy":584,"xmlns":284},"0 0 800 244",[585,586],"ad-t","ad-d",[287,588,589],{"id":585},"Where the mock boundary sits with and without an adapter",[291,591,592],{"id":586},"Without an adapter, the business logic calls the SDK client directly and every test must mock the SDK's dynamic surface. With an adapter, the business logic calls two plain methods on an InvoiceStorage class, tests mock or fake that small class, and only the adapter's own integration tests touch the SDK.",[594,595,596,597,285],"defs",{},"\n    ",[598,599,606],"marker",{"id":600,"viewBox":601,"refX":602,"refY":603,"markerWidth":604,"markerHeight":604,"orient":605},"ad-a","0 0 10 10","9","5","7","auto-start-reverse",[607,608],"path",{"d":609,"fill":310},"M0 0 L10 5 L0 10 z",[295,611],{"x":297,"y":297,"width":612,"height":613,"rx":300,"fill":301},"800","244",[303,615,618],{"x":616,"y":306,"textAnchor":307,"fontSize":617,"fontWeight":309,"fill":310},"400","15.5","Mock the boundary you own",[295,620],{"x":314,"y":621,"width":622,"height":623,"rx":624,"fill":625,"stroke":310,"strokeWidth":626},"56","180","60","10","#f4f1de","1.6",[303,628,632],{"x":629,"y":630,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"116","91","11.5","billing service",[86,634],{"x1":324,"y1":635,"x2":299,"y2":635,"stroke":310,"strokeWidth":626,"markerEnd":636},"86","url(#ad-a)",[295,638],{"x":639,"y":621,"width":640,"height":623,"rx":624,"fill":353,"stroke":354,"strokeWidth":321},"268","240",[303,642,557],{"x":643,"y":644,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"388","82",[303,646,648],{"x":643,"y":647,"textAnchor":307,"fontSize":332,"fill":368},"102","save_pdf · load_pdf",[86,650],{"x1":651,"y1":635,"x2":652,"y2":635,"stroke":310,"strokeWidth":626,"markerEnd":636},"512","564",[295,654],{"x":655,"y":621,"width":656,"height":623,"rx":624,"fill":319,"stroke":320,"strokeWidth":321},"570","204",[303,658,660],{"x":659,"y":644,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"672","boto3 client",[303,662,663],{"x":659,"y":647,"textAnchor":307,"fontSize":332,"fill":341},"dynamic surface",[295,665],{"x":314,"y":666,"width":667,"height":668,"rx":332,"fill":353,"stroke":354,"strokeWidth":669},"140","482","80","1.8",[303,671,674],{"x":672,"y":673,"textAnchor":307,"fontSize":318,"fontWeight":309,"fill":310},"267","166","business-logic tests",[303,676,678],{"x":672,"y":677,"textAnchor":307,"fontSize":332,"fill":310},"188","autospec or fake InvoiceStorage — two plain methods",[303,680,682],{"x":672,"y":681,"textAnchor":307,"fontSize":332,"fill":368},"206","never mention S3",[295,684],{"x":685,"y":666,"width":686,"height":668,"rx":332,"fill":687,"stroke":688,"strokeWidth":669},"524","250","#f7f0da","#f2cc8f",[303,690,692],{"x":691,"y":673,"textAnchor":307,"fontSize":318,"fontWeight":309,"fill":310},"649","adapter tests",[303,694,695],{"x":691,"y":677,"textAnchor":307,"fontSize":332,"fill":310},"real or emulated bucket",[303,697,699],{"x":691,"y":681,"textAnchor":307,"fontSize":332,"fill":698},"#8a5a00","the only SDK contact",[377,701,702],{},"An SDK upgrade now touches the adapter and its few tests, not every test in the service.",[36,704,706],{"id":705},"keeping-the-protocol-honest","Keeping the Protocol honest",[10,708,709],{},"A Protocol written by hand is a claim about the SDK, and like any claim it can go stale. The SDK adds a required argument, renames a keyword, changes a return shape — and every test autospecced against the old Protocol keeps passing. The defence is a single integration test that checks the real client against the declaration.",[10,711,712,713,716,717,719],{},"Two checks cover most of the risk. The first is structural: for each method declared in the Protocol, confirm the real client has it and that its accepted parameters include every name the Protocol declares. Dynamic clients often expose their operations through a service model — boto3's ",[13,714,715],{},"client.meta.service_model"," lists every operation and its input shape — which makes this a straightforward comparison rather than a guess. Clients generated from OpenAPI or protobuf definitions usually expose an equivalent description, and where none exists, ",[13,718,395],{}," on a bound method of a real instance often works even when the class itself shows nothing. The second is behavioural: call each method once against a real or emulated service with the arguments the production code uses, and check the response has the fields the code reads.",[10,721,722],{},"Running that test in the integration stage, and whenever the SDK version changes, turns the Protocol from documentation into a verified contract. It fails exactly when the SDK changes in a way the rest of the suite would otherwise not notice, and it names the method and parameter that drifted, which makes the fix a one-line edit to the Protocol followed by whatever changes the production code needs.",[272,724,726,807],{"className":725},[275],[277,727,285,732,285,735,285,738,285,745,285,748,285,751,285,756,285,760,285,764,285,770,285,773,285,777,285,782,285,786,285,788,285,792,285,796,285,800,285,804],{"viewBox":728,"role":280,"ariaLabelledBy":729,"xmlns":284},"0 0 800 236",[730,731],"ph-t","ph-d",[287,733,734],{"id":730},"Verifying the Protocol against the real SDK",[291,736,737],{"id":731},"A Protocol declares the methods and parameters the code depends on. An integration test compares it against the real client in two ways: structurally, by checking the service model lists each operation with the declared parameters, and behaviourally, by calling each method against a real or emulated service and checking the response fields. A mismatch fails the test and names the drifted method.",[594,739,596,740,285],{},[598,741,743],{"id":742,"viewBox":601,"refX":602,"refY":603,"markerWidth":604,"markerHeight":604,"orient":605},"ph-a",[607,744],{"d":609,"fill":354},[295,746],{"x":297,"y":297,"width":612,"height":747,"rx":300,"fill":301},"236",[303,749,750],{"x":616,"y":306,"textAnchor":307,"fontSize":617,"fontWeight":309,"fill":310},"The Protocol is a claim; one test verifies it",[295,752],{"x":314,"y":753,"width":754,"height":755,"rx":332,"fill":687,"stroke":688,"strokeWidth":321},"88","200","70",[303,757,759],{"x":758,"y":629,"textAnchor":307,"fontSize":318,"fontWeight":309,"fill":310},"126","ObjectStore",[303,761,763],{"x":758,"y":762,"textAnchor":307,"fontSize":332,"fill":698},"138","declared methods",[86,765],{"x1":766,"y1":767,"x2":768,"y2":668,"stroke":354,"strokeWidth":669,"markerEnd":769},"230","110","306","url(#ph-a)",[86,771],{"x1":766,"y1":772,"x2":768,"y2":673,"stroke":354,"strokeWidth":669,"markerEnd":769},"136",[295,774],{"x":775,"y":776,"width":640,"height":623,"rx":624,"fill":353,"stroke":354,"strokeWidth":321},"312","50",[303,778,781],{"x":779,"y":780,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"432","76","structural check",[303,783,785],{"x":779,"y":784,"textAnchor":307,"fontSize":332,"fill":310},"96","service model lists the params",[295,787],{"x":775,"y":772,"width":640,"height":623,"rx":624,"fill":353,"stroke":354,"strokeWidth":321},[303,789,791],{"x":779,"y":790,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"162","behavioural check",[303,793,795],{"x":779,"y":794,"textAnchor":307,"fontSize":332,"fill":310},"182","real call, expected fields back",[295,797],{"x":798,"y":753,"width":799,"height":755,"rx":332,"fill":319,"stroke":320,"strokeWidth":321},"580","194",[303,801,803],{"x":802,"y":629,"textAnchor":307,"fontSize":631,"fontWeight":309,"fill":310},"677","mismatch",[303,805,806],{"x":802,"y":762,"textAnchor":307,"fontSize":332,"fill":341},"names the drifted method",[377,808,809],{},"Without this test, the Protocol ages silently. With it, an SDK upgrade either passes cleanly or produces a precise list of what changed.",[36,811,813],{"id":812},"frequently-asked-questions","Frequently Asked Questions",[10,815,816,819,820,822],{},[431,817,818],{},"Why does create_autospec accept any method on my SDK client?","\nBecause the client's methods do not exist as attributes on the class; they are produced at runtime by ",[13,821,19],{}," or built from a service description. autospec inspects the class, finds only the dynamic hook, and has nothing to restrict against, so it falls back to accepting everything.",[10,824,825,828,829,831],{},[431,826,827],{},"What is the cleanest way to get a strict mock of such a client?","\nDefine a ",[13,830,30],{}," listing the methods your code actually calls, with their signatures, and autospec that. The Protocol documents your real dependency, gives the type checker something to check, and gives autospec concrete methods to enforce.",[10,833,834,837],{},[431,835,836],{},"Should I wrap third-party SDK clients in my own adapter?","\nUsually, yes. A thin adapter exposing the handful of operations your code needs gives a stable, strictly-typed boundary to mock, isolates SDK changes to one module, and makes the test doubles far simpler than doubling the full SDK surface.",[36,839,841],{"id":840},"related","Related",[41,843,844,850,857,864],{},[44,845,846,849],{},[59,847,848],{"href":61},"Autospec & Strict Mocking"," — what autospec enforces when it can see the methods.",[44,851,852,856],{},[59,853,855],{"href":854},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fcatching-signature-drift-with-spec-set\u002F","Catching Signature Drift with spec_set"," — the attribute-assignment half of strictness.",[44,858,859,863],{},[59,860,862],{"href":861},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","Dependency Injection for Testability"," — passing the adapter in rather than constructing the SDK inside.",[44,865,866,870],{},[59,867,869],{"href":868},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fmocking-network-and-http-calls\u002Frecording-and-replaying-http-with-vcrpy\u002F","Recording and Replaying HTTP with VCR.py"," — realistic return payloads for the adapter's integration tests.",[10,872,873,874],{},"← Back to ",[59,875,848],{"href":61},[877,878,879],"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":80,"searchDepth":93,"depth":93,"links":881},[882,883,884,885,886,887,888,889],{"id":38,"depth":93,"text":39},{"id":69,"depth":93,"text":70},{"id":382,"depth":93,"text":383},{"id":424,"depth":93,"text":425},{"id":480,"depth":93,"text":481},{"id":705,"depth":93,"text":706},{"id":812,"depth":93,"text":813},{"id":840,"depth":93,"text":841},"Get strict mocks for dynamic SDK clients whose methods are generated through __getattr__: why autospec sees nothing, explicit specs, Protocols, and wrapper adapters.","md",{"slug":893,"type":894,"breadcrumb":895,"datePublished":896,"dateModified":896,"faq":897,"howto":904},"autospeccing-clients-that-use-getattr","article","Dynamic Clients","2026-09-18",[898,900,902],{"q":818,"a":899},"Because the client's methods do not exist as attributes on the class; they are produced at runtime by __getattr__ or built from a service description. autospec inspects the class, finds only the dynamic hook, and has nothing to restrict against, so it falls back to accepting everything.",{"q":827,"a":901},"Define a Protocol listing the methods your code actually calls, with their signatures, and autospec that. The Protocol documents your real dependency, gives the type checker something to check, and gives autospec concrete methods to enforce.",{"q":836,"a":903},"Usually, yes. A thin adapter exposing the handful of operations your code needs gives a stable, strictly-typed boundary to mock, isolates SDK changes to one module, and makes the test doubles far simpler than doubling the full SDK surface.",{"name":905,"description":906,"steps":907},"How to get strict mocks for dynamic clients","Recognise when autospec has nothing to inspect, then give it something concrete — a Protocol or an adapter — so signatures are enforced again.",[908,911,914,917,920],{"name":909,"text":910},"Check what autospec actually sees","Call dir() on the client class and confirm whether the methods you use exist as real attributes.",{"name":912,"text":913},"Declare the dependency as a Protocol","List the methods your code calls, with their real signatures, in a typing.Protocol.",{"name":915,"text":916},"Autospec the Protocol","Use create_autospec(MyClientProtocol, instance=True) so misspelt methods and wrong arguments fail.",{"name":918,"text":919},"Or introduce an adapter","Wrap the SDK in a small class exposing only the needed operations and mock that instead.",{"name":921,"text":922},"Verify the Protocol against the real client","Add an integration check that the real client satisfies the Protocol so the spec cannot drift.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fautospeccing-clients-that-use-getattr",{"title":5,"description":890},"advanced-mocking-test-doubles-in-python\u002Fautospec-strict-mocking\u002Fautospeccing-clients-that-use-getattr\u002Findex","Q34BfHrz-pDD9jevB8JnAYC_uLp8SBMIKQgBRmAcAIs",1789718767582]