Pattern Comparison and Testing Strategies¶
Simple English (B1). Comparisons help you pick a pattern. Testing proves the boundaries behave as designed.
Pattern Comparison¶
Strategy vs State¶
| Aspect | Strategy | State |
|---|---|---|
| What changes | The algorithm used | Behavior by lifecycle phase |
| Who picks | Caller passes a strategy | Object moves itself by rules |
| Example | Payment provider plug-in | Order: Pending → Paid → Shipped |
Factory vs Builder¶
| Aspect | Factory | Builder |
|---|---|---|
| Job | Create one product type | Build one complex object step by step |
| When | Runtime type from config | Many optional parts, fluent setup |
Decorator vs Inheritance¶
| Aspect | Decorator | Inheritance |
|---|---|---|
| Binding | Runtime, stackable | Compile-time class tree |
| When | Add behavior around existing object | Clear IS-A and Liskov rules |
Adapter vs Facade¶
| Aspect | Adapter | Facade |
|---|---|---|
| Goal | Match a foreign interface | Hide a complex subsystem |
| When | Legacy or third-party API | One entry for many calls |
Observer vs Pub/Sub¶
| Aspect | Observer | Pub/Sub |
|---|---|---|
| Links | Subject knows observers | Producers and consumers know only the bus |
| Scope | Usually one process | Often many processes or services |
Testing Strategies for Patterns¶
Unit Tests¶
Test one unit. Replace dependencies with fakes or mocks. The pattern boundary is often a mock boundary.
from unittest.mock import MagicMock
def test_order_service_calls_repository() -> None:
repo = MagicMock()
svc = OrderService(repo=repo)
svc.place_order("ORD-1", 99.0)
repo.save.assert_called_once()
Rule: mock the port (interface), not random internals.
Integration Tests¶
Use real collaborators for the slice you care about. No mocks across that slice.
def test_place_order_persists(real_db_session) -> None:
repo = SQLAlchemyOrderRepository(real_db_session)
svc = OrderService(repo=repo)
svc.place_order("ORD-1", 99.0)
assert repo.find_by_id("ORD-1") is not None
Contract Tests¶
Consumer and producer agree on the API shape. Tools like Pact record expectations and verify in CI.
Property-Based Tests¶
Many random inputs; check invariants (e.g. amount always positive).
from hypothesis import given
from hypothesis import strategies as st
@given(amount=st.floats(min_value=0.01, max_value=1_000_000, allow_nan=False))
def test_order_amount_valid(amount: float) -> None:
order = Order(id="x", amount=amount)
assert order.is_valid() is True
Mutation Testing¶
Tools change your code (== to !=, delete lines). If tests still pass, coverage is weak. Use scores as a guide, not a religion.
Decision Hints (Not Rules)¶
- Many algorithms, one interface → Strategy.
- Unknown object type at runtime (config/input) → Factory.
- Object phases with different rules → State.
- One complex build with options → Builder.
- Add optional cross-cutting behavior at runtime → Decorator.
- One producer, many in-process listeners → Observer.
- Request passes ordered handlers (auth, limit, validation) → Chain.
- Unknown external API → Adapter.
- Many internal calls, one call for clients → Facade.
Pair each pattern with the right test level: unit for pure logic, integration for real I/O, contracts for API stability.