11"""Smoke and acceptance tests for main Decoy interface."""
22
33import inspect
4+ import sys
5+ from typing import Any
46
57import pytest
68
79from decoy import Decoy , errors
810from decoy .spy import AsyncSpy , Spy
911from decoy .warnings import IncorrectCallWarning
1012
11- from .fixtures import SomeClass , some_func
13+ from .fixtures import (
14+ SomeAsyncCallableClass ,
15+ SomeAsyncClass ,
16+ SomeCallableClass ,
17+ SomeClass ,
18+ SomeNestedClass ,
19+ noop ,
20+ some_async_func ,
21+ some_func ,
22+ )
1223
1324
1425def test_create_mock (decoy : Decoy ) -> None :
15- """It creates a mock from a class."""
26+ """It creates a mock from a class spec ."""
1627 subject = decoy .mock (cls = SomeClass )
1728
1829 assert isinstance (subject , SomeClass )
1930 assert isinstance (subject , Spy )
2031 assert repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass`>"
2132
2233
34+ def test_create_method_mock (decoy : Decoy ) -> None :
35+ """It creates a child mock from a class spec."""
36+ subject = decoy .mock (cls = SomeClass ).foo
37+
38+ def _expected_signature (val : str ) -> str :
39+ raise NotImplementedError ()
40+
41+ assert isinstance (subject , Spy )
42+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
43+ assert repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass.foo`>"
44+
45+
2346def test_method_noop (decoy : Decoy ) -> None :
2447 """A method mock no-ops by default."""
2548 subject = decoy .mock (cls = SomeClass )
@@ -28,8 +51,126 @@ def test_method_noop(decoy: Decoy) -> None:
2851 assert result is None
2952
3053
31- def test_decoy_creates_func_spy (decoy : Decoy ) -> None :
32- """It should be able to create a Spy from a function."""
54+ def test_create_staticmethod_mock (decoy : Decoy ) -> None :
55+ """It creates a child staticmethod mock from a class spec."""
56+ subject = decoy .mock (cls = SomeClass ).static_method
57+
58+ def _expected_signature (hello : str ) -> int :
59+ raise NotImplementedError ()
60+
61+ assert isinstance (subject , Spy )
62+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
63+ assert repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass.static_method`>"
64+
65+
66+ def test_create_classmethod_mock (decoy : Decoy ) -> None :
67+ """It creates a child classmethod mock from a class spec."""
68+ subject = decoy .mock (cls = SomeClass ).class_method
69+
70+ def _expected_signature (hello : str ) -> int :
71+ raise NotImplementedError ()
72+
73+ assert isinstance (subject , Spy )
74+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
75+ assert repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass.class_method`>"
76+
77+
78+ def test_create_decorated_method_mock (decoy : Decoy ) -> None :
79+ """It creates a child mock of a decorated method from a class spec."""
80+ subject = decoy .mock (cls = SomeClass ).some_wrapped_method
81+
82+ def _expected_signature (val : str ) -> str :
83+ raise NotImplementedError ()
84+
85+ assert isinstance (subject , Spy )
86+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
87+ assert (
88+ repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass.some_wrapped_method`>"
89+ )
90+
91+
92+ def test_create_attribute_mock (decoy : Decoy ) -> None :
93+ """It creates a child mock of an attribute."""
94+ subject = decoy .mock (cls = SomeClass ).some_attr
95+
96+ expected_signature = (
97+ inspect .signature (bool )
98+ if sys .version_info >= (3 , 13 )
99+ else inspect .signature (noop )
100+ )
101+
102+ assert isinstance (subject , Spy )
103+ assert inspect .signature (subject ) == expected_signature
104+ assert repr (subject ) == "<Decoy mock `tests.fixtures.SomeClass.some_attr`>"
105+
106+
107+ def test_create_attribute_class_mock (decoy : Decoy ) -> None :
108+ """It creates a child class mock from an attribute."""
109+ subject = decoy .mock (cls = SomeNestedClass ).child_attr
110+
111+ assert isinstance (subject , Spy )
112+ assert isinstance (subject , SomeClass )
113+
114+
115+ def test_create_property_class_mock (decoy : Decoy ) -> None :
116+ """It creates a child class mock from an property getter."""
117+ subject = decoy .mock (cls = SomeNestedClass ).child
118+
119+ assert isinstance (subject , Spy )
120+ assert isinstance (subject , SomeClass )
121+
122+
123+ def test_create_optional_property_class_mock (decoy : Decoy ) -> None :
124+ """It creates a child class mock from an property getter with Optional return."""
125+ subject = decoy .mock (cls = SomeNestedClass ).optional_child
126+ assert isinstance (subject , Spy )
127+ assert isinstance (subject , SomeClass )
128+
129+ subject = decoy .mock (cls = SomeNestedClass ).union_child_and_none
130+ assert isinstance (subject , Spy )
131+ assert isinstance (subject , SomeClass )
132+
133+ subject = decoy .mock (cls = SomeNestedClass ).union_none_and_child
134+ assert isinstance (subject , Spy )
135+ assert isinstance (subject , SomeClass )
136+
137+
138+ def test_create_union_class_mock (decoy : Decoy ) -> None :
139+ """A child class mock from an property with union return is not typed."""
140+ subject = decoy .mock (cls = SomeNestedClass ).union_child
141+
142+ assert isinstance (subject , Spy )
143+ assert not isinstance (subject , SomeClass )
144+ assert not isinstance (subject , SomeAsyncClass )
145+
146+
147+ def test_create_callable_mock (decoy : Decoy ) -> None :
148+ """It creates a mock from a callable class."""
149+ subject = decoy .mock (cls = SomeCallableClass )
150+
151+ def _expected_signature (val : int ) -> int :
152+ raise NotImplementedError ()
153+
154+ assert isinstance (subject , Spy )
155+ assert isinstance (subject , SomeCallableClass )
156+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
157+
158+
159+ def test_create_async_callable_mock (decoy : Decoy ) -> None :
160+ """It creates a mock from an async callable class."""
161+ subject = decoy .mock (cls = SomeAsyncCallableClass )
162+
163+ async def _expected_signature (val : int ) -> int :
164+ raise NotImplementedError ()
165+
166+ assert isinstance (subject , AsyncSpy )
167+ assert isinstance (subject , SomeAsyncCallableClass )
168+ assert inspect .signature (subject ) == inspect .signature (_expected_signature )
169+ assert inspect .iscoroutinefunction (subject .__call__ )
170+
171+
172+ def test_create_func_mock (decoy : Decoy ) -> None :
173+ """It creates a mock from a function spec."""
33174 subject = decoy .mock (func = some_func )
34175
35176 assert isinstance (subject , Spy )
@@ -45,6 +186,23 @@ def test_func_noop(decoy: Decoy) -> None:
45186 assert result is None
46187
47188
189+ def test_create_async_func_mock (decoy : Decoy ) -> None :
190+ """It creates a mock from an async function spec."""
191+ subject = decoy .mock (func = some_async_func )
192+
193+ assert isinstance (subject , AsyncSpy )
194+ assert inspect .signature (subject ) == inspect .signature (some_async_func )
195+ assert repr (subject ) == "<Decoy mock `tests.fixtures.some_async_func`>"
196+
197+
198+ async def test_async_func_noop (decoy : Decoy ) -> None :
199+ """An async function mock no-ops by default."""
200+ subject = decoy .mock (func = some_async_func )
201+ result = await subject ("hello" )
202+
203+ assert result is None
204+
205+
48206def test_func_bad_call (decoy : Decoy ) -> None :
49207 """It raises an IncorrectCallWarning if call is bad."""
50208 subject = decoy .mock (func = some_func )
@@ -53,22 +211,43 @@ def test_func_bad_call(decoy: Decoy) -> None:
53211 subject ("hello" , "world" ) # type: ignore[call-arg]
54212
55213
56- def test_decoy_creates_specless_spy (decoy : Decoy ) -> None :
57- """It should be able to create a spec-less spy ."""
214+ def test_create_specless_mock (decoy : Decoy ) -> None :
215+ """It creates a mock without a spec."""
58216 subject = decoy .mock (name = "subject" )
59217
60218 assert isinstance (subject , Spy )
61219 assert repr (subject ) == "<Decoy mock `subject`>"
62220
63221
64- def test_decoy_creates_specless_async_spy (decoy : Decoy ) -> None :
65- """It should be able to create an async specless spy ."""
222+ def test_create_specless_async_mock (decoy : Decoy ) -> None :
223+ """It creates an async mock without a spec ."""
66224 subject = decoy .mock (name = "subject" , is_async = True )
67225
68226 assert isinstance (subject , AsyncSpy )
227+ assert inspect .iscoroutinefunction (subject .__call__ )
69228
70229
71- def test_decoy_mock_name_required (decoy : Decoy ) -> None :
72- """A name should be required for the mock."""
230+ async def test_async_specless_mock_noop (decoy : Decoy ) -> None :
231+ """An async specless mock no-ops by default."""
232+ subject = decoy .mock (name = "subject" , is_async = True )
233+ result = await subject ("hello" )
234+
235+ assert result is None
236+
237+
238+ def test_mock_name_required (decoy : Decoy ) -> None :
239+ """A name is required for a mock without a spec."""
73240 with pytest .raises (errors .MockNameRequiredError ):
74241 decoy .mock () # type: ignore[call-overload]
242+
243+
244+ @pytest .mark .filterwarnings ("ignore:'NoneType' object is not subscriptable" )
245+ def test_bad_type_hints (decoy : Decoy ) -> None :
246+ """It tolerates bad type hints without failing at runtime."""
247+
248+ class _BadTypeHints :
249+ not_ok : "None[Any]" # pyright: ignore[reportInvalidTypeArguments]
250+
251+ subject = decoy .mock (cls = _BadTypeHints ).not_ok
252+
253+ assert isinstance (subject , Spy )
0 commit comments