Skip to content

Spy Mode

Spy mode tracks uuid.uuid4() calls without mocking them. This is useful when you need to verify UUID generation happens without controlling the output.

spy_uuid Fixture

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

Switching to Spy Mode

Use mock_uuid.uuid4.spy() to switch from mocked to real UUIDs mid-test:

import uuid

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.

Call Tracking

Both fixtures provide detailed call tracking:

import uuid

def test_call_tracking(spy_uuid):
    first = uuid.uuid4()
    second = uuid.uuid4()

    assert spy_uuid.call_count == 2
    assert spy_uuid.generated_uuids == [first, second]
    assert spy_uuid.last_uuid == second

Call Details

Access detailed metadata for each call:

import uuid

def test_call_details(spy_uuid):
    uuid.uuid4()

    call = spy_uuid.calls[0]
    assert call.uuid is not None
    assert call.was_mocked is False
    assert call.caller_module is not None
    assert call.caller_file is not None

Filtering Calls by Module

import uuid

def test_filter_calls(spy_uuid):
    uuid.uuid4()  # Call from test module
    mymodule.do_something()  # Calls uuid4 internally

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

    assert len(test_calls) == 1
    assert len(module_calls) == 1

Properties

Property Description
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

Method Description
reset() Reset tracking data
calls_from(module_prefix) Filter calls by module prefix

Distinguishing Mocked vs Real Calls

When using mock_uuid with ignored modules, track both types:

import uuid

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

UUIDCall Dataclass

The UUIDCall dataclass contains:

Field Description
uuid The UUID that was returned
was_mocked True if mocked, False if real
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)

Thread Safety

Call tracking is fully thread-safe. If your test code spawns multiple threads that call UUID functions concurrently, all calls will be accurately tracked:

import threading
import uuid

def test_concurrent_tracking(spy_uuid):
    """Thread-safe call tracking with concurrent UUID generation."""
    def generate_uuids():
        for _ in range(10):
            uuid.uuid4()

    threads = [threading.Thread(target=generate_uuids) for _ in range(4)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert spy_uuid.call_count == 40  # All calls tracked accurately

Each tracking operation uses per-instance locks to ensure consistent counting and metadata recording.