Skip to main content

A strict, type-safe mock library for Python with full IDE autocomplete support.

Project description

tmock

Type-safe mocking for modern Python.

tmock is a mocking library designed to keep your tests aligned with your actual code. It prioritizes type safety and signature adherence over the infinite flexibility of standard "magic" mocks.

Why tmock?

The standard library's unittest.mock is incredibly flexible. However, that flexibility can sometimes be a liability. A MagicMock will happily accept arguments that don't exist in your function signature, or types that would cause your real code to crash.

The Trade-off:

  • unittest.mock: Optimizes for ease of setup. If you change a method signature in your code, your old tests often keep passing silently, only for the code to fail in production.
  • tmock: Optimizes for correctness. It reads the type hints and signatures of your actual classes. If you try to stub a mock with the wrong arguments or types, the test fails immediately.

Scenario: The Silent Drift

Imagine you refactor a method from save(data) to save(data, should_commit).

  1. With Standard Mocks: Your existing test calls mock.save(data). The mock accepts it without complaint. The test passes, but it's no longer testing the reality of your API.
  2. With tmock: When you run the test, tmock validates the call against the new signature. It notices discrepancies immediately, forcing you to update your test to match the new code structure.

Key Features

1. Runtime Type Validation

This is the core differentiator of tmock. It doesn't just count arguments; it checks their types against your source code's annotations.

If your method is defined as:

def update_score(self, user_id: int, score: float) -> bool: ...

Trying to stub it with incorrect types raises an error before the test even runs:

# RAISES ERROR: TypeError: Argument 'user_id' expected int, got str
given().call(mock.update_score("user_123", 95.5)).returns(True)

2. Better IDE Support

Because tmock mirrors the structure of your class, it plays much nicer with your editor than dynamic mocks. You get better autocompletion and static analysis support, making it easier to write tests without constantly flipping back to the source file to remember argument names.

3. Native Property & Field Support

Mocking properties or data attributes usually requires verbose __setattr__ patching. tmock handles them natively via its DSL, supporting getters, setters, Dataclasses, and Pydantic models out of the box.


Installation

Install tmock from PyPI using your favorite package manager:

# Using pip
pip install tmock

# Using uv (recommended)
uv add tmock

Quick Demo

Here's a realistic example: testing a NotificationService that depends on an EmailClient and uses a module-level config.

The production code:

# notification_service.py
from myapp.config import MAX_BATCH_SIZE
from myapp.email import EmailClient

class NotificationService:
    def __init__(self, email_client: EmailClient):
        self.client = email_client

    def notify_users(self, user_ids: list[int], message: str) -> int:
        """Send notifications in batches. Returns count of successful sends."""
        sent = 0
        for i in range(0, len(user_ids), MAX_BATCH_SIZE):
            batch = user_ids[i:i + MAX_BATCH_SIZE]
            for user_id in batch:
                if self.client.send(user_id, message):
                    sent += 1
        return sent

Testing with mocks — Use tmock() when you control the dependency and pass it in (dependency injection):

from tmock import tmock, given, verify, any

def test_notify_users_returns_success_count():
    # Create a type-safe mock of the dependency
    client = tmock(EmailClient)

    # Stub the send method - first call succeeds, second fails
    given().call(client.send(1, "Hello")).returns(True)
    given().call(client.send(2, "Hello")).returns(False)

    # Inject the mock and test
    service = NotificationService(client)
    result = service.notify_users([1, 2], "Hello")

    assert result == 1  # Only one succeeded
    verify().call(client.send(any(int), "Hello")).times(2)

Testing with patches — Use tpatch when you need to replace something you don't control, like a module variable or a function called internally:

from tmock import tpatch

def test_notify_users_respects_batch_size():
    client = tmock(EmailClient)
    given().call(client.send(any(int), any(str))).returns(True)

    # Patch the module variable to force smaller batches
    with tpatch.module_var("path.to.notification_service.MAX_BATCH_SIZE", 2):
        service = NotificationService(client)
        service.notify_users([1, 2, 3, 4, 5], "Hi")

        # Verify all 5 emails were sent despite small batch size
        verify().call(client.send(any(int), "Hi")).times(5)

