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.
Python code
28 linesimport 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
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
- Use `unittest.mock.patch` to replace the backend globally, e.g., in a build script.
- 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
More from Modern tooling
- Build a Recipe Runner Mock in Python easy
- Build a Textual TUI App Skeleton in Python easy
- Configure ruff linter rules in pyproject.toml with Python easy
- Data Conversion Helper Functions in Python easy
- How to Bind and Mock structlog Context in Python medium
- How to Build a Chainable Filter Helper in Python easy
Keep learning
Related tutorials and quizzes for this topic.