An execution entry point without import side effects
A support module should be importable without sending reports or starting jobs. Keep reusable functions separate from main and call main under the __name__ == "__main__" guard. Also define the process contract: messages on intended channels and an integer status the scheduler can interpret. A string returned by main and passed to sys.exit does not mean success; it produces status 1 and text on stderr. With argparse, invalid input is normally rejected before execution with status 2. In an exercise, check import behavior, valid arguments, invalid arguments, and work completion separately.
Test an obligation that can fail
Before writing a test, identify the observable obligation. If a quantity must be an integer between zero and one hundred, cover zero, one hundred, and values immediately outside the limits alongside an ordinary case. Use assertRaises to require an exception; a try that simply ignores ValueError can also pass when nothing is rejected. Test the result and permitted effects, including absence of sending when validation fails. A green test establishes only conditions actually exercised. Record boundaries that still depend on integration, actual concurrency, or data of representative size.
Replace the dependency actually used
A patch must reach the name the code looks up. If pipeline imported deliver directly, replacing only sender.deliver after that import can leave pipeline holding the earlier reference. Use a local sentinel to demonstrate which function was called without sending network data. Autospec helps detect incompatible arguments but does not establish response schema. Configure returns, exceptions, and sequences consistent with the contract. In a retry test, check each call’s identity, the attempt limit, and the reported result. Deduplication in an external service requires its own documentation and evidence.
Independent fixtures and predictable cleanup
Each test should control the resources and state it changes. Register addCleanup as soon as a resource needing release is acquired; these cleanups can run even when setUp fails and tearDown is not called. Use fixture-owned directories and objects without deleting other processes’ resources. If global configuration changes, restore it or redesign the dependency to receive explicit configuration. Running a test alone and in a different order helps reveal contamination. Fixing a favorable order or repeating until green does not establish independence and may hide precisely the behavior the suite should detect.
Reproduce the environment and interpret observability
A virtual environment should not be treated as a portable folder between machines. Recreate it using the intended interpreter and dependencies, then exercise the scheduler invocation. Establish paths, working directory, and configuration without depending on manual terminal activation. In logs, distinguish an operation from each emission of its event. A child logger handler and a root handler, with propagation enabled to the same destination, can produce repeated lines. Before compensating a delivery, check its reference and recipient state. Then correct logging configuration and test the expected number of emissions.
from unittest.mock import create_autospec
def send(batch_id, *, timeout):
raise RuntimeError("external client not configured")
def deliver(batch_id, client):
response = client(batch_id, timeout=2)
if not isinstance(response, dict) or response.get("accepted") is not True:
raise ValueError("invalid acceptance response")
return batch_id
client = create_autospec(send, return_value={"accepted": True})
assert deliver("TRAIN-204", client) == "TRAIN-204"
client.assert_called_once_with("TRAIN-204", timeout=2)
client.return_value = {"status": "accepted"}
try:
deliver("TRAIN-205", client)
except ValueError:
print("response contract rejected")
else:
raise AssertionError("invalid response accepted")Run the local example: no network client is configured. The mock respects the signature, but the second response has a different schema and must be rejected. Add an assertRaises test and confirm autospec rejects a positional timeout. This exercise establishes local decisions, not operation of an external service.
Common pitfalls
Tests without assertions; patching the wrong namespace; treating autospec as payload validation; global state left by fixtures; assuming tearDown after failed setUp; reporting success using a string; inferring duplication solely from logs.
Related topics: Small functions, clear contracts · Errors and files with context · Observable and repeatable automation · Subprocesses, paths, and verifiable results
A useful test fails when an obligation is violated, controls its own effects, and makes the boundary between local evidence and external behavior explicit.
Reference: Python 3.14: Unit testing framework · Python 3.14; DR Python 2026.3