Skip to main content

A pytest plugin for mocking uuid.uuid4() calls

Project description

pytest-uuid logo

pytest-uuid

A pytest plugin for mocking UUID generation in your tests. Supports uuid1, uuid3, uuid4, uuid5, uuid6, uuid7, and uuid8.

PyPI version Docs llms.txt License: MIT Tests codecov Ruff uv ty pytest

Python

Features

  • Mock all UUID versions: uuid1, uuid3, uuid4, uuid5, uuid6, uuid7, uuid8
  • Works with both import uuid and from uuid import uuid4 patterns (how?)
  • Multiple ways to mock: static, sequence, seeded, or node-seeded
  • Decorator, marker, and fixture APIs (inspired by freezegun)
  • Configurable exhaustion behavior for sequences
  • Ignore list for packages that should use real UUIDs
  • Spy mode to track calls without mocking
  • Detailed call tracking with caller module/file info
  • Automatic cleanup after each test
  • Zero configuration required - just use the fixture
  • uuid6/uuid7/uuid8 support via uuid6 backport (Python < 3.14)

Installation

uv add --group dev pytest-uuid

Or with pip:

pip install pytest-uuid

Quick Start

Fixture API

import uuid

def test_single_uuid(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

def test_multiple_uuids(mock_uuid):
    mock_uuid.uuid4.set(
        "11111111-1111-4111-8111-111111111111",
        "22222222-2222-4222-8222-222222222222",
    )
    assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
    assert str(uuid.uuid4()) == "22222222-2222-4222-8222-222222222222"
    # Cycles back to the first UUID
    assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"

Decorator API

import uuid
from pytest_uuid import freeze_uuid4, freeze_uuid1, freeze_uuid7

@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_with_decorator():
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

@freeze_uuid4(seed=42)
def test_seeded():
    # Reproducible UUIDs from seed
    result = uuid.uuid4()
    assert result.version == 4

# Stack multiple versions
@freeze_uuid4("44444444-4444-4444-8444-444444444444")
@freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple_versions():
    assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
    assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"

Backward compatibility: The generic freeze_uuid is still available as an alias for freeze_uuid4.

Marker API

import uuid
import pytest

@pytest.mark.freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_with_marker():
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

@pytest.mark.freeze_uuid4(seed="node")
def test_node_seeded():
    # Same test always gets the same UUIDs
    result = uuid.uuid4()
    assert result.version == 4

# Stack multiple version markers
@pytest.mark.freeze_uuid4("44444444-4444-4444-8444-444444444444")
@pytest.mark.freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple_versions():
    assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
    assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"

Backward compatibility: The generic freeze_uuid marker is still available as an alias for freeze_uuid4.

Usage

Static UUIDs

Return the same UUID every time:

def test_static(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    assert uuid.uuid4() == uuid.uuid4()  # Same UUID

# Or with decorator
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_static_decorator():
    assert uuid.uuid4() == uuid.uuid4()  # Same UUID

UUID Sequences

Return UUIDs from a list:

def test_sequence(mock_uuid):
    mock_uuid.uuid4.set(
        "11111111-1111-4111-8111-111111111111",
        "22222222-2222-4222-8222-222222222222",
    )
    assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
    assert str(uuid.uuid4()) == "22222222-2222-4222-8222-222222222222"
    # Cycles back by default
    assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"

Seeded UUIDs

Generate reproducible UUIDs from a seed:

def test_seeded(mock_uuid):
    mock_uuid.uuid4.set_seed(42)
    first = uuid.uuid4()

    mock_uuid.uuid4.set_seed(42)  # Reset to same seed
    assert uuid.uuid4() == first  # Same UUID

# With decorator
@freeze_uuid4(seed=42)
def test_seeded_decorator():
    result = uuid.uuid4()
    assert result.version == 4  # Valid UUID v4

Node-Seeded UUIDs (Recommended)

Derive the seed from the test's node ID for automatic reproducibility:

def test_node_seeded(mock_uuid):
    mock_uuid.uuid4.set_seed_from_node()
    # Same test always produces the same sequence

# With marker
@pytest.mark.freeze_uuid4(seed="node")
def test_node_seeded_marker():
    # Same test always produces the same sequence
    pass

Why node seeding is recommended: Node-seeded UUIDs give you deterministic, reproducible tests without the maintenance burden of hardcoded UUIDs. Each test gets its own unique seed derived from its fully-qualified name (e.g., test_module.py::TestClass::test_method), so tests are isolated and don't affect each other. When a test fails, you get the same UUIDs on every run, making debugging easier. Unlike static UUIDs, you never have to update test files when adding new UUID calls.

Class-Level Node Seeding

import uuid
import pytest


@pytest.mark.freeze_uuid4(seed="node")
class TestUserService:
    def test_create(self):
        # Seed derived from "test_module.py::TestUserService::test_create"
        result = uuid.uuid4()
        assert result.version == 4

    def test_update(self):
        # Seed derived from "test_module.py::TestUserService::test_update"
        result = uuid.uuid4()
        assert result.version == 4

Module-Level Node Seeding

# tests/test_user_creation.py
import uuid
import pytest

pytestmark = pytest.mark.freeze_uuid4(seed="node")


def test_create_user():
    # Seed derived from "test_user_creation.py::test_create_user"
    result = uuid.uuid4()
    assert result.version == 4


def test_create_admin():
    # Seed derived from "test_user_creation.py::test_create_admin"
    result = uuid.uuid4()
    assert result.version == 4

Function-Level Node Seeding (Autouse Fixture)

For automatic node seeding on every test without markers:

# conftest.py
import pytest


@pytest.fixture(autouse=True)
def auto_seed_uuids(mock_uuid):
    """Automatically seed uuid4 from test node ID for all tests."""
    mock_uuid.uuid4.set_seed_from_node()

Every test now gets reproducible UUIDs without any boilerplate:

# test_example.py
import uuid


def test_user_creation():
    # No fixture argument needed - autouse handles it
    # UUIDs are deterministic based on this test's node ID
    user_id = uuid.uuid4()
    assert user_id.version == 4


def test_order_creation():
    # Different test = different seed = different UUIDs
    order_id = uuid.uuid4()
    assert order_id.version == 4

Session-Level Node Seeding

# conftest.py
import hashlib

import pytest
from pytest_uuid import freeze_uuid4


@pytest.fixture(scope="session", autouse=True)
def freeze_uuids_globally(request):
    # Use hashlib for deterministic seeding across processes.
    # Python's hash() is randomized per-process via PYTHONHASHSEED:
    # https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED
    #
    # Convert node ID to a deterministic integer seed:
    # 1. hashlib.sha256() creates a hash of the node ID string
    # 2. .hexdigest() returns the hash as a 64-char hex string
    # 3. [:16] takes first 16 hex chars (64 bits) - plenty of uniqueness
    # 4. int(..., 16) converts hex string to integer
    node_bytes = request.node.nodeid.encode()
    seed = int(hashlib.sha256(node_bytes).hexdigest()[:16], 16)
    with freeze_uuid4(seed=seed):
        yield

Note: For session-level fixtures, use request.node.nodeid directly since seed="node" in the marker requires per-test context. Alternatively, use a fixed seed for true global determinism. Always use hashlib (not hash()) for node-derived seeds, as Python's built-in hash() is randomized per-process.

Exhaustion Behavior

Control what happens when a UUID sequence is exhausted:

from pytest_uuid import ExhaustionBehavior, UUIDsExhaustedError

def test_exhaustion_raise(mock_uuid):
    mock_uuid.uuid4.set_exhaustion_behavior("raise")
    mock_uuid.uuid4.set("11111111-1111-4111-8111-111111111111")

    uuid.uuid4()  # Returns the UUID

    with pytest.raises(UUIDsExhaustedError):
        uuid.uuid4()  # Raises - sequence exhausted

# With decorator
@freeze_uuid4(
    ["11111111-1111-4111-8111-111111111111"],
    on_exhausted="raise",  # or "cycle" or "random"
)
def test_exhaustion_decorator():
    uuid.uuid4()
    with pytest.raises(UUIDsExhaustedError):
        uuid.uuid4()

Exhaustion behaviors:

  • "cycle" (default): Loop back to the start of the sequence
  • "random": Fall back to generating random UUIDs
  • "raise": Raise UUIDsExhaustedError

Spy Mode

Track uuid.uuid4() calls without mocking them. Useful when you need to verify UUID generation happens without controlling the output.

Using spy_uuid Fixture

# myapp/models.py
from uuid import uuid4

class User:
    def __init__(self, name):
        self.id = str(uuid4())
        self.name = name

# tests/test_models.py
def test_user_generates_uuid(spy_uuid):
    """Verify User creates a UUID without controlling its value."""
    user = User("Alice")

    assert spy_uuid.call_count == 1
    assert user.id == str(spy_uuid.last_uuid)

Using mock_uuid.uuid4.spy()

Switch from mocked to real UUIDs mid-test:

def test_start_mocked_then_spy(mock_uuid):
    """Start with mocked UUIDs, then switch to real ones."""
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    first = uuid.uuid4()  # Mocked

    mock_uuid.uuid4.spy()  # Switch to spy mode
    second = uuid.uuid4()  # Real random UUID

    assert str(first) == "12345678-1234-4678-8234-567812345678"
    assert first != second  # second is random
    assert mock_uuid.uuid4.mocked_count == 1
    assert mock_uuid.uuid4.real_count == 1

When to use which: Use spy_uuid when you never need mocking in the test. Use mock_uuid.uuid4.spy() when you need to switch between mocked and real UUIDs within the same test.

Additional UUID Versions

In addition to uuid4, pytest-uuid supports mocking all UUID versions:

UUID1 (Time-based with MAC)

def test_uuid1(mock_uuid):
    mock_uuid.uuid1.set("12345678-1234-1234-8234-567812345678")
    assert str(uuid.uuid1()) == "12345678-1234-1234-8234-567812345678"

def test_uuid1_seeded(mock_uuid):
    mock_uuid.uuid1.set_seed(42)
    first = uuid.uuid1()
    mock_uuid.uuid1.reset()
    mock_uuid.uuid1.set_seed(42)
    assert uuid.uuid1() == first

def test_uuid1_fixed_node(mock_uuid):
    # Return real uuid1 values with a fixed node (MAC address)
    mock_uuid.uuid1.set_node(0x123456789ABC)
    result = uuid.uuid1()
    assert result.node == 0x123456789ABC

UUID3 and UUID5 (Namespace-based)

UUID3 (MD5) and UUID5 (SHA-1) are deterministic - the same namespace and name always produce the same UUID. These functions are tracked in spy mode only:

def test_uuid3_tracking(mock_uuid):
    _ = mock_uuid.uuid3  # Initialize spy
    result = uuid.uuid3(uuid.NAMESPACE_DNS, "example.com")

    assert mock_uuid.uuid3.call_count == 1
    assert mock_uuid.uuid3.calls[0].namespace == uuid.NAMESPACE_DNS
    assert mock_uuid.uuid3.calls[0].name == "example.com"

def test_uuid5_filtering(mock_uuid):
    _ = mock_uuid.uuid5
    uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
    uuid.uuid5(uuid.NAMESPACE_URL, "https://example.com")

    dns_calls = mock_uuid.uuid5.calls_with_namespace(uuid.NAMESPACE_DNS)
    assert len(dns_calls) == 1

UUID6, UUID7, UUID8 (RFC 9562)

These newer UUID versions require Python 3.14+ or the uuid6 backport package:

from uuid6 import uuid6, uuid7, uuid8  # or from uuid import ... on Python 3.14+

def test_uuid7(mock_uuid):
    mock_uuid.uuid7.set("12345678-1234-7234-8234-567812345678")
    result = uuid7()
    assert str(result) == "12345678-1234-7234-8234-567812345678"

def test_uuid7_seeded(mock_uuid):
    mock_uuid.uuid7.set_seed(42)
    first = uuid7()
    mock_uuid.uuid7.reset()
    mock_uuid.uuid7.set_seed(42)
    assert uuid7() == first

def test_all_versions_independent(mock_uuid):
    """Each UUID version is mocked independently."""
    mock_uuid.uuid4.set("44444444-4444-4444-8444-444444444444")  # uuid4
    mock_uuid.uuid1.set("11111111-1111-1111-8111-111111111111")
    mock_uuid.uuid7.set("77777777-7777-7777-8777-777777777777")

    assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
    assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"
    assert str(uuid7()) == "77777777-7777-7777-8777-777777777777"

    # Call counts are tracked independently
    assert mock_uuid.uuid4.call_count == 1      # uuid4
    assert mock_uuid.uuid1.call_count == 1
    assert mock_uuid.uuid7.call_count == 1

Note: uuid6/uuid7/uuid8 require the uuid6 package on Python < 3.14. Install with pip install uuid6 or it will be installed automatically as a dependency.

Ignoring Modules

Exclude specific packages from UUID mocking so they receive real UUIDs. This is useful for third-party libraries like SQLAlchemy or Celery that need real UUIDs for internal operations.

Fixture API

def test_with_ignored_modules(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    mock_uuid.uuid4.set_ignore("sqlalchemy", "celery")

    # Direct calls are mocked
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

    # Calls from sqlalchemy/celery get real UUIDs
    # (the ignore check walks the entire call stack)

Decorator/Marker API

@freeze_uuid4("12345678-1234-4678-8234-567812345678", ignore=["sqlalchemy"])
def test_with_decorator():
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

@pytest.mark.freeze_uuid4("...", ignore=["celery"])
def test_with_marker():
    pass

How it works: The ignore check inspects the entire call stack, not just the immediate caller. If any frame in the call chain is from an ignored module, real UUIDs are returned. This handles cases where your code calls a library that internally calls uuid.uuid4().

Tracking Ignored Calls

def test_tracking(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    mock_uuid.uuid4.set_ignore("mylib")

    uuid.uuid4()           # mocked
    mylib.create_record()  # real (from ignored module)

    assert mock_uuid.uuid4.mocked_count == 1
    assert mock_uuid.uuid4.real_count == 1

Global Configuration

Configure default behavior for all tests via pyproject.toml:

# pyproject.toml
[tool.pytest_uuid]
default_ignore_list = ["sqlalchemy", "celery"]
extend_ignore_list = ["myapp.internal"]
default_exhaustion_behavior = "raise"

Or programmatically in conftest.py:

# conftest.py
import pytest_uuid

pytest_uuid.configure(
    default_ignore_list=["sqlalchemy", "celery"],
    extend_ignore_list=["myapp.internal"],
    default_exhaustion_behavior="raise",
)

Default Ignore List: By default, botocore is in the ignore list. This prevents pytest-uuid from interfering with AWS SDK operations that use uuid.uuid4() internally for idempotency tokens. Use extend_ignore_list to add more packages, or set default_ignore_list to override completely.

Module-Specific Mocking

For granular control, use mock_uuid_factory:

# myapp/models.py
from uuid import uuid4

def create_user():
    return {"id": str(uuid4()), "name": "John"}

# tests/test_models.py
def test_create_user(mock_uuid_factory):
    with mock_uuid_factory("myapp.models") as mocker:
        mocker.uuid4.set("12345678-1234-4678-8234-567812345678")
        user = create_user()
        assert user["id"] == "12345678-1234-4678-8234-567812345678"

Context Manager

Use freeze functions as context managers:

from pytest_uuid import freeze_uuid4

def test_context_manager():
    with freeze_uuid4("12345678-1234-4678-8234-567812345678"):
        assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

    # Original uuid.uuid4 is restored
    assert uuid.uuid4() != uuid.UUID("12345678-1234-4678-8234-567812345678")

Bring Your Own Randomizer

Pass a random.Random instance for full control:

import random
from pytest_uuid import freeze_uuid4

rng = random.Random(42)
rng.random()  # Advance the state

@freeze_uuid4(seed=rng)
def test_custom_rng():
    # Gets UUIDs from the pre-advanced random state
    result = uuid.uuid4()

Scoped Mocking

Module-Level

Apply to all tests in a module using pytest's pytestmark:

# tests/test_user_creation.py
import uuid
import pytest

pytestmark = pytest.mark.freeze_uuid4("12345678-1234-4678-8234-567812345678")


def test_create_user():
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"


def test_create_another_user():
    assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

Class-Level

Apply the decorator to a test class to freeze UUIDs for all test methods:

import uuid
from pytest_uuid import freeze_uuid4


@freeze_uuid4("12345678-1234-4678-8234-567812345678")
class TestUserService:
    def test_create(self):
        assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

    def test_update(self):
        assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"

Or use the marker:

import uuid
import pytest


@pytest.mark.freeze_uuid4(seed=42)
class TestSeededService:
    def test_one(self):
        result = uuid.uuid4()
        assert result.version == 4

    def test_two(self):
        result = uuid.uuid4()
        assert result.version == 4

Session-Level

For session-wide mocking, use a session-scoped autouse fixture in conftest.py:

# conftest.py
import pytest
from pytest_uuid import freeze_uuid4


@pytest.fixture(scope="session", autouse=True)
def freeze_uuids_globally():
    with freeze_uuid4(seed=12345):
        yield

API Reference

Fixtures

mock_uuid

Container fixture providing access to mockers for all UUID versions.

Properties (version-specific mockers):

  • mock_uuid.uuid4 - UUID4Mocker for uuid.uuid4() (see methods below)
  • mock_uuid.uuid1 - UUID1Mocker for uuid.uuid1() with set(), set_seed(), set_node(), set_clock_seq()
  • mock_uuid.uuid3 - NamespaceUUIDSpy for uuid.uuid3() (spy-only, tracks namespace/name)
  • mock_uuid.uuid5 - NamespaceUUIDSpy for uuid.uuid5() (spy-only, tracks namespace/name)
  • mock_uuid.uuid6 - UUID6Mocker for uuid.uuid6() with set(), set_seed(), set_node() (requires uuid6 package)
  • mock_uuid.uuid7 - UUID7Mocker for uuid.uuid7() with set(), set_seed() (requires uuid6 package)
  • mock_uuid.uuid8 - UUID8Mocker for uuid.uuid8() with set(), set_seed() (requires uuid6 package)

Container Methods:

  • reset() - Reset all initialized sub-mockers to initial state

UUID4Mocker Methods (accessed via mock_uuid.uuid4):

  • set(*uuids) - Set one or more UUIDs to return (cycles by default)
  • set_default(uuid) - Set a default UUID for all calls
  • set_seed(seed) - Set a seed for reproducible generation
  • set_seed_from_node() - Use test node ID as seed
  • set_exhaustion_behavior(behavior) - Set behavior when sequence exhausted
  • spy() - Switch to spy mode (return real UUIDs while still tracking)
  • reset() - Reset to initial state
  • set_ignore(*module_prefixes) - Set modules to ignore (returns real UUIDs)

mock_uuid_factory

Factory for module-specific mocking.

with mock_uuid_factory("module.path") as mocker:
    mocker.uuid4.set("...")

spy_uuid

Spy fixture that tracks uuid.uuid4() calls without mocking them.

def test_spy(spy_uuid):
    result = uuid.uuid4()  # Returns real random UUID

    assert spy_uuid.call_count == 1
    assert spy_uuid.last_uuid == result

Properties:

  • call_count - Number of times uuid4 was called
  • generated_uuids - List of all generated UUIDs
  • last_uuid - Most recently generated UUID
  • calls - List of UUIDCall records with metadata

Methods:

  • reset() - Reset tracking data
  • calls_from(module_prefix) - Filter calls by module prefix

Call Tracking

Both mock_uuid and spy_uuid fixtures provide detailed call tracking via the UUIDCall dataclass:

from pytest_uuid.types import UUIDCall

def test_call_tracking(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    uuid.uuid4()

    call = mock_uuid.uuid4.calls[0]
    assert call.uuid == uuid.UUID("12345678-1234-4678-8234-567812345678")
    assert call.was_mocked is True
    assert call.caller_module is not None
    assert call.caller_file is not None

UUIDCall Fields:

  • uuid - The UUID that was returned
  • was_mocked - True if mocked, False if real (spy mode or ignored module)
  • uuid_version - The UUID version (1, 3, 4, 5, 6, 7, or 8)
  • caller_module - Name of the module that made the call
  • caller_file - File path where the call originated
  • caller_line - Line number of the call
  • caller_function - Function name where the call originated
  • caller_qualname - Qualified name (e.g., MyClass.method or outer.<locals>.inner)

Tracking Properties (available on mock_uuid.uuid4 and spy_uuid):

  • call_count - Total number of uuid4 calls
  • generated_uuids - List of all UUIDs returned
  • last_uuid - Most recently returned UUID
  • calls - List of UUIDCall records with full metadata

Additional mock_uuid.uuid4 Properties:

  • mocked_calls - Only calls that returned mocked UUIDs
  • real_calls - Only calls that returned real UUIDs (spy mode or ignored modules)
  • mocked_count - Number of mocked calls
  • real_count - Number of real calls

Interrogating Multiple Calls

def test_interrogate_calls(mock_uuid):
    """Inspect detailed metadata for all uuid4 calls."""
    mock_uuid.uuid4.set(
        "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
        "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
    )

    first = uuid.uuid4()
    second = uuid.uuid4()

    # Check all UUIDs generated
    assert len(mock_uuid.uuid4.generated_uuids) == 2
    assert mock_uuid.uuid4.generated_uuids[0] == first
    assert mock_uuid.uuid4.generated_uuids[1] == second

    # Get the last UUID quickly
    assert mock_uuid.uuid4.last_uuid == second

    # Iterate through call details
    for i, call in enumerate(mock_uuid.uuid4.calls):
        print(f"Call {i}: {call.uuid}")
        print(f"  Module: {call.caller_module}")
        print(f"  File: {call.caller_file}")
        print(f"  Mocked: {call.was_mocked}")

Distinguishing Mocked vs Real Calls

def test_mixed_mocked_and_real(mock_uuid):
    """Track both mocked calls and real calls from ignored modules."""
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
    mock_uuid.uuid4.set_ignore("mylib")

    uuid.uuid4()              # Mocked (direct call)
    mylib.create_record()     # Real (from ignored module)
    uuid.uuid4()              # Mocked (direct call)

    # Count by type
    assert mock_uuid.uuid4.call_count == 3
    assert mock_uuid.uuid4.mocked_count == 2
    assert mock_uuid.uuid4.real_count == 1

    # Access only real calls
    for call in mock_uuid.uuid4.real_calls:
        print(f"Real UUID from {call.caller_module}: {call.uuid}")

    # Access only mocked calls
    for call in mock_uuid.uuid4.mocked_calls:
        assert call.was_mocked is True

Filtering Calls by Module

def test_filter_calls(mock_uuid):
    mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")

    uuid.uuid4()  # Call from test module
    mymodule.do_something()  # Calls uuid4 internally

    # Filter calls by module prefix
    test_calls = mock_uuid.uuid4.calls_from("tests")
    module_calls = mock_uuid.uuid4.calls_from("mymodule")

    # Useful for verifying specific modules made expected calls
    assert len(module_calls) == 1

Decorator/Context Manager

Version-Specific Freeze Functions

The recommended API uses version-specific functions for clarity:

from pytest_uuid import freeze_uuid4, freeze_uuid1, freeze_uuid6, freeze_uuid7, freeze_uuid8

# uuid4 (most common)
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_static(): ...

@freeze_uuid4(seed=42)
def test_seeded(): ...

# uuid1 with node/clock_seq
@freeze_uuid1(seed=42, node=0x123456789ABC)
def test_uuid1(): ...

# uuid7 (requires uuid6 package or Python 3.14+)
@freeze_uuid7(seed=42)
def test_uuid7(): ...

# Context manager
with freeze_uuid4("...") as freezer:
    result = uuid.uuid4()
    freezer.reset()

# Stack multiple versions
with freeze_uuid4("..."), freeze_uuid7(seed=42):
    uuid.uuid4()  # frozen
    uuid7()       # frozen with seed

Available Functions:

  • freeze_uuid4 - Freeze uuid.uuid4() calls
  • freeze_uuid1 - Freeze uuid.uuid1() calls (supports node, clock_seq parameters)
  • freeze_uuid6 - Freeze uuid.uuid6() calls (requires uuid6 package, supports node, clock_seq)
  • freeze_uuid7 - Freeze uuid.uuid7() calls (requires uuid6 package)
  • freeze_uuid8 - Freeze uuid.uuid8() calls (requires uuid6 package)
  • freeze_uuid - Backward-compatible alias for freeze_uuid4

Parameters:

  • uuids - UUID(s) to return (string, UUID, or sequence)
  • seed - Integer, random.Random, or "node" for reproducible generation
  • on_exhausted - "cycle", "random", or "raise"
  • ignore - Module prefixes to exclude from patching
  • ignore_defaults - If False, don't include the default ignore list (default: True)
  • node - (uuid1/uuid6 only) Fixed 48-bit node (MAC address) for seeded generation
  • clock_seq - (uuid1/uuid6 only) Fixed 14-bit clock sequence for seeded generation

Markers

Version-specific markers correspond to the freeze functions:

# uuid4 markers
@pytest.mark.freeze_uuid4("uuid")
@pytest.mark.freeze_uuid4(seed=42)
@pytest.mark.freeze_uuid4(seed="node")

# uuid1 markers
@pytest.mark.freeze_uuid1("uuid")
@pytest.mark.freeze_uuid1(seed=42, node=0x123456789ABC)

# uuid7 markers
@pytest.mark.freeze_uuid7(seed=42)

# Stack multiple version markers
@pytest.mark.freeze_uuid4("44444444-4444-4444-8444-444444444444")
@pytest.mark.freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple():
    uuid.uuid4()  # returns 44444444-...
    uuid.uuid1()  # returns 11111111-...

# Backward-compatible (alias for freeze_uuid4)
@pytest.mark.freeze_uuid("uuid")

Configuration

import pytest_uuid

pytest_uuid.configure(
    default_ignore_list=["package1", "package2"],
    extend_ignore_list=["package3"],
    default_exhaustion_behavior="raise",
)

References

Development

This project uses uv for package management and just as a command runner.

Prerequisites

# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install just (macOS)
brew install just

Setup

git clone https://github.com/CaptainDriftwood/pytest-uuid.git
cd pytest-uuid
just sync

Available Commands

just              # List all commands
just test         # Run tests
just test-cov     # Run tests with coverage
just nox          # Run tests across all Python versions with nox
just nox 3.12     # Run tests for a specific Python version
just lint         # Run linting
just format       # Format code
just check        # Run all checks
just build        # Build the package

Coverage with Pytester

This project uses pytester for integration testing. Getting accurate coverage for pytest plugins requires special handling because plugins are imported before coverage can start measuring.

The Problem:

When running pytest --cov=pytest_uuid, the plugin is loaded when pytest starts—before pytest-cov begins measuring. This causes incomplete coverage and the warning:

CoverageWarning: Module pytest_uuid was previously imported, but not measured

The Solution:

Use coverage run -m pytest instead of pytest --cov:

# Instead of this:
pytest --cov=pytest_uuid --cov-report=term-missing

# Use this:
coverage run -m pytest
coverage combine
coverage report --show-missing

This works because coverage run starts measuring before Python imports anything, so the plugin import is captured.

Configuration (pyproject.toml):

[tool.coverage.run]
source = ["src/pytest_uuid"]
branch = true
parallel = true           # Required for combining coverage files
patch = ["subprocess"]    # Enables coverage in subprocesses
sigterm = true            # Ensures coverage is saved on SIGTERM

Why parallel = true?

When coverage patches subprocesses, each subprocess writes its own .coverage.<hostname>.<pid>.<random> file. The coverage combine command merges these into a single .coverage file for reporting.

References:

License

MIT License - see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_uuid-1.0.0.tar.gz (51.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_uuid-1.0.0-py3-none-any.whl (49.5 kB view details)

Uploaded Python 3

File details

Details for the file pytest_uuid-1.0.0.tar.gz.

File metadata

  • Download URL: pytest_uuid-1.0.0.tar.gz
  • Upload date:
  • Size: 51.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytest_uuid-1.0.0.tar.gz
Algorithm Hash digest
SHA256 4bf6bcf0be4b39fdc3fc961020a408e11f62269a68746b3ef8fc04efe899cef3
MD5 52b3276a2c11e7d58c9beda218011f0e
BLAKE2b-256 5d2ae895a27bb07936661b60777846a2569335244459984ac7d8586fd4a866ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_uuid-1.0.0.tar.gz:

Publisher: publish.yml on CaptainDriftwood/pytest-uuid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_uuid-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pytest_uuid-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 49.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytest_uuid-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a18098e6b7d9ad0ce84bc7f015425f48c5a5cdd4fdc54171f8de79eeb1bf0b00
MD5 2c62f29aa69d5cb6b3319c240969c265
BLAKE2b-256 dc1b85179c602887729b0edc1af548a7a1b28cdd1a93d5b4bac0fa0ceee69ead

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_uuid-1.0.0-py3-none-any.whl:

Publisher: publish.yml on CaptainDriftwood/pytest-uuid

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page