Interface Design for Testability
Good interfaces make testing natural:
-
Accept dependencies, don’t create them
Depend on a Port (ABC), not a concrete adapter. Inject at construction time.
# Testable: dependency is injected, can be swapped in tests class OrderService: def __init__(self, payment_gateway: PaymentGatewayPort) -> None: self._payment_gateway = payment_gateway # Hard to test: creates its own dependency internally class OrderService: def __init__(self) -> None: self._payment_gateway = StripeGateway(settings.STRIPE_KEY) -
Return results, don’t produce side effects
# Testable: assert on the return value def calculate_discount(cart: Cart) -> Discount: ... # Hard to test: must inspect state after the call def apply_discount(cart: Cart) -> None: cart.total -= _compute_discount(cart) -
Small surface area
- Fewer methods = fewer tests needed
- Fewer params = simpler test setup
- Hide complexity behind
_prefixed helpers (see deep-modules.md)