Travel through time in your tests.
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.
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.
class TimeMachineFixture:
def move_to(self, destination: DestinationType, tick: bool | None = None) -> None:
"""
Initialize or move to a time destination.
Parameters:
- destination: Target time (timestamp, datetime, string, etc.)
- tick: Whether time advances during travel (optional)
"""
def shift(self, delta: dt.timedelta | int | float) -> None:
"""
Shift time by a relative amount.
Parameters:
- delta: Time delta as timedelta object, or seconds as int/float
Raises:
RuntimeError: If called before move_to()
"""
def stop(self) -> None:
"""Stop time travel and restore real time."""@pytest.fixture(name="time_machine")
def time_machine_fixture(request: pytest.FixtureRequest) -> Generator[TimeMachineFixture, None, None]:
"""
Pytest fixture providing TimeMachineFixture instance.
Automatically processes @pytest.mark.time_machine markers and
ensures proper cleanup after test completion.
Parameters:
- request: Pytest fixture request object
Yields:
TimeMachineFixture instance for test use
"""Usage examples:
import pytest
from datetime import datetime, timedelta
def test_with_fixture(time_machine):
# Start time travel
time_machine.move_to("2023-01-01 12:00:00")
assert datetime.now().year == 2023
assert datetime.now().hour == 12
# Shift time forward
time_machine.shift(3600) # 1 hour
assert datetime.now().hour == 13
# Move to new destination
time_machine.move_to("2024-06-15")
assert datetime.now().year == 2024
assert datetime.now().month == 6
def test_with_timedelta_shift(time_machine):
time_machine.move_to("2023-01-01")
# Shift using timedelta
time_machine.shift(timedelta(days=7, hours=3))
assert datetime.now().day == 8
assert datetime.now().hour == 3
def test_tick_modes(time_machine):
# Static time mode
time_machine.move_to("2023-01-01", tick=False)
time1 = datetime.now()
time.sleep(0.1)
time2 = datetime.now()
assert time1 == time2
# Switch to advancing time
time_machine.move_to("2023-01-01", tick=True)
time1 = datetime.now()
time.sleep(0.1)
time2 = datetime.now()
assert time2 > time1Declarative time travel using pytest markers. Tests marked with @pytest.mark.time_machine automatically receive time travel without explicit fixture usage.
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
"""
Pytest hook that automatically adds time_machine fixture to tests
with the time_machine marker.
Parameters:
- items: List of collected test items
"""
def pytest_configure(config: pytest.Config) -> None:
"""
Pytest hook that registers the time_machine marker.
Parameters:
- config: Pytest configuration object
"""Usage examples:
import pytest
from datetime import datetime
# Basic marker usage
@pytest.mark.time_machine("2023-01-01")
def test_new_year():
assert datetime.now().year == 2023
# Marker with datetime object
@pytest.mark.time_machine(datetime(2023, 6, 15, 10, 30))
def test_specific_datetime():
assert datetime.now().month == 6
assert datetime.now().day == 15
assert datetime.now().hour == 10
# Marker with tick=False
@pytest.mark.time_machine("2023-01-01", tick=False)
def test_static_time():
time1 = datetime.now()
time.sleep(0.1)
time2 = datetime.now()
assert time1 == time2
# Marker with timestamp
@pytest.mark.time_machine(0) # Unix epoch
def test_epoch():
assert datetime.now().year == 1970
# Combining marker with fixture for additional control
@pytest.mark.time_machine("2023-01-01")
def test_marker_with_fixture(time_machine):
assert datetime.now().year == 2023
# Additional time manipulation
time_machine.shift(timedelta(months=6))
assert datetime.now().month == 7Combine pytest parametrization with time travel for comprehensive time-based testing:
@pytest.mark.parametrize("test_date", [
"2023-01-01",
"2023-06-15",
"2023-12-25"
])
def test_multiple_dates(time_machine, test_date):
time_machine.move_to(test_date)
parsed_date = datetime.strptime(test_date, "%Y-%m-%d")
assert datetime.now().year == parsed_date.year
assert datetime.now().month == parsed_date.month
assert datetime.now().day == parsed_date.day
@pytest.mark.parametrize("hours_shift", [1, 6, 12, 24])
def test_time_shifts(time_machine, hours_shift):
time_machine.move_to("2023-01-01 00:00:00")
time_machine.shift(hours_shift * 3600)
expected_hour = hours_shift % 24
assert datetime.now().hour == expected_hourTime Machine can be applied to entire test classes using markers:
@pytest.mark.time_machine("2023-01-01")
class TestNewYearFeatures:
def test_feature_one(self):
assert datetime.now().year == 2023
def test_feature_two(self, time_machine):
assert datetime.now().year == 2023
# Can still manipulate time within tests
time_machine.shift(timedelta(days=30))
assert datetime.now().month == 2Time Machine works with async tests through the async context manager interface:
@pytest.mark.asyncio
async def test_async_time_travel():
async with time_machine.travel("2023-01-01"):
assert datetime.now().year == 2023
await asyncio.sleep(0.1) # Async operations work normally
@pytest.mark.asyncio
@pytest.mark.time_machine("2023-01-01")
async def test_async_with_marker():
assert datetime.now().year == 2023
await some_async_operation()The pytest integration can be configured through pytest configuration files:
# pytest.ini
[tool:pytest]
markers =
time_machine: Set time for testing with time-machine# conftest.py - Custom fixture configuration
import pytest
import time_machine
@pytest.fixture(scope="session")
def frozen_time():
"""Session-scoped time freeze for consistent test runs."""
with time_machine.travel("2023-01-01", tick=False):
yieldThe pytest integration provides clear error messages for common mistakes:
def test_shift_before_move(time_machine):
# This will raise RuntimeError
try:
time_machine.shift(3600)
except RuntimeError as e:
assert "Initialize time_machine with move_to()" in str(e)DestinationType = Union[
int, float, dt.datetime, dt.timedelta, dt.date, str,
Callable[[], Any], Generator[Any, None, None]
]Install with Tessl CLI
npx tessl i tessl/pypi-time-machine