<< Home

Chapter 14: Testing and Patterns

Design patterns often behave like quiet stagehands in the background of a test suite. They can lift scenes into clearer view or tug ropes so hard that everything wobbles. This chapter explores how patterns shape the structure of tests, how they help or hinder isolation, and how to keep your test code from turning into a cluttered attic of tangled mocks.

Patterns That Improve Testability

Some patterns open windows in your design so that tests can see inside without leaning on awkward scaffolding. These patterns support separation, allow substitution, and provide controlled seams where test objects slide neatly into place. Clear boundaries help a test suite move with grace rather than feeling like a bulky robot trying to tiptoe.

Strategy for Swap Friendly Logic

The Strategy pattern invites you to swap behaviours at runtime. This creates an ideal spot for injecting a stub or fake during a test. When behaviour is externalised like a removable puzzle piece, you can examine a class without dragging the real behaviour along for the ride.

Adapter for Predictable Interfaces

An Adapter gives you a stable surface to grip during tests. Even when the underlying system has quirks, the adapter wraps those quirks in a consistent shape. A test can then focus on the contract presented by the adapter rather than the unpredictable details hidden behind it.

💡 A good adapter lets you write tests that read like small stories rather than long manuals filled with vendor specific oddities.

Patterns That Hinder Testing

Some patterns build walls instead of windows. These designs trap logic in lifetimes that are hard to poke, or they encourage sprawling behaviour that tests struggle to isolate. The result feels like trying to test a clock while all the gears spin at once.

Singleton and Hidden Dependencies

The Singleton pattern promotes a global instance that sticks around like glue. This makes tests run into shared state, which spreads unpredictability. Resetting the world between tests becomes a chore, and subtle interactions creep in like dust between gears.

Facade With Overgrown Responsibilities

A Facade that tries to do everything creates a testing challenge because its behaviour becomes wide and shallow. A test must dance around too many branches at once. This often leads to bloated mocks, which become difficult to maintain.

Dependency Injection for Isolation

Dependency injection lets you slip testing pieces into the gaps between layers. Instead of creating objects deep inside a class, you pass them in from the outside so tests can feed in their own controlled versions. This helps each class become a small island that can be inspected without disturbing the surrounding sea.

Constructor Injection for Stable Tests

Constructor injection provides a clear path for objects that a class relies on. Tests gain the ability to wire predictable fakes in place, which keeps behaviour steady. This approach helps keep surprises out of your test runs.

Setter Injection for Flexible Scenarios

Setter injection gives you a way to slip in dependencies after object creation. This approach can feel more flexible, although you must be careful to avoid leaving objects in half finished states. Tests benefit from the ease of swapping collaborators when trying varied scenarios.

⚠️ Setter based injection only works cleanly if the class guarantees some sensible default or validates that required dependencies are in place before use.

Using Spies With Observer

When events ripple through an Observer structure, spies give you a clear record of what happened. A spy listens like a quiet clerk taking notes. Rather than pretending to perform real behaviour, it collects calls, arguments and invocation order so your test can confirm that notifications flowed correctly.

Tracking Notifications

A spy observer records each event it receives. Later the test can inspect these records to ensure the sequence of calls matches expectations. This helps verify that the subject broadcasts exactly what it should without reordering or skipping steps.

Testing Unsubscribe Behaviour

Observers can come and go like swallows passing over a lake. A spy helps you check that unsubscribed observers truly stop receiving messages. This keeps your system from sending unnecessary signals into empty space.

Testing Template Method Sequences

The Template Method pattern defines a skeleton with customisable steps. Tests must verify that this skeleton is followed correctly. You can use small subclasses or spies inserted into specific hooks to track calls as they unfold. This sequential focus lets you confirm that the template is honoured.

Checking Required Steps

Some steps in a template are required for correctness. A test ensures these steps run in the intended order. By keeping each hook’s behaviour small and clear, the test can examine output without distraction.

Verifying Optional Hooks

Optional hooks allow flexibility. Tests must confirm that they are skipped when unused and invoked when implemented. This keeps behaviour consistent for different subclasses that plug into the same template.

Anti Patterns That Damage Test Suites

Anti patterns can sink a test suite with surprising weight. They mix concerns, hide state and wrap logic inside shapes that tests cannot grip. A healthy design avoids such pitfalls to keep the test environment predictable.

Hidden Work in Getters

Getters that sneak extra behaviour into a simple read operation surprise both code and tests. When a test expects a straightforward value but discovers computation, it becomes harder to maintain. Predictable interfaces help keep testing focused.

💡 A getter should act like a pane of glass rather than a puzzle box. Transparency makes tests easier to reason about.

Long Methods With Entangled Branches

Long methods force tests to dig through complicated paths. Each branch becomes a separate scenario that needs careful setup. Splitting behaviour into smaller units helps tests become lighter and more direct.