How to Mock a PEP 517 Build Backend in Python

Use unittest.mock.Mock to simulate a PEP 517 backend interface, stub build hooks, and verify calls for package build automation.

Medium Python 3.9+ Aug 9, 2026 Modern tooling 14 views 0 copies

Python code

28 lines
Python 3.9+
import json
from unittest.mock import Mock

# Simulate a PEP 517 backend interface
class Pep517Backend:
    def build_wheel(self, wheel_directory, config_settings=None, metadata_directory=None):
        return f"{wheel_directory}/mock_package-1.0.0-py3-none-any.whl"

    def get_requires_for_build_wheel(self, config_settings=None):
        return ["setuptools>=40.8.0"]

# Mock the backend for testing without actual invocation
backend = Pep517Backend()
mock_backend = Mock(wraps=backend)

# Simulate the PEP 517 build hook calls
wheel_dir = "/tmp/dist"
mock_backend.get_requires_for_build_wheel.return_value = ["mock-extra"]
mock_backend.build_wheel.return_value = f"{wheel_dir}/mock_package-1.0.0-py3-none-any.whl"

if __name__ == "__main__":
    requires = mock_backend.get_requires_for_build_wheel()
    wheel = mock_backend.build_wheel(wheel_dir, config_settings={"pure": True})

    print("Requirements:", requires)
    print("Built wheel:", wheel)
    print("Calls:", mock_backend.build_wheel.call_count)
    print("Call args:", mock_backend.build_wheel.call_args)

Output

stdout
Requirements: ['mock-extra']
Built wheel: /tmp/dist/mock_package-1.0.0-py3-none-any.whl
Calls: 1
Call args: call('/tmp/dist', config_settings={'pure': True})

How it works

The Mock object wraps the real Pep517Backend class, retaining method signatures and attribute access while allowing you to stub return values. Setting return_value on the mock’s methods overrides the actual implementation, so build_wheel returns the expected wheel path without performing a real build. The call_count and call_args attributes let you assert that the hook was invoked exactly once and with the right arguments — essential for testing build tooling. Because the mock wraps the original, un-stubbed methods still run the real logic, giving flexibility in hybrid tests.

Common mistakes

  • Forgetting to set `return_value` on the mock’s methods, so it returns a Mock instead of a string.
  • Using `Mock()` without `wraps` loses real method signatures, making tests less realistic.
  • Not resetting mocks between test cases, leading to inflated `call_count` assertions.

Variations

  1. Use `unittest.mock.patch` to replace the backend globally, e.g., in a build script.
  2. Create a simple fake class with `__call__` to mimic the hook instead of a full Mock object.

Real-world use cases

  • Testing a build orchestrator that invokes PEP 517 hooks without needing a real packaging environment.
  • Verifying that a CI pipeline passes the correct config settings to `build_wheel`.
  • Simulating a backend that returns extra requirements to validate dependency resolution logic.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Modern tooling

Related tutorials and quizzes for this topic.