11"""Decoy test double stubbing and verification library."""
22from os import linesep
33from typing import cast , Any , Optional , Sequence , Type
4+ from warnings import warn
45
56from .registry import Registry
67from .spy import create_spy , SpyCall
78from .stub import Stub
89from .types import ClassT , FuncT , ReturnT
10+ from .warnings import MissingStubWarning
911
1012
1113class Decoy :
1214 """Decoy test double state container."""
1315
1416 _registry : Registry
17+ _warn_on_missing_stubs : bool
18+ _next_call_is_when_rehearsal : bool
1519
16- def __init__ (self ) -> None :
20+ def __init__ (
21+ self ,
22+ warn_on_missing_stubs : bool = True ,
23+ ) -> None :
1724 """Initialize the state container for test doubles and stubs.
1825
1926 You should initialize a new Decoy instance for every test.
2027
28+ Arguments:
29+ warn_on_missing_stubs: Trigger a warning if a stub is called
30+ with arguments that do not match any of its rehearsals.
31+
2132 Example:
2233 ```python
2334 import pytest
@@ -29,6 +40,21 @@ def decoy() -> Decoy:
2940 ```
3041 """
3142 self ._registry = Registry ()
43+ self ._warn_on_missing_stubs = warn_on_missing_stubs
44+ self ._next_call_is_when_rehearsal = False
45+
46+ def __getattribute__ (self , name : str ) -> Any :
47+ """Proxy to catch calls to `when` and mark the subsequent spy call as a rehearsal.
48+
49+ This is to ensure that rehearsal calls don't accidentally trigger a
50+ `MissingStubWarning`.
51+ """
52+ actual_method = super ().__getattribute__ (name )
53+
54+ if name == "when" :
55+ self ._next_call_is_when_rehearsal = True
56+
57+ return actual_method
3258
3359 def create_decoy (self , spec : Type [ClassT ], * , is_async : bool = False ) -> ClassT :
3460 """Create a class decoy for `spec`.
@@ -103,10 +129,10 @@ def when(self, _rehearsal_result: ReturnT) -> Stub[ReturnT]:
103129 ```
104130
105131 Note:
106- The "rehearsal" is an actual call to the test fake. The fact that
107- the call is written inside `when` is purely for typechecking and
108- API sugar. Decoy will pop the last call to _any_ fake off its
109- call stack, which will end up being the call inside `when` .
132+ The "rehearsal" is an actual call to the test fake. Because the
133+ call is written inside `when`, Decoy is able to infer that the call
134+ is a rehearsal for stub configuration purposes rather than a call
135+ from the code-under-test .
110136 """
111137 rehearsal = self ._pop_last_rehearsal ()
112138 stub = Stub [ReturnT ](rehearsal = rehearsal )
@@ -173,11 +199,16 @@ def _handle_spy_call(self, call: SpyCall) -> Any:
173199 self ._registry .register_call (call )
174200
175201 stubs = self ._registry .get_stubs_by_spy_id (call .spy_id )
202+ is_when_rehearsal = self ._next_call_is_when_rehearsal
203+ self ._next_call_is_when_rehearsal = False
176204
177205 for stub in reversed (stubs ):
178206 if stub ._rehearsal == call :
179207 return stub ._act ()
180208
209+ if not is_when_rehearsal and self ._warn_on_missing_stubs and len (stubs ) > 0 :
210+ warn (MissingStubWarning (call , stubs ))
211+
181212 return None
182213
183214 def _build_verify_error (
0 commit comments