0
# Pytest Integration
1
2
Time Machine provides seamless integration with pytest through fixtures, markers, and automatic test discovery. This enables both imperative and declarative approaches to time travel in tests.
3
4
## Capabilities
5
6
### Time Machine Fixture
7
8
A pytest fixture that provides a high-level interface for time travel within test functions. The fixture automatically manages time travel lifecycle and provides methods for time manipulation.
9
10
```python { .api }
11
class TimeMachineFixture:
12
def move_to(self, destination: DestinationType, tick: bool | None = None) -> None:
13
"""
14
Initialize or move to a time destination.
15
16
Parameters:
17
- destination: Target time (timestamp, datetime, string, etc.)
18
- tick: Whether time advances during travel (optional)
19
"""
20
21
def shift(self, delta: dt.timedelta | int | float) -> None:
22
"""
23
Shift time by a relative amount.
24
25
Parameters:
26
- delta: Time delta as timedelta object, or seconds as int/float
27
28
Raises:
29
RuntimeError: If called before move_to()
30
"""
31
32
def stop(self) -> None:
33
"""Stop time travel and restore real time."""
34
```
35
36
```python { .api }
37
@pytest.fixture(name="time_machine")
38
def time_machine_fixture(request: pytest.FixtureRequest) -> Generator[TimeMachineFixture, None, None]:
39
"""
40
Pytest fixture providing TimeMachineFixture instance.
41
42
Automatically processes @pytest.mark.time_machine markers and
43
ensures proper cleanup after test completion.
44
45
Parameters:
46
- request: Pytest fixture request object
47
48
Yields:
49
TimeMachineFixture instance for test use
50
"""
51
```
52
53
Usage examples:
54
55
```python
56
import pytest
57
from datetime import datetime, timedelta
58
59
def test_with_fixture(time_machine):
60
# Start time travel
61
time_machine.move_to("2023-01-01 12:00:00")
62
assert datetime.now().year == 2023
63
assert datetime.now().hour == 12
64
65
# Shift time forward
66
time_machine.shift(3600) # 1 hour
67
assert datetime.now().hour == 13
68
69
# Move to new destination
70
time_machine.move_to("2024-06-15")
71
assert datetime.now().year == 2024
72
assert datetime.now().month == 6
73
74
def test_with_timedelta_shift(time_machine):
75
time_machine.move_to("2023-01-01")
76
77
# Shift using timedelta
78
time_machine.shift(timedelta(days=7, hours=3))
79
assert datetime.now().day == 8
80
assert datetime.now().hour == 3
81
82
def test_tick_modes(time_machine):
83
# Static time mode
84
time_machine.move_to("2023-01-01", tick=False)
85
time1 = datetime.now()
86
time.sleep(0.1)
87
time2 = datetime.now()
88
assert time1 == time2
89
90
# Switch to advancing time
91
time_machine.move_to("2023-01-01", tick=True)
92
time1 = datetime.now()
93
time.sleep(0.1)
94
time2 = datetime.now()
95
assert time2 > time1
96
```
97
98
### Pytest Markers
99
100
Declarative time travel using pytest markers. Tests marked with `@pytest.mark.time_machine` automatically receive time travel without explicit fixture usage.
101
102
```python { .api }
103
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
104
"""
105
Pytest hook that automatically adds time_machine fixture to tests
106
with the time_machine marker.
107
108
Parameters:
109
- items: List of collected test items
110
"""
111
112
def pytest_configure(config: pytest.Config) -> None:
113
"""
114
Pytest hook that registers the time_machine marker.
115
116
Parameters:
117
- config: Pytest configuration object
118
"""
119
```
120
121
Usage examples:
122
123
```python
124
import pytest
125
from datetime import datetime
126
127
# Basic marker usage
128
@pytest.mark.time_machine("2023-01-01")
129
def test_new_year():
130
assert datetime.now().year == 2023
131
132
# Marker with datetime object
133
@pytest.mark.time_machine(datetime(2023, 6, 15, 10, 30))
134
def test_specific_datetime():
135
assert datetime.now().month == 6
136
assert datetime.now().day == 15
137
assert datetime.now().hour == 10
138
139
# Marker with tick=False
140
@pytest.mark.time_machine("2023-01-01", tick=False)
141
def test_static_time():
142
time1 = datetime.now()
143
time.sleep(0.1)
144
time2 = datetime.now()
145
assert time1 == time2
146
147
# Marker with timestamp
148
@pytest.mark.time_machine(0) # Unix epoch
149
def test_epoch():
150
assert datetime.now().year == 1970
151
152
# Combining marker with fixture for additional control
153
@pytest.mark.time_machine("2023-01-01")
154
def test_marker_with_fixture(time_machine):
155
assert datetime.now().year == 2023
156
157
# Additional time manipulation
158
time_machine.shift(timedelta(months=6))
159
assert datetime.now().month == 7
160
```
161
162
### Parametrized Time Tests
163
164
Combine pytest parametrization with time travel for comprehensive time-based testing:
165
166
```python
167
@pytest.mark.parametrize("test_date", [
168
"2023-01-01",
169
"2023-06-15",
170
"2023-12-25"
171
])
172
def test_multiple_dates(time_machine, test_date):
173
time_machine.move_to(test_date)
174
parsed_date = datetime.strptime(test_date, "%Y-%m-%d")
175
assert datetime.now().year == parsed_date.year
176
assert datetime.now().month == parsed_date.month
177
assert datetime.now().day == parsed_date.day
178
179
@pytest.mark.parametrize("hours_shift", [1, 6, 12, 24])
180
def test_time_shifts(time_machine, hours_shift):
181
time_machine.move_to("2023-01-01 00:00:00")
182
time_machine.shift(hours_shift * 3600)
183
184
expected_hour = hours_shift % 24
185
assert datetime.now().hour == expected_hour
186
```
187
188
### Test Class Integration
189
190
Time Machine can be applied to entire test classes using markers:
191
192
```python
193
@pytest.mark.time_machine("2023-01-01")
194
class TestNewYearFeatures:
195
def test_feature_one(self):
196
assert datetime.now().year == 2023
197
198
def test_feature_two(self, time_machine):
199
assert datetime.now().year == 2023
200
# Can still manipulate time within tests
201
time_machine.shift(timedelta(days=30))
202
assert datetime.now().month == 2
203
```
204
205
### Async Test Support
206
207
Time Machine works with async tests through the async context manager interface:
208
209
```python
210
@pytest.mark.asyncio
211
async def test_async_time_travel():
212
async with time_machine.travel("2023-01-01"):
213
assert datetime.now().year == 2023
214
await asyncio.sleep(0.1) # Async operations work normally
215
216
@pytest.mark.asyncio
217
@pytest.mark.time_machine("2023-01-01")
218
async def test_async_with_marker():
219
assert datetime.now().year == 2023
220
await some_async_operation()
221
```
222
223
### Configuration
224
225
The pytest integration can be configured through pytest configuration files:
226
227
```ini
228
# pytest.ini
229
[tool:pytest]
230
markers =
231
time_machine: Set time for testing with time-machine
232
```
233
234
```python
235
# conftest.py - Custom fixture configuration
236
import pytest
237
import time_machine
238
239
@pytest.fixture(scope="session")
240
def frozen_time():
241
"""Session-scoped time freeze for consistent test runs."""
242
with time_machine.travel("2023-01-01", tick=False):
243
yield
244
```
245
246
### Error Handling
247
248
The pytest integration provides clear error messages for common mistakes:
249
250
```python
251
def test_shift_before_move(time_machine):
252
# This will raise RuntimeError
253
try:
254
time_machine.shift(3600)
255
except RuntimeError as e:
256
assert "Initialize time_machine with move_to()" in str(e)
257
```
258
259
## Type Definitions
260
261
```python { .api }
262
DestinationType = Union[
263
int, float, dt.datetime, dt.timedelta, dt.date, str,
264
Callable[[], Any], Generator[Any, None, None]
265
]
266
```