tmock 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tmock-0.1.0/LICENSE +21 -0
- tmock-0.1.0/PKG-INFO +273 -0
- tmock-0.1.0/README.md +245 -0
- tmock-0.1.0/pyproject.toml +63 -0
- tmock-0.1.0/setup.cfg +4 -0
- tmock-0.1.0/src/tmock/__init__.py +19 -0
- tmock-0.1.0/src/tmock/call_record.py +83 -0
- tmock-0.1.0/src/tmock/class_schema.py +336 -0
- tmock-0.1.0/src/tmock/exceptions.py +24 -0
- tmock-0.1.0/src/tmock/field_ref.py +15 -0
- tmock-0.1.0/src/tmock/interceptor.py +378 -0
- tmock-0.1.0/src/tmock/matchers/__init__.py +0 -0
- tmock-0.1.0/src/tmock/matchers/any.py +39 -0
- tmock-0.1.0/src/tmock/matchers/base.py +23 -0
- tmock-0.1.0/src/tmock/mock_generator.py +170 -0
- tmock-0.1.0/src/tmock/reset.py +58 -0
- tmock-0.1.0/src/tmock/stubbing_dsl.py +115 -0
- tmock-0.1.0/src/tmock/tpatch.py +584 -0
- tmock-0.1.0/src/tmock/verification_dsl.py +139 -0
- tmock-0.1.0/src/tmock.egg-info/PKG-INFO +273 -0
- tmock-0.1.0/src/tmock.egg-info/SOURCES.txt +27 -0
- tmock-0.1.0/src/tmock.egg-info/dependency_links.txt +1 -0
- tmock-0.1.0/src/tmock.egg-info/requires.txt +9 -0
- tmock-0.1.0/src/tmock.egg-info/top_level.txt +1 -0
- tmock-0.1.0/tests/test_async.py +308 -0
- tmock-0.1.0/tests/test_mock_generator.py +37 -0
- tmock-0.1.0/tests/test_mock_protocol.py +99 -0
- tmock-0.1.0/tests/test_signature_validation.py +176 -0
- tmock-0.1.0/tests/test_stubbing_dsl.py +180 -0
tmock-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedram Hajesmaeeli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tmock-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tmock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A strict, type-safe mock library for Python with full IDE autocomplete support.
|
|
5
|
+
Author-email: Pedram <pedram.esmaeeli@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/soypedram/tmock
|
|
8
|
+
Keywords: mock,testing,type-safe,mockito,mypy,pyright
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: typeguard>=4.0.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
26
|
+
Requires-Dist: pydantic>=2.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# tmock
|
|
30
|
+
|
|
31
|
+
**Type-safe mocking for modern Python.**
|
|
32
|
+
|
|
33
|
+
`tmock` is a mocking library designed to keep your tests aligned with your actual code. It prioritizes type safety and signature adherence over the infinite flexibility of standard "magic" mocks.
|
|
34
|
+
|
|
35
|
+
## Why tmock?
|
|
36
|
+
|
|
37
|
+
The standard library's `unittest.mock` is incredibly flexible. However, that flexibility can sometimes be a liability. A `MagicMock` will happily accept arguments that don't exist in your function signature, or types that would cause your real code to crash.
|
|
38
|
+
|
|
39
|
+
**The Trade-off:**
|
|
40
|
+
|
|
41
|
+
* **unittest.mock:** Optimizes for ease of setup. If you change a method signature in your code, your old tests often keep passing silently, only for the code to fail in production.
|
|
42
|
+
* **tmock:** Optimizes for correctness. It reads the type hints and signatures of your actual classes. If you try to stub a mock with the wrong arguments or types, the test fails immediately.
|
|
43
|
+
|
|
44
|
+
### Scenario: The Silent Drift
|
|
45
|
+
|
|
46
|
+
Imagine you refactor a method from `save(data)` to `save(data, should_commit)`.
|
|
47
|
+
|
|
48
|
+
1. **With Standard Mocks:** Your existing test calls `mock.save(data)`. The mock accepts it without complaint. The test passes, but it's no longer testing the reality of your API.
|
|
49
|
+
2. **With tmock:** When you run the test, `tmock` validates the call against the new signature. It notices discrepancies immediately, forcing you to update your test to match the new code structure.
|
|
50
|
+
|
|
51
|
+
## Key Features
|
|
52
|
+
|
|
53
|
+
### 1. Runtime Type Validation
|
|
54
|
+
|
|
55
|
+
This is the core differentiator of `tmock`. It doesn't just count arguments; it checks their types against your source code's annotations.
|
|
56
|
+
|
|
57
|
+
If your method is defined as:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
def update_score(self, user_id: int, score: float) -> bool: ...
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Trying to stub it with incorrect types raises an error *before the test even runs*:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# RAISES ERROR: TypeError: Argument 'user_id' expected int, got str
|
|
68
|
+
given().call(mock.update_score("user_123", 95.5)).returns(True)
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Better IDE Support
|
|
73
|
+
|
|
74
|
+
Because `tmock` mirrors the structure of your class, it plays much nicer with your editor than dynamic mocks. You get better autocompletion and static analysis support, making it easier to write tests without constantly flipping back to the source file to remember argument names.
|
|
75
|
+
|
|
76
|
+
### 3. Native Property & Field Support
|
|
77
|
+
|
|
78
|
+
Mocking properties or data attributes usually requires verbose `__setattr__` patching. `tmock` handles them natively via its DSL, supporting getters, setters, Dataclasses, and Pydantic models out of the box.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
`tmock` is currently in late-stage development and is **coming soon to PyPI**.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Coming soon
|
|
88
|
+
pip install tmock
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Quick Demo
|
|
93
|
+
|
|
94
|
+
Here's a realistic example: testing a `NotificationService` that depends on an `EmailClient` and uses a module-level config.
|
|
95
|
+
|
|
96
|
+
**The production code:**
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# notification_service.py
|
|
100
|
+
from myapp.config import MAX_BATCH_SIZE
|
|
101
|
+
from myapp.email import EmailClient
|
|
102
|
+
|
|
103
|
+
class NotificationService:
|
|
104
|
+
def __init__(self, email_client: EmailClient):
|
|
105
|
+
self.client = email_client
|
|
106
|
+
|
|
107
|
+
def notify_users(self, user_ids: list[int], message: str) -> int:
|
|
108
|
+
"""Send notifications in batches. Returns count of successful sends."""
|
|
109
|
+
sent = 0
|
|
110
|
+
for i in range(0, len(user_ids), MAX_BATCH_SIZE):
|
|
111
|
+
batch = user_ids[i:i + MAX_BATCH_SIZE]
|
|
112
|
+
for user_id in batch:
|
|
113
|
+
if self.client.send(user_id, message):
|
|
114
|
+
sent += 1
|
|
115
|
+
return sent
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Testing with mocks** — Use `tmock()` when you control the dependency and pass it in (dependency injection):
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from tmock import tmock, given, verify, any
|
|
122
|
+
|
|
123
|
+
def test_notify_users_returns_success_count():
|
|
124
|
+
# Create a type-safe mock of the dependency
|
|
125
|
+
client = tmock(EmailClient)
|
|
126
|
+
|
|
127
|
+
# Stub the send method - first call succeeds, second fails
|
|
128
|
+
given().call(client.send(1, "Hello")).returns(True)
|
|
129
|
+
given().call(client.send(2, "Hello")).returns(False)
|
|
130
|
+
|
|
131
|
+
# Inject the mock and test
|
|
132
|
+
service = NotificationService(client)
|
|
133
|
+
result = service.notify_users([1, 2], "Hello")
|
|
134
|
+
|
|
135
|
+
assert result == 1 # Only one succeeded
|
|
136
|
+
verify().call(client.send(any(int), "Hello")).times(2)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Testing with patches** — Use `tpatch` when you need to replace something you don't control, like a module variable or a function called internally:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from tmock import tpatch
|
|
143
|
+
|
|
144
|
+
def test_notify_users_respects_batch_size():
|
|
145
|
+
client = tmock(EmailClient)
|
|
146
|
+
given().call(client.send(any(int), any(str))).returns(True)
|
|
147
|
+
|
|
148
|
+
# Patch the module variable to force smaller batches
|
|
149
|
+
with tpatch.module_var("path.to.notification_service.MAX_BATCH_SIZE", 2):
|
|
150
|
+
service = NotificationService(client)
|
|
151
|
+
service.notify_users([1, 2, 3, 4, 5], "Hi")
|
|
152
|
+
|
|
153
|
+
# Verify all 5 emails were sent despite small batch size
|
|
154
|
+
verify().call(client.send(any(int), "Hi")).times(5)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## Usage Guide
|
|
159
|
+
|
|
160
|
+
### Creating a Mock
|
|
161
|
+
|
|
162
|
+
The entry point is simple. Pass your class to `tmock` to create a strict proxy.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from tmock import tmock, given, verify, any
|
|
166
|
+
from my_app import Database
|
|
167
|
+
|
|
168
|
+
db = tmock(Database)
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Stubbing (The `given` DSL)
|
|
173
|
+
|
|
174
|
+
**Define stubs before calling.** Unlike `unittest.mock`, tmock requires you to declare behavior upfront—calling an unstubbed method raises `TMockUnexpectedCallError`.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Simple return value
|
|
178
|
+
given().call(db.get_user(123)).returns({"name": "Alice"})
|
|
179
|
+
|
|
180
|
+
# Using Matchers for loose constraints
|
|
181
|
+
given().call(db.save_record(any(dict))).returns(True)
|
|
182
|
+
|
|
183
|
+
# Raising exceptions to test error handling
|
|
184
|
+
given().call(db.connect()).raises(ConnectionError("Timeout"))
|
|
185
|
+
|
|
186
|
+
# Dynamic responses
|
|
187
|
+
given().call(db.calculate(10)).runs(lambda args: args.get_by_name("val") * 2)
|
|
188
|
+
|
|
189
|
+
# Returning a mock from a stubbed method (for chained dependencies)
|
|
190
|
+
session = tmock(Session)
|
|
191
|
+
given().call(factory.create_session()).returns(session)
|
|
192
|
+
given().call(session.execute(any(str))).returns([{"id": 1}])
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Verification (The `verify` DSL)
|
|
196
|
+
|
|
197
|
+
Assert that specific interactions occurred.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# Verify exact call count
|
|
201
|
+
verify().call(db.save_record(any())).once()
|
|
202
|
+
|
|
203
|
+
# Verify something never happened
|
|
204
|
+
verify().call(db.delete_all()).never()
|
|
205
|
+
|
|
206
|
+
# Verify with specific counts
|
|
207
|
+
verify().call(db.connect()).times(3)
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Working with Fields and Properties
|
|
212
|
+
|
|
213
|
+
Stub and verify state changes on attributes, properties, or Pydantic fields.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
# Stubbing a value retrieval
|
|
217
|
+
given().get(db.is_connected).returns(True)
|
|
218
|
+
|
|
219
|
+
# Stubbing a value assignment
|
|
220
|
+
given().set(db.timeout, 5000).returns(None)
|
|
221
|
+
|
|
222
|
+
# Verifying a setter was called
|
|
223
|
+
verify().set(db.timeout, 5000).once()
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Patching (`tpatch`)
|
|
228
|
+
|
|
229
|
+
When you need to swap out objects internally used by other modules, use `tpatch`. It wraps `unittest.mock.patch` but creates a typed `tmock` interceptor instead of a generic mock.
|
|
230
|
+
|
|
231
|
+
The API forces you to be explicit about *what* you are patching (Method vs. Function vs. Field), which prevents common patching errors.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from tmock import tpatch, given
|
|
235
|
+
|
|
236
|
+
# Patching an instance method
|
|
237
|
+
with tpatch.method(UserService, "get_current_user") as mock:
|
|
238
|
+
given().call(mock()).returns(admin_user)
|
|
239
|
+
|
|
240
|
+
# Patching a module-level function
|
|
241
|
+
with tpatch.function("my_module.external_api_call") as mock:
|
|
242
|
+
given().call(mock()).returns(200)
|
|
243
|
+
|
|
244
|
+
# Patching a class variable
|
|
245
|
+
with tpatch.class_var(Config, "MAX_RETRIES") as field:
|
|
246
|
+
given().get(field).returns(1)
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Async Support
|
|
251
|
+
|
|
252
|
+
`tmock` natively handles `async`/`await`. You stub async methods exactly the same way as synchronous ones; `tmock` handles the coroutine wrapping for you.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# Stubbing an async method
|
|
256
|
+
given().call(api_client.fetch_data()).returns(data)
|
|
257
|
+
|
|
258
|
+
# The mock is automatically awaitable
|
|
259
|
+
result = await api_client.fetch_data()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Documentation
|
|
263
|
+
|
|
264
|
+
- [Stubbing](docs/stubbing.md) — Define behavior with `.returns()`, `.raises()`, `.runs()`
|
|
265
|
+
- [Verification](docs/verification.md) — Assert interactions with `.once()`, `.times()`, `.never()`
|
|
266
|
+
- [Argument Matchers](docs/matchers.md) — Flexible matching with `any()` and `any(Type)`
|
|
267
|
+
- [Fields & Properties](docs/fields.md) — Mock getters/setters on dataclasses, Pydantic, properties
|
|
268
|
+
- [Patching](docs/patching.md) — Replace real code with `tpatch.method()`, `tpatch.function()`, etc.
|
|
269
|
+
- [Mocking Functions](docs/functions.md) — Mock standalone functions with `tmock(func)`
|
|
270
|
+
- [Protocols](docs/protocols.md) — Mock `typing.Protocol` interfaces
|
|
271
|
+
- [Async Support](docs/async.md) — Async methods and context managers
|
|
272
|
+
- [Reset Functions](docs/reset.md) — Clear stubs and interactions between tests
|
|
273
|
+
- [Magic Methods](docs/magic-methods.md) — Context managers, iteration, containers, and more
|
tmock-0.1.0/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# tmock
|
|
2
|
+
|
|
3
|
+
**Type-safe mocking for modern Python.**
|
|
4
|
+
|
|
5
|
+
`tmock` is a mocking library designed to keep your tests aligned with your actual code. It prioritizes type safety and signature adherence over the infinite flexibility of standard "magic" mocks.
|
|
6
|
+
|
|
7
|
+
## Why tmock?
|
|
8
|
+
|
|
9
|
+
The standard library's `unittest.mock` is incredibly flexible. However, that flexibility can sometimes be a liability. A `MagicMock` will happily accept arguments that don't exist in your function signature, or types that would cause your real code to crash.
|
|
10
|
+
|
|
11
|
+
**The Trade-off:**
|
|
12
|
+
|
|
13
|
+
* **unittest.mock:** Optimizes for ease of setup. If you change a method signature in your code, your old tests often keep passing silently, only for the code to fail in production.
|
|
14
|
+
* **tmock:** Optimizes for correctness. It reads the type hints and signatures of your actual classes. If you try to stub a mock with the wrong arguments or types, the test fails immediately.
|
|
15
|
+
|
|
16
|
+
### Scenario: The Silent Drift
|
|
17
|
+
|
|
18
|
+
Imagine you refactor a method from `save(data)` to `save(data, should_commit)`.
|
|
19
|
+
|
|
20
|
+
1. **With Standard Mocks:** Your existing test calls `mock.save(data)`. The mock accepts it without complaint. The test passes, but it's no longer testing the reality of your API.
|
|
21
|
+
2. **With tmock:** When you run the test, `tmock` validates the call against the new signature. It notices discrepancies immediately, forcing you to update your test to match the new code structure.
|
|
22
|
+
|
|
23
|
+
## Key Features
|
|
24
|
+
|
|
25
|
+
### 1. Runtime Type Validation
|
|
26
|
+
|
|
27
|
+
This is the core differentiator of `tmock`. It doesn't just count arguments; it checks their types against your source code's annotations.
|
|
28
|
+
|
|
29
|
+
If your method is defined as:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
def update_score(self, user_id: int, score: float) -> bool: ...
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Trying to stub it with incorrect types raises an error *before the test even runs*:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# RAISES ERROR: TypeError: Argument 'user_id' expected int, got str
|
|
40
|
+
given().call(mock.update_score("user_123", 95.5)).returns(True)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Better IDE Support
|
|
45
|
+
|
|
46
|
+
Because `tmock` mirrors the structure of your class, it plays much nicer with your editor than dynamic mocks. You get better autocompletion and static analysis support, making it easier to write tests without constantly flipping back to the source file to remember argument names.
|
|
47
|
+
|
|
48
|
+
### 3. Native Property & Field Support
|
|
49
|
+
|
|
50
|
+
Mocking properties or data attributes usually requires verbose `__setattr__` patching. `tmock` handles them natively via its DSL, supporting getters, setters, Dataclasses, and Pydantic models out of the box.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
`tmock` is currently in late-stage development and is **coming soon to PyPI**.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Coming soon
|
|
60
|
+
pip install tmock
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Demo
|
|
65
|
+
|
|
66
|
+
Here's a realistic example: testing a `NotificationService` that depends on an `EmailClient` and uses a module-level config.
|
|
67
|
+
|
|
68
|
+
**The production code:**
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# notification_service.py
|
|
72
|
+
from myapp.config import MAX_BATCH_SIZE
|
|
73
|
+
from myapp.email import EmailClient
|
|
74
|
+
|
|
75
|
+
class NotificationService:
|
|
76
|
+
def __init__(self, email_client: EmailClient):
|
|
77
|
+
self.client = email_client
|
|
78
|
+
|
|
79
|
+
def notify_users(self, user_ids: list[int], message: str) -> int:
|
|
80
|
+
"""Send notifications in batches. Returns count of successful sends."""
|
|
81
|
+
sent = 0
|
|
82
|
+
for i in range(0, len(user_ids), MAX_BATCH_SIZE):
|
|
83
|
+
batch = user_ids[i:i + MAX_BATCH_SIZE]
|
|
84
|
+
for user_id in batch:
|
|
85
|
+
if self.client.send(user_id, message):
|
|
86
|
+
sent += 1
|
|
87
|
+
return sent
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Testing with mocks** — Use `tmock()` when you control the dependency and pass it in (dependency injection):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from tmock import tmock, given, verify, any
|
|
94
|
+
|
|
95
|
+
def test_notify_users_returns_success_count():
|
|
96
|
+
# Create a type-safe mock of the dependency
|
|
97
|
+
client = tmock(EmailClient)
|
|
98
|
+
|
|
99
|
+
# Stub the send method - first call succeeds, second fails
|
|
100
|
+
given().call(client.send(1, "Hello")).returns(True)
|
|
101
|
+
given().call(client.send(2, "Hello")).returns(False)
|
|
102
|
+
|
|
103
|
+
# Inject the mock and test
|
|
104
|
+
service = NotificationService(client)
|
|
105
|
+
result = service.notify_users([1, 2], "Hello")
|
|
106
|
+
|
|
107
|
+
assert result == 1 # Only one succeeded
|
|
108
|
+
verify().call(client.send(any(int), "Hello")).times(2)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Testing with patches** — Use `tpatch` when you need to replace something you don't control, like a module variable or a function called internally:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from tmock import tpatch
|
|
115
|
+
|
|
116
|
+
def test_notify_users_respects_batch_size():
|
|
117
|
+
client = tmock(EmailClient)
|
|
118
|
+
given().call(client.send(any(int), any(str))).returns(True)
|
|
119
|
+
|
|
120
|
+
# Patch the module variable to force smaller batches
|
|
121
|
+
with tpatch.module_var("path.to.notification_service.MAX_BATCH_SIZE", 2):
|
|
122
|
+
service = NotificationService(client)
|
|
123
|
+
service.notify_users([1, 2, 3, 4, 5], "Hi")
|
|
124
|
+
|
|
125
|
+
# Verify all 5 emails were sent despite small batch size
|
|
126
|
+
verify().call(client.send(any(int), "Hi")).times(5)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## Usage Guide
|
|
131
|
+
|
|
132
|
+
### Creating a Mock
|
|
133
|
+
|
|
134
|
+
The entry point is simple. Pass your class to `tmock` to create a strict proxy.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from tmock import tmock, given, verify, any
|
|
138
|
+
from my_app import Database
|
|
139
|
+
|
|
140
|
+
db = tmock(Database)
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Stubbing (The `given` DSL)
|
|
145
|
+
|
|
146
|
+
**Define stubs before calling.** Unlike `unittest.mock`, tmock requires you to declare behavior upfront—calling an unstubbed method raises `TMockUnexpectedCallError`.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Simple return value
|
|
150
|
+
given().call(db.get_user(123)).returns({"name": "Alice"})
|
|
151
|
+
|
|
152
|
+
# Using Matchers for loose constraints
|
|
153
|
+
given().call(db.save_record(any(dict))).returns(True)
|
|
154
|
+
|
|
155
|
+
# Raising exceptions to test error handling
|
|
156
|
+
given().call(db.connect()).raises(ConnectionError("Timeout"))
|
|
157
|
+
|
|
158
|
+
# Dynamic responses
|
|
159
|
+
given().call(db.calculate(10)).runs(lambda args: args.get_by_name("val") * 2)
|
|
160
|
+
|
|
161
|
+
# Returning a mock from a stubbed method (for chained dependencies)
|
|
162
|
+
session = tmock(Session)
|
|
163
|
+
given().call(factory.create_session()).returns(session)
|
|
164
|
+
given().call(session.execute(any(str))).returns([{"id": 1}])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Verification (The `verify` DSL)
|
|
168
|
+
|
|
169
|
+
Assert that specific interactions occurred.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Verify exact call count
|
|
173
|
+
verify().call(db.save_record(any())).once()
|
|
174
|
+
|
|
175
|
+
# Verify something never happened
|
|
176
|
+
verify().call(db.delete_all()).never()
|
|
177
|
+
|
|
178
|
+
# Verify with specific counts
|
|
179
|
+
verify().call(db.connect()).times(3)
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Working with Fields and Properties
|
|
184
|
+
|
|
185
|
+
Stub and verify state changes on attributes, properties, or Pydantic fields.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# Stubbing a value retrieval
|
|
189
|
+
given().get(db.is_connected).returns(True)
|
|
190
|
+
|
|
191
|
+
# Stubbing a value assignment
|
|
192
|
+
given().set(db.timeout, 5000).returns(None)
|
|
193
|
+
|
|
194
|
+
# Verifying a setter was called
|
|
195
|
+
verify().set(db.timeout, 5000).once()
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Patching (`tpatch`)
|
|
200
|
+
|
|
201
|
+
When you need to swap out objects internally used by other modules, use `tpatch`. It wraps `unittest.mock.patch` but creates a typed `tmock` interceptor instead of a generic mock.
|
|
202
|
+
|
|
203
|
+
The API forces you to be explicit about *what* you are patching (Method vs. Function vs. Field), which prevents common patching errors.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from tmock import tpatch, given
|
|
207
|
+
|
|
208
|
+
# Patching an instance method
|
|
209
|
+
with tpatch.method(UserService, "get_current_user") as mock:
|
|
210
|
+
given().call(mock()).returns(admin_user)
|
|
211
|
+
|
|
212
|
+
# Patching a module-level function
|
|
213
|
+
with tpatch.function("my_module.external_api_call") as mock:
|
|
214
|
+
given().call(mock()).returns(200)
|
|
215
|
+
|
|
216
|
+
# Patching a class variable
|
|
217
|
+
with tpatch.class_var(Config, "MAX_RETRIES") as field:
|
|
218
|
+
given().get(field).returns(1)
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Async Support
|
|
223
|
+
|
|
224
|
+
`tmock` natively handles `async`/`await`. You stub async methods exactly the same way as synchronous ones; `tmock` handles the coroutine wrapping for you.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Stubbing an async method
|
|
228
|
+
given().call(api_client.fetch_data()).returns(data)
|
|
229
|
+
|
|
230
|
+
# The mock is automatically awaitable
|
|
231
|
+
result = await api_client.fetch_data()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Documentation
|
|
235
|
+
|
|
236
|
+
- [Stubbing](docs/stubbing.md) — Define behavior with `.returns()`, `.raises()`, `.runs()`
|
|
237
|
+
- [Verification](docs/verification.md) — Assert interactions with `.once()`, `.times()`, `.never()`
|
|
238
|
+
- [Argument Matchers](docs/matchers.md) — Flexible matching with `any()` and `any(Type)`
|
|
239
|
+
- [Fields & Properties](docs/fields.md) — Mock getters/setters on dataclasses, Pydantic, properties
|
|
240
|
+
- [Patching](docs/patching.md) — Replace real code with `tpatch.method()`, `tpatch.function()`, etc.
|
|
241
|
+
- [Mocking Functions](docs/functions.md) — Mock standalone functions with `tmock(func)`
|
|
242
|
+
- [Protocols](docs/protocols.md) — Mock `typing.Protocol` interfaces
|
|
243
|
+
- [Async Support](docs/async.md) — Async methods and context managers
|
|
244
|
+
- [Reset Functions](docs/reset.md) — Clear stubs and interactions between tests
|
|
245
|
+
- [Magic Methods](docs/magic-methods.md) — Context managers, iteration, containers, and more
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tmock"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A strict, type-safe mock library for Python with full IDE autocomplete support."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Pedram", email = "pedram.esmaeeli@gmail.com"}]
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
keywords = ["mock", "testing", "type-safe", "mockito", "mypy", "pyright"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Testing :: Mocking",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typeguard>=4.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pre-commit",
|
|
30
|
+
"ruff",
|
|
31
|
+
"mypy",
|
|
32
|
+
"pytest",
|
|
33
|
+
"pytest-asyncio",
|
|
34
|
+
"pydantic>=2.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Repository = "https://github.com/soypedram/tmock"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
python_version = "3.13"
|
|
45
|
+
warn_return_any = true
|
|
46
|
+
warn_unused_configs = true
|
|
47
|
+
mypy_path = "src"
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
python_files = ["test_*.py"]
|
|
52
|
+
python_functions = ["test_*"]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 120
|
|
57
|
+
target-version = "py313"
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["E", "F", "I"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.format]
|
|
63
|
+
quote-style = "double"
|
tmock-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from tmock.interceptor import CallArguments
|
|
2
|
+
from tmock.matchers.any import any
|
|
3
|
+
from tmock.mock_generator import tmock
|
|
4
|
+
from tmock.reset import reset, reset_behaviors, reset_interactions
|
|
5
|
+
from tmock.stubbing_dsl import given
|
|
6
|
+
from tmock.tpatch import tpatch
|
|
7
|
+
from tmock.verification_dsl import verify
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
any.__name__,
|
|
11
|
+
CallArguments.__name__,
|
|
12
|
+
given.__name__,
|
|
13
|
+
reset.__name__,
|
|
14
|
+
reset_behaviors.__name__,
|
|
15
|
+
reset_interactions.__name__,
|
|
16
|
+
tmock.__name__,
|
|
17
|
+
tpatch.__name__,
|
|
18
|
+
verify.__name__,
|
|
19
|
+
]
|