Usage Guide

Creating a Mock

The entry point is simple. Pass your class to tmock to create a strict proxy.

from tmock import tmock, given, verify, any
from my_app import Database

db = tmock(Database)

Stubbing (The given DSL)

Define stubs before calling. Unlike unittest.mock, tmock requires you to declare behavior upfront—calling an unstubbed method raises TMockUnexpectedCallError.

# Simple return value
given().call(db.get_user(123)).returns({"name": "Alice"})

# Using Matchers for loose constraints
given().call(db.save_record(any(dict))).returns(True)

# Raising exceptions to test error handling
given().call(db.connect()).raises(ConnectionError("Timeout"))

# Dynamic responses
given().call(db.calculate(10)).runs(lambda args: args.get_by_name("val") * 2)

# Returning a mock from a stubbed method (for chained dependencies)
session = tmock(Session)
given().call(factory.create_session()).returns(session)
given().call(session.execute(any(str))).returns([{"id": 1}])

Verification (The verify DSL)

Assert that specific interactions occurred.

# Verify exact call count
verify().call(db.save_record(any())).once()

# Verify something never happened
verify().call(db.delete_all()).never()

# Verify with specific counts
verify().call(db.connect()).times(3)

Working with Fields and Properties

Stub and verify state changes on attributes, properties, or Pydantic fields.

# Stubbing a value retrieval
given().get(db.is_connected).returns(True)

# Stubbing a value assignment
given().set(db.timeout, 5000).returns(None)

# Verifying a setter was called
verify().set(db.timeout, 5000).once()

Patching (tpatch)

When you need to swap out objects internally used by other modules, use tpatch. It wraps unittest.mock.patch but creates a typed tmock interceptor instead of a generic mock.

The API forces you to be explicit about what you are patching (Method vs. Function vs. Field), which prevents common patching errors.

from tmock import tpatch, given

# Patching an instance method
with tpatch.method(UserService, "get_current_user") as mock:
    given().call(mock()).returns(admin_user)
    
# Patching a module-level function
with tpatch.function("my_module.external_api_call") as mock:
    given().call(mock()).returns(200)

# Patching a class variable
with tpatch.class_var(Config, "MAX_RETRIES") as field:
    given().get(field).returns(1)

Async Support

tmock natively handles async/await. You stub async methods exactly the same way as synchronous ones; tmock handles the coroutine wrapping for you.

# Stubbing an async method
given().call(api_client.fetch_data()).returns(data)

# The mock is automatically awaitable
result = await api_client.fetch_data()

Documentation

  • Stubbing — Define behavior with .returns(), .raises(), .runs()
  • Verification — Assert interactions with .once(), .times(), .never()
  • Argument Matchers — Flexible matching with any() and any(Type)
  • Fields & Properties — Mock getters/setters on dataclasses, Pydantic, properties
  • Patching — Replace real code with tpatch.method(), tpatch.function(), etc.
  • Mocking Functions — Mock standalone functions with tmock(func)
  • Protocols — Mock typing.Protocol interfaces
  • Async Support — Async methods and context managers
  • Reset Functions — Clear stubs and interactions between tests
  • Magic Methods — Context managers, iteration, containers, and more

Project details


Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

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

tmock-0.1.2-py3-none-any.whl (25.0 kB view details)

Uploaded Python 3

File details

Details for the file tmock-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: tmock-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 25.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tmock-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3d8b0076d161effd7b9beade67d0312734de8a7aa192f170c3a0a67f39662c41
MD5 3ad623046885e8f8cce4a4552bffe595
BLAKE2b-256 b604c5791df51ee0e1ed7df61ff790592f9a79d21430761f3cfcee69591252cd

See more details on using hashes here.

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