Skip to content
GuiforgePublic

About

pytest plugin to fake time.sleep and asyncio.sleep in tests

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

100 Commits

Folders and files

Repository files navigation

SleepFake Logo

CI PyPI version Python versions Coverage 100% Free-threading ready License: MIT

πŸ’€ SleepFake: Time Travel for Your Tests

Ever wish your tests could skip the waiting but keep correct time behavior? SleepFake patches time.sleep and asyncio.sleep so tests return instantly while frozen time moves forward exactly as requested. Async timeouts (asyncio.wait_for, asyncio.timeout, loop.call_later) fire on the fake clock too.

πŸ“¦ Install

pip install sleepfake

Python 3.10 to 3.15, including free-threaded builds (3.13t+). The pytest plugin registers itself.

⚑ Quick start: global autouse

Make SleepFake apply to every test. Add to pyproject.toml:

[tool.pytest.ini_options]
sleepfake_autouse = true

Now regular tests skip sleeps:

import time


def test_retry():
    start = time.time()
    time.sleep(30)  # returns instantly
    assert time.time() - start >= 30

Async works the same way, timeouts included:

import asyncio

import pytest


async def test_polling():
    start = asyncio.get_running_loop().time()
    await asyncio.sleep(10)  # returns instantly
    assert asyncio.get_running_loop().time() - start >= 10


async def test_gives_up():
    with pytest.raises(TimeoutError):
        await asyncio.wait_for(asyncio.Event().wait(), timeout=60)  # ~20 ms, not 60 s

pytest-asyncio users: add asyncio_mode = "auto" (or mark tests with @pytest.mark.asyncio) so pytest collects async tests.

🧭 Choose your usage style

Use case Best option
Apply everywhere (most teams) Global autouse (sleepfake_autouse = true or --sleepfake)
Per-test explicit control sleepfake fixture
Decoration-style usage @pytest.mark.sleepfake
Non-pytest scripts / direct control SleepFake context manager

All pytest styles share one instance per test: combining them never patches twice.

πŸ“š Usage

Context manager
import asyncio
import time

from sleepfake import SleepFake

# Sync
with SleepFake():
    start = time.time()
    time.sleep(10)  # returns instantly
    assert time.time() - start >= 10


# Async: `async with` or plain `with` both work
async def main():
    async with SleepFake():
        start = asyncio.get_running_loop().time()
        await asyncio.sleep(5)  # returns instantly
        assert asyncio.get_running_loop().time() - start >= 5

Keep some modules on real clocks with ignore:

with SleepFake(ignore=["my_project.telemetry"]):
    ...
Fixture (sleepfake)
import asyncio
import time


def test_retry_logic(sleepfake):
    start = time.time()
    time.sleep(30)  # instantly skipped
    assert time.time() - start >= 30


async def test_polling(sleepfake):
    start = asyncio.get_running_loop().time()
    await asyncio.gather(asyncio.sleep(1), asyncio.sleep(5), asyncio.sleep(3))
    # All three complete instantly; the clock sits at +5 s
    assert asyncio.get_running_loop().time() - start >= 5

The fixture yields the active SleepFake; sleepfake.mock_sleep(60) advances the clock by hand.

Deprecated: asleepfake still works but warns. Use sleepfake for sync and async tests.

Marker (@pytest.mark.sleepfake)
import time

import pytest


@pytest.mark.sleepfake
def test_marked():
    start = time.time()
    time.sleep(100)
    assert time.time() - start >= 100

Works on async tests, classes and modules (pytestmark) too.

Global autouse, opt-out and ignores

Enable it in config or on the command line:

# pyproject.toml
[tool.pytest.ini_options]
sleepfake_autouse = true
sleepfake_ignore = ["my_project.telemetry", "my_project.metrics"]
pytest --sleepfake --sleepfake-ignore my_project.telemetry

Opt a single test out with @pytest.mark.no_sleepfake (it has no effect if the test explicitly requests the sleepfake fixture).

Directory-scoped ignores go in a conftest.py; the nearest one wins:

# conftest.py
pytest_sleepfake_ignore = ["my_project.telemetry"]

