CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-time-machine

Travel through time in your tests.

Overview
Eval results
Files

pytest-integration.mddocs/

Pytest Integration

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.

Capabilities

Time Machine Fixture

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 > time1

Pytest Markers

Declarative 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 == 7

Parametrized Time Tests

Combine 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_hour

Test Class Integration

Time 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 == 2

Async Test Support

Time 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()

Configuration

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):
        yield

Error Handling

The 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)

Type Definitions

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

docs

cli-migration.md

core-time-travel.md

escape-hatch.md

index.md

pytest-integration.md

tile.json