Skip to content

Commit d3e0ae9

Browse files
authored
feat: add mock method to replace create_decoy, create_decoy_func (#37)
1 parent 633159f commit d3e0ae9

File tree

12 files changed

+165
-77
lines changed

12 files changed

+165
-77
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ def test_add_todo(decoy: Decoy) -> None:
8383

8484
### Create a mock
8585

86-
Use `decoy.create_decoy` to create a mock based on some specification. From there, inject the mock into your test subject.
86+
Use `decoy.mock` to create a mock based on some specification. From there, inject the mock into your test subject.
8787

8888
```python
8989
def test_add_todo(decoy: Decoy) -> None:
90-
todo_store = decoy.create_decoy(spec=TodoStore)
90+
todo_store = decoy.mock(cls=TodoStore)
9191
subject = TodoAPI(store=todo_store)
9292
...
9393
```
@@ -101,7 +101,7 @@ Use `decoy.when` to configure your mock's behaviors. For example, you can set th
101101
```python
102102
def test_add_todo(decoy: Decoy) -> None:
103103
"""Adding a todo should create a TodoItem in the TodoStore."""
104-
todo_store = decoy.create_decoy(spec=TodoStore)
104+
todo_store = decoy.mock(cls=TodoStore)
105105
subject = TodoAPI(store=todo_store)
106106

107107
decoy.when(
@@ -123,7 +123,7 @@ Use `decoy.verify` to assert that a mock was called in a certain way. This is be
123123
```python
124124
def test_remove_todo(decoy: Decoy) -> None:
125125
"""Removing a todo should remove the item from the TodoStore."""
126-
todo_store = decoy.create_decoy(spec=TodoStore)
126+
todo_store = decoy.mock(cls=TodoStore)
127127
subject = TodoAPI(store=todo_store)
128128

129129
subject.remove("abc123")

decoy/__init__.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Decoy stubbing and spying library."""
22
from __future__ import annotations
3-
from typing import cast, Any, Callable, Generic, Optional
3+
from typing import Any, Callable, Generic, Optional, cast, overload
44

55
from . import matchers, errors, warnings
66
from .core import DecoyCore, StubCore
@@ -18,32 +18,58 @@ def __init__(self) -> None:
1818
"""
1919
self._core = DecoyCore()
2020

21-
def create_decoy(
21+
@overload
22+
def mock(self, *, cls: Callable[..., ClassT]) -> ClassT:
23+
...
24+
25+
@overload
26+
def mock(self, *, func: FuncT) -> FuncT:
27+
...
28+
29+
@overload
30+
def mock(self, *, is_async: bool = False) -> Any:
31+
...
32+
33+
def mock(
2234
self,
23-
spec: Callable[..., ClassT],
2435
*,
36+
cls: Optional[Any] = None,
37+
func: Optional[Any] = None,
2538
is_async: bool = False,
26-
) -> ClassT:
27-
"""Create a class decoy for `spec`.
39+
) -> Any:
40+
"""Create a mock.
2841
29-
See [decoy creation usage guide](../usage/create) for more details.
42+
See the [mock creation guide](../usage/create) for more details.
3043
3144
Arguments:
32-
spec: A class definition that the decoy should mirror.
33-
is_async: Force the returned spy to be asynchronous. In most cases,
34-
this argument is unnecessary, since the Spy will use `spec` to
35-
determine if a method should be asynchronous.
45+
cls: A class definition that the mock should imitate.
46+
func: A function definition the mock should imitate.
47+
is_async: Force the returned spy to be asynchronous. This argument
48+
only applies if you don't use `cls` nor `func`.
3649
3750
Returns:
38-
A spy typecast as an instance of `spec`.
51+
A spy typecast as the object it's imitating, if any.
3952
4053
Example:
4154
```python
4255
def test_get_something(decoy: Decoy):
43-
db = decoy.create_decoy(spec=Database)
56+
db = decoy.mock(cls=Database)
4457
# ...
4558
```
59+
"""
60+
spec = cls or func
61+
return self._core.mock(spec=spec, is_async=is_async)
62+
63+
def create_decoy(
64+
self,
65+
spec: Callable[..., ClassT],
66+
*,
67+
is_async: bool = False,
68+
) -> ClassT:
69+
"""Create a class mock for `spec`.
4670
71+
!!! warning "Deprecated since v1.6.0"
72+
Use [decoy.Decoy.mock][] with the `cls` parameter, instead.
4773
"""
4874
spy = self._core.mock(spec=spec, is_async=is_async)
4975
return cast(ClassT, spy)
@@ -54,25 +80,10 @@ def create_decoy_func(
5480
*,
5581
is_async: bool = False,
5682
) -> FuncT:
57-
"""Create a function decoy for `spec`.
58-
59-
See [decoy creation usage guide](../usage/create) for more details.
60-
61-
Arguments:
62-
spec: A function that the decoy should mirror.
63-
is_async: Force the returned spy to be asynchronous. In most cases,
64-
this argument is unnecessary, since the Spy will use `spec` to
65-
determine if the function should be asynchronous.
66-
67-
Returns:
68-
A spy typecast as `spec` function.
83+
"""Create a function mock for `spec`.
6984
70-
Example:
71-
```python
72-
def test_create_something(decoy: Decoy):
73-
gen_id = decoy.create_decoy_func(spec=generate_unique_id)
74-
# ...
75-
```
85+
!!! warning "Deprecated since v1.6.0"
86+
Use [decoy.Decoy.mock][] with the `func` parameter, instead.
7687
"""
7788
spy = self._core.mock(spec=spec, is_async=is_async)
7889
return cast(FuncT, spy)
@@ -90,7 +101,7 @@ def when(self, _rehearsal_result: ReturnT) -> Stub[ReturnT]:
90101
91102
Example:
92103
```python
93-
db = decoy.create_decoy(spec=Database)
104+
db = decoy.mock(cls=Database)
94105
decoy.when(db.exists("some-id")).then_return(True)
95106
```
96107
@@ -119,7 +130,7 @@ def verify(self, *_rehearsal_results: Any, times: Optional[int] = None) -> None:
119130
Example:
120131
```python
121132
def test_create_something(decoy: Decoy):
122-
gen_id = decoy.create_decoy_func(spec=generate_unique_id)
133+
gen_id = decoy.mock(func=generate_unique_id)
123134
124135
# ...
125136

docs/usage/create.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,36 @@ Decoy can create two kinds of mocks:
55
- Mocks of a class instance
66
- Mocks of a function
77

8-
## Mocking a class
8+
Mocks are created using the [decoy.Decoy.mock][] method.
99

10-
To create a mock class instance, use [decoy.Decoy.create_decoy][]. Decoy will inspect type annotations and method signatures to automatically configure methods as synchronous or asynchronous. Decoy mocks are automatically deep.
10+
## Mocking a class
1111

12-
To type checkers, the mock will appear to have the exact same type as the `spec` argument. The mock will also pass `isinstance` checks.
12+
To mock a class instance, pass the `cls` argument to `decoy.mock`. Decoy will inspect type annotations and method signatures to automatically configure methods as synchronous or asynchronous. Decoy mocks are automatically deep.
1313

1414
```python
1515
def test_my_thing(decoy: Decoy) -> None:
16-
some_dependency = decoy.create_decoy(spec=SomeDependency)
16+
some_dependency = decoy.mock(cls=SomeDependency)
1717
```
1818

19+
To type checkers, the mock will appear to have the exact same type as the `cls` argument. The mock will also pass `isinstance` checks.
20+
1921
## Mocking a function
2022

21-
To create a mock function, use [decoy.Decoy.create_decoy_func][]. Decoy can inspect a `spec` function signature to automatically configure the function as synchronous or asynchronous. Otherwise, the `is_async` argument can force the mock to be asynchronous.
23+
To mock a function, pass the `func` argument to `decoy.mock`. Decoy will inspect `func` to automatically configure the function as synchronous or asynchronous.
24+
25+
```python
26+
def test_my_thing(decoy: Decoy) -> None:
27+
mock_function = decoy.mock(func=some_function)
28+
```
29+
30+
To type checkers, the mock will appear to have the exact same type as the `func` argument. The function mock will pass `inspect.signature` checks.
31+
32+
## Creating an anonymous mock
2233

23-
To type checkers, the mock will appear to have the exact same type as the `spec` argument, if used.
34+
If you use neither `cls` nor `func` when calling `decoy.mock`, you will get an anonymous mock. You can use the `is_async` argument to return an asynchronous mock.
2435

2536
```python
2637
def test_my_thing(decoy: Decoy) -> None:
27-
mock_function = decoy.create_decoy_func(spec=some_function)
28-
free_async_function = decoy.create_decoy_func(is_async=True)
38+
anon_function = decoy.mock()
39+
async_anon_function = decoy.mock(is_async=True)
2940
```

docs/usage/errors-and-warnings.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Decoy's job as a mocking library is to provide you, the user, with useful design
99
A [decoy.errors.VerifyError][] will be raised if a call to [decoy.Decoy.verify][] does not match the given rehearsal. This is a normal assertion, and means your code under test isn't behaving according to your tests specifications.
1010

1111
```python
12-
func = decoy.create_decoy_func()
12+
func = decoy.mock()
1313

1414
func("hello")
1515

@@ -69,8 +69,8 @@ class Subject:
6969
return result
7070

7171
def test_subject(decoy: Decoy):
72-
data_getter = decoy.create_decoy(spec=DataGetter)
73-
data_handler = decoy.create_decoy(spec=DataHandler)
72+
data_getter = decoy.mock(cls=DataGetter)
73+
data_handler = decoy.mock(cls=DataHandler)
7474
subject = Subject(data_getter=data_getter, data_handler=data_handler)
7575

7676
decoy.when(data_getter.get("data-id")).then_return(42)
@@ -133,8 +133,8 @@ This, however, requires a sizable mentality shift in terms of how you use mocks
133133

134134
```python
135135
def test_subject(decoy: Decoy):
136-
data_getter = decoy.create_decoy(spec=DataGetter)
137-
data_handler = decoy.create_decoy(spec=DataHandler)
136+
data_getter = decoy.mock(cls=DataGetter)
137+
data_handler = decoy.mock(cls=DataHandler)
138138
subject = Subject(data_getter=data_getter, data_handler=data_handler)
139139

140140
decoy.when(data_getter.get("data-id")).then_return(42)

docs/usage/matchers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ from .logger import Logger
1717
from .my_thing import MyThing
1818

1919
def test_log_warning(decoy: Decoy):
20-
logger = decoy.create_decoy(spec=Logger)
20+
logger = decoy.mock(cls=Logger)
2121

2222
subject = MyThing(logger=logger)
2323

@@ -46,7 +46,7 @@ from .event_consumer import EventConsumer
4646

4747

4848
def test_event_listener(decoy: Decoy):
49-
event_source = decoy.create_decoy(spec=EventSource)
49+
event_source = decoy.mock(cls=EventSource)
5050
subject = EventConsumer(event_source=event_source)
5151
captor = matchers.Captor()
5252

docs/usage/verify.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The `verify` API uses the same "rehearsal" syntax as the [`when` API](./when).
2222

2323
```python
2424
def test_my_thing(decoy: Decoy) -> None:
25-
database = decoy.create_decoy(spec=Database)
25+
database = decoy.mock(cls=Database)
2626

2727
subject = MyThing(database=database)
2828
subject.delete_model_by_id("some-id")
@@ -39,7 +39,7 @@ If your dependency uses async/await, simply add `await` to the rehearsal:
3939
```python
4040
@pytest.mark.asyncio
4141
async def test_my_async_thing(decoy: Decoy) -> None:
42-
database = decoy.create_decoy(spec=Database)
42+
database = decoy.mock(cls=Database)
4343

4444
subject = MyThing(database=database)
4545
await subject.delete_model_by_id("some-id")

docs/usage/when.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A stub is a mock that is pre-configured to return a result or raise an error if
88

99
```python
1010
def test_my_thing(decoy: Decoy) -> None:
11-
database = decoy.create_decoy(spec=Database)
11+
database = decoy.mock(cls=Database)
1212
subject = MyThing(database=database)
1313

1414
decoy.when(database.get("some-id")).then_return(Model(id="some-id"))
@@ -32,7 +32,7 @@ You can configure your stub to raise an error if called in a certain way:
3232

3333
```python
3434
def test_my_thing_when_database_raises(decoy: Decoy) -> None:
35-
database = decoy.create_decoy(spec=Database)
35+
database = decoy.mock(cls=Database)
3636
subject = MyThing(database=database)
3737

3838
decoy.when(database.get("foo")).then_raise(KeyError(f"foo does not exist"))
@@ -48,7 +48,7 @@ If your dependency uses async/await, simply add `await` to the rehearsal:
4848
```python
4949
@pytest.mark.asyncio
5050
async def test_my_async_thing(decoy: Decoy) -> None:
51-
database = decoy.create_decoy(spec=Database)
51+
database = decoy.mock(cls=Database)
5252
subject = MyThing(database=database)
5353

5454
decoy.when(await database.get("some-id")).then_return(Model(id="some-id"))

docs/why.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def decoy() -> Decoy:
177177

178178
@pytest.fixture
179179
def mock_database(decoy: Decoy) -> Database:
180-
return decoy.create_decoy(spec=Database)
180+
return decoy.mock(cls=Database)
181181

182182
@pytest.fixture
183183
def mock_book() -> Book:
@@ -265,7 +265,7 @@ def decoy() -> Decoy:
265265

266266
@pytest.fixture
267267
def mock_logger(decoy: Decoy) -> Logger:
268-
return decoy.create_decoy(spec=Logger)
268+
return decoy.mock(cls=Logger)
269269
```
270270

271271
For verification of spies, Decoy doesn't do much except set out to add typechecking.

tests/test_call_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
@pytest.fixture
1212
def call_stack(decoy: Decoy) -> CallStack:
1313
"""Get a mock instance of a CallStack."""
14-
return decoy.create_decoy(spec=CallStack)
14+
return decoy.mock(cls=CallStack)
1515

1616

1717
@pytest.fixture
1818
def stub_store(decoy: Decoy) -> StubStore:
1919
"""Get a mock instance of a StubStore."""
20-
return decoy.create_decoy(spec=StubStore)
20+
return decoy.mock(cls=StubStore)
2121

2222

2323
@pytest.fixture

tests/test_core.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,37 @@
1717
@pytest.fixture
1818
def create_spy(decoy: Decoy) -> SpyFactory:
1919
"""Get a mock instance of a create_spy factory function."""
20-
return decoy.create_decoy_func(spec=default_create_spy)
20+
return decoy.mock(func=default_create_spy)
2121

2222

2323
@pytest.fixture
2424
def call_handler(decoy: Decoy) -> CallHandler:
2525
"""Get a mock instance of a create_spy factory function."""
26-
return decoy.create_decoy(spec=CallHandler)
26+
return decoy.mock(cls=CallHandler)
2727

2828

2929
@pytest.fixture
3030
def call_stack(decoy: Decoy) -> CallStack:
3131
"""Get a mock instance of a CallStack."""
32-
return decoy.create_decoy(spec=CallStack)
32+
return decoy.mock(cls=CallStack)
3333

3434

3535
@pytest.fixture
3636
def stub_store(decoy: Decoy) -> StubStore:
3737
"""Get a mock instance of a StubStore."""
38-
return decoy.create_decoy(spec=StubStore)
38+
return decoy.mock(cls=StubStore)
3939

4040

4141
@pytest.fixture
4242
def verifier(decoy: Decoy) -> Verifier:
4343
"""Get a mock instance of a Verifier."""
44-
return decoy.create_decoy(spec=Verifier)
44+
return decoy.mock(cls=Verifier)
4545

4646

4747
@pytest.fixture
4848
def warning_checker(decoy: Decoy) -> WarningChecker:
4949
"""Get a mock instance of a Verifier."""
50-
return decoy.create_decoy(spec=WarningChecker)
50+
return decoy.mock(cls=WarningChecker)
5151

5252

5353
@pytest.fixture

0 commit comments

Comments
 (0)