Function-scoped fixtures are set up and torn down with the fake active; session-, module- and class-scoped fixtures stay on real time.

Async timeouts and the autojump threshold

asyncio.sleep always wakes instantly. Loop timers (asyncio.wait_for, asyncio.timeout, loop.call_later) fire once the event loop has been idle for autojump_threshold real seconds (default 0.02). That short grace period lets real I/O, such as a local test server, answer before a timeout is forced.

async def test_timeout(sleepfake):
    with pytest.raises(TimeoutError):
        async with asyncio.timeout(2):
            await asyncio.sleep(10)  # the clock stops at +2 s, like real asyncio

Tune it per instance or per project:

SleepFake(autojump_threshold=0)  # jump as soon as the loop is idle
SleepFake(autojump_threshold=math.inf)  # never jump: timers only fire when sleeps move the clock
[tool.pytest.ini_options]
sleepfake_autojump_threshold = "0.5"

πŸ› οΈ Options reference

Where Option Purpose
SleepFake(...) ignore: list[str] Module prefixes that stay on real clocks.
SleepFake(...) autojump_threshold: float Real idle seconds before loop timers fire (default 0.02, 0, math.inf).
pytest config sleepfake_autouse = true Apply SleepFake to every test.
pytest config sleepfake_ignore Module prefixes that stay on real clocks, for every test.
pytest config sleepfake_autojump_threshold Same as the constructor argument, for every test.
pytest CLI --sleepfake Same as sleepfake_autouse = true.
pytest CLI --sleepfake-ignore MODULE Add an ignored prefix (repeatable).
conftest.py pytest_sleepfake_ignore Ignored prefixes for that directory subtree (a string or an iterable).
marker @pytest.mark.sleepfake Apply SleepFake to one test, class or module.
marker @pytest.mark.no_sleepfake Opt one test out of global autouse.

Every ignore list is merged with DEFAULT_IGNORE = ["_pytest.timing", "pytest_timeout"], which keeps pytest's --durations and pytest-timeout on real clocks. Disable the plugin with -p no:sleepfake.

πŸ§ͺ How it works

Aspect Detail
Clock freezegun freezes time.time, time.monotonic, datetime.now... and so the event loop's clock
Sync sleep time.sleep(n) ticks the frozen clock by n (thread-safe)
Async sleep Once the event loop is idle, the clock jumps to the earliest pending deadline and that sleep wakes, so concurrent sleeps resolve in order
Loop timers BaseEventLoop.call_at is patched; after autojump_threshold real idle seconds the clock jumps to the next timer
Module aliases from time import sleep / from asyncio import sleep bindings in sys.modules are swapped on entry and restored on exit
Nesting Contexts nest; the innermost one drives the clock

⚠️ Limitations

  • Local bindings. A sleep captured in a local variable before the context starts (_sleep = time.sleep) keeps calling the real function.
  • Real I/O under a timeout. If real I/O takes longer than autojump_threshold inside a wait_for/timeout, the timeout fires on the fake clock. Raise the threshold, or set it to math.inf.
  • Other event loops. Timer autojump reads asyncio's pure-Python loop internals. On other loops (e.g. uvloop) asyncio.sleep is still faked, but loop timers need real time.
  • Timer precision. A timer jump lands 1 Β΅s after the deadline: asyncio only runs a timer once the clock is strictly past it.
  • Shared clock. The frozen clock is global: every thread and every event loop sees the same time.

πŸ†š Alternatives

Tool Fakes time.sleep Fakes asyncio.sleep / loop timers Freezes datetime / time.time
SleepFake βœ… βœ… βœ… (via freezegun)
freezegun ❌ (really sleeps) ❌ βœ…
time-machine ❌ ❌ βœ…
looptime ❌ βœ… (asyncio loop time) ❌

🀝 Contributing

make dev-install   # uv sync + prek git hooks
make test-all      # ruff + ty, then tests
make cov           # tests under coverage (100% required)
make test-all-python  # 3.10 to 3.15 and 3.14t

PRs and issues welcome. See CHANGELOG.md for release notes.

About

pytest plugin to fake time.sleep and asyncio.sleep in tests

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages