One of the defining attributes of Unit Tests is isolation. Even the name “Unit” implies it. Well isolated tests make it easier to pinpoint errors, are less brittle, and run faster.
But are your tests really well isolated? And are they easy to isolate or are they painful to setup?
There are a couple of progressive steps you can take to improve your tests’ isolation:
BASIC ISOLATION TECHNIQUE: Test Doubles plus Dependency Injection.
The most fundamental pattern related to test isolation are Test Doubles: mocks, spies, stubs, and dummy objects. You can try to reduce the number of dependencies, but it’s not possible for a non-trivial system to have no dependencies at all. Substituting them with Test Doubles is the only way to fully isolate a unit of code.
Test Doubles are inseparably linked with the Dependency Injection pattern. A dependency can’t be substituted with a Test Double if its instantiation is hard-coded inside the tested unit 1. Instantiating dependencies outside of the unit and injecting them into it makes such a substitution possible.
BUT: There are things I can’t fake!
Unfortunately, some code constructs are difficult to fake. A few examples: singletons, static methods, or concrete classes used for parameter types. Constructs like these don’t play well with mocking frameworks. They require hard to setup and hard to maintain solutions like implementing fake objects by hand or subclassing the dependencies of a tested unit 2.
GOING FURTHER: Avoid code that’s hard to fake.
Fortunately, the hard to fake code almost always has easier to fake alternatives:
- Instead of using concrete classes for parameter types you can use interfaces.
- Static methods can be extracted to a separate helper class, as instance methods.
- Singletons can be replaced with dynamic classes plus an external mechanism to control the number of instances created.
Such a fake-friendly design not only makes your units easier to isolate, it also produces better, more maintainable code, so it’s a win-win situation.
BUT: Setting up Test Doubles is so complex!
By replacing all hard-coded dependencies with Dependency Injection, and eliminating all non-fakeable code constructs, we’ve made it possible to fully isolate the tested unit of code. But possible doesn’t equal simple.
I’ve seen tests instantiating 4-5 stubs, each 3 levels of nesting deep, to fully isolate the tested unit. Writing and maintaining such a setup code isn’t fun, and its brittleness kills most of the benefits of unit isolation.
GOING FURTHER: Reduce dependencies in your code.
The basic unit isolation technique is to fake the unit’s dependencies. But even better technique is to eliminate dependencies, so you don’t have to fake them at all.
There are many design patterns or principles that help eliminate dependencies or make them more “shallow”. A few examples:
- Law of Demeter
- Single Level of Abstraction Principle
- Single Responsibility Principle
- Command-Query Separation
The most important thing to remember is that the test isolation isn’t achieved only – or even primarily – in the tests; it’s achieved mostly through a clean design of the code being tested.
BUT: My code is tightly coupled with my framework!
Achieving isolation through the design of the code being tested sounds nice in theory, but the code never exists in a vacuum – it is coupled to the architecture of the system. Implementation details of various architectural layers or frameworks tend to leak into your domain code. Your classes often inherit from ORM framework base classes, are entangled with the routing or presentation layer of an MVC framework, and so on. This makes them hard to isolate.
GOING FURTHER: Use overall system architecture that promotes isolation.
The architecture doesn’t have to be so invasive. There are several architectures that provide clean isolation between the infrastructural layers and domain logic. A few examples:
- Alistair Cockburn’s Hexagonal Architecture
- Bob Martin’s Clean Architecture
- Jeffrey Palermo’s Onion Architecture
What’s important, you can reap the benefits of such architectures even if your framework doesn’t adhere to their principles. Architectures like the ones mentioned above help you isolate yourself from the framework, decouple it from your code, and push it to the edges of your system. This, in turn, gives you the room to write your code in a way that enables fully – and well – isolated tests.
A BONUS: The simplest test isolation heuristic.
Going towards well isolated tests is a gradual progression:
- use Test Doubles and Dependency Injection
- replace hard to fake code with different constructs
- use design patterns that support unit isolation
- use system architecture that decouples you from the architectural scaffolding
There is a lot to do at each level – this article only scratched the surface. How do you know that you’ve done enough? Or even that you’re going in the right direction? And on which level of progression you should focus at the moment?
The simplest heuristic that can help you decide is this: The test setup should be painless.
If the test setup is hard or complicated, pay attention to what makes it so: Is it a particular method? An overall class design? A coupling to a framework? Try to identify an offender and isolate it from the code being tested, using techniques like the ones I describe in this post. Rinse and repeat, until the test setup doesn’t cause you any more trouble.
What techniques do you use to isolate unit tests? I’d love to hear what works best for you. Please drop me a comment and share your experience!
- Again, this depends on the language and the Dependency Injection framework used. However, practically any language has its quirks, so although the concrete examples I give may not apply to your situation, the overall strategy still applies. ↩