Long ago, I wrote about high-level testing and alluded to scale minimization as a useful technique in doing so. In this post, I’ll explore this idea a bit more.
What is scale minimization? You may have heard it referred to by other names such as one-box testing or hermetic servers. The idea is to consolidate a distributed system onto a single machine to facilitate testing and analysis. It sounds difficult, but it doesn’t have to be if you keep a few software design principles in mind.
Let’s start with some software design rules espoused by Steve Freeman and Nat Pryce, Sandi Metz and others: loose coupling, high cohesion, ease of composition, and context independence. Of particular importance here is the last item — context (read: hardware and topology) independence.
Here are some common examples of hardware and topology dependence that make scale minimization hard:
- Fixed network port numbers (e.g. “http://localhost:12345/MyListenerService”)
- Fixed file system paths (e.g. “C:\MyPrivateFolder”)
- Assuming the presence of specialized hardware (e.g. load balancers or other network appliances)
- Assuming a physical disk layout (e.g. “C:” is the log volume, “D:” is the RAID volume containing system files, …)
- Any type of “Highlander” assumption (“There can be only one”)
Fortunately there are some easy fixes for most of these problems. The first is to make every important decision come from a configuration setting. Do not assume port 12345 is always available; instead, come up with a reasonable default and allow the user to override it via a settings file.
For explicit hardware dependencies, abstraction is the key. Leverage ports/adapters/simulators design practices to build a domain-specific interface with configurable adapters depending on the use case. In the scale minimization case, wire up the simulator (the same one you used for your unit tests) instead of the real thing. Of course, this is only easy to do when the interface is relatively simple. If you have a complex dependency, you are better off simplifying the way you use the dependency at the core of your system as opposed to providing a “mimic adapter.”
Lastly, you must consider changing any part of the system that assumes it is the only one of its kind on the entire machine. This is like an extreme case of the singleton anti-pattern. While there are some interesting workarounds to this problem (e.g. using Docker containers or VMs for isolation), they are all just that — workarounds for highly coupled designs. Break these dependencies and you’ll have an easier time not only with scale minimization, but with unit testing and maintenance of the code in general.