Learn the concept
Composition builds an object from collaborators instead of making a deep inheritance hierarchy. A document service can use a repository and a model client without inheriting from either. This keeps responsibilities separate and makes replacement possible.
A protocol describes the operations a collaborator must provide for type checking. It does not automatically validate data or prove that an implementation behaves correctly. Tests still need to exercise the contract.
Dependency injection simply means supplying collaborators from outside instead of creating them invisibly inside the function. A fake client can then replace a paid model during tests. Keep the fake faithful to error behavior as well as successful results, otherwise tests only prove the happy path.
Run and inspect
from typing import Protocol
class Reader(Protocol):
def read(self, identifier: str) -> str: ...
class FakeReader:
def read(self, identifier: str) -> str:
return f"document:{identifier}"
def load(reader: Reader, identifier: str) -> str:
return reader.read(identifier)
assert load(FakeReader(), "d1") == "document:d1"
Your exercise
Inject a reader into a processing function. Add fake implementations for success, missing content, and timeout.
Check your understanding
Business logic can be tested without network access, and each fake failure produces the intended application outcome.