Many developers have a hate relationship with testing. However, I believe the main cause of that is code that is highly-coupled and difficult to test.
This post states some principles and guidelines that can help you write easily-testable code, which is not only easier to test but also more flexible and maintainable, due to its better modularity. Here at Feedzai, we try to follow these principles and guidelines in order to have better code quality, increased test coverage and confidence in the products we deliver.
This article mostly serves as an introduction to these concepts. If you want to understand these concepts better, you may want to do some additional research. The article explores the SOLID principles, the Law of Demeter, some other guidelines and ends with a small example that illustrates some of the aspects explored here.
SOLID design principles
One of the most well-known collections of principles in the software engineering industry are the SOLID principles, documented by Robert C. Martin (also known as Uncle Bob).
This collection of principles can help your code to be more modular and to have increased testability. Let’s go deeper into each of these principles:
Single Responsibility Principle (SRP)
Each software module should only have one reason to change.
So, what does that mean? Let’s take an example:
Imagine that you write a program that calls an external REST API endpoint, does some kind of processing with the received data and writes it to a CSV file. A naïve approach might be to have everything in the same class. However, this class has multiple reasons to change:
- You might want to change the API call to a different provider or change it to read from a different source, such as a file.
- The processing that you are doing might need to be changed.
- You may want to write the results to a different output, maybe a different file format.
- In addition, you might want to be able to have multiple types of inputs and outputs that can be interchanged in runtime based on some kind of configuration or input.
For these reasons, you should break your application down into multiple classes and interfaces. Here’s an example:
This principle can be applied at both class and method levels (and to some extent at the package level, but Robert Martin has specific principles for that).
However, you should be careful not to overdo it. The Single Responsibility Principle does not state that a module should only do one thing. It states it should have one and only one reason to change. For more information about this, please read this blog post from Robert Martin.
Open/Closed Principle (OCP)
Your classes should be open for extension but closed to modifications.
This means that your design should allow the addition of new features with minimal change to the existing code. This can be achieved by coding against abstractions instead of concrete implementations, as well as through the use of some design patterns such as Decorator, Visitor and Strategy. Following the Single Responsibility Principle also helps with that, as you will have things segregated.
The best way to think about this principle is to think about a plugin architecture. In such a scenario, plugins are developed to add behaviour to a system without changing any of its code.
Another smaller but more concrete example is Java’s Collections.sort method. It can sort any type of class that implements the Comparable interface without modifying the sort method for each new class. If you instead had a sort method with a switch statement with a case for every type you wanted to compare, you would have to modify it every time you needed to compare a new type.
For more information on this principle, you can read Robert Martin’s blog post about it.
Liskov Substitution Principle (LSP)
Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.
A good example for this principle is the square-rectangle problem: you might be tempted to have a Shape interface, a Rectangle that implements that, and a Square which extends it but guarantees that both height and width are always the same. This might look good at first glance. However, this example breaks the Liskov Substitution Principle because even though the programmatic API is the same, its preconditions and postconditions are not. If you have a class or method which accepts a Rectangle, you might not be able to simply pass in a Square in its place because the code might be assuming that the height and width can be changed independently. For the user of the interface, the implementation should not matter.
Another common example of breaking this principle is having implementations with methods that throw UnsupportedOperationException, which indicate that a certain operation is not supported in some specific implementations.
Interface Segregation Principle (ISP)
No client should be forced to depend on methods it does not use.
Large interfaces should be split into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
Following this principle helps to keep your system decoupled and makes it easier not to break the Liskov Substitution Principle.
For example, if you are creating an interface for a multi-functional printer, instead of having a single MultiFunctionalPrinter interface with a print() and a scan() method, you should instead have two interfaces: Printer and Scanner, each with the respective method. That way, if a client only needs the print() method, you can provide it with a simple printer without having to change any of the application code, as the client was not dependent on the scan aspects of the MultiFunctionalPrinter.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; both should depend on abstractions.
Abstractions should not depend on details. Details should depend upon abstractions.
Following this principle allows you to easily replace certain implementations with compatible ones which follow the same interface. This is very useful for testing as it allows you to replace real implementations with test doubles. It also allows you to better react to changing requirements.
The following diagram shows how the example presented in the Single Responsibility Principle section also follows the Dependency Inversion Principle. You can see that both high-level and low-level modules depend on abstractions.
For more information on the topic of abstractions, you may read this post by Gabriel Candal on the Feedzai TechBlog. 💪
Law of Demeter (LoD)
Another “law” which is useful for keeping the code decoupled and testable is the Law of Demeter. This principle states the following:
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
Each unit should only talk to its friends; don’t talk to strangers.
Only talk to your immediate friends.
What this basically means is that you should not obtain dependencies through your dependencies. You should not do things like this.getA().getB().doSomething(). If you need dependency B, it should be provided to your class through its constructor or as a method argument.
Breaking this principle makes the code highly-coupled to implementation details and substantially less reusable. It also makes tests much harder to write because it requires you to mock or create instances of not only the explicit collaborators of the class under test but also all the implicit chain of collaborators required by it.
You can find more information about this principle on this blog post by Miško Hevery.
Besides these principles, there are a few more guidelines that ease the testability of a codebase:
Make sure your code has seams: From the definition on Working Effectively with Legacy Code by Michael Feathers, “A seam is a place where you can alter behaviour in your program without editing that place”. Having seams is required in order to unit test code as you can replace behaviour with test doubles. Following the principles stated above will help with that as well as practising Test-Driven-Development, as unit tests can help with the design of your APIs.
Don’t mix object creation with application logic: You should not carelessly create object instances. You should instead have two types of classes: application classes and factories. Application classes are those that do real work and have all the business logic while factories are used to create objects and respective dependencies. You should avoid using “new” outside of factories, with the exception of the creation of data-objects (data structures or objects with only getters/setters), which you can create freely. If you create other classes in your application code, you won’t be able to replace those with test doubles when unit testing (unless you use monkey-patching or bytecode-manipulation frameworks, but that leads to tests that are both difficult to create and maintain). If you have to create objects dynamically in your application code, you should use the Abstract Factory design pattern. That way, you can pass in a concrete factory as a dependency that implements that factory interface, and your application code can create objects without depending on a concrete implementation.
Use Dependency Injection: Related to the guideline above, you should provide the dependencies to your classes. A class should not be responsible for fetching its dependencies, either by creating them, using global state (e.g. Singletons) or getting dependencies through other dependencies (breaking the Law of Demeter). Preferably, dependencies should be provided to the class through its constructor. Take note that Dependency Injection is not a synonym of using a framework. It is something that can be perfectly done manually.
Don’t use global state: Global state makes code more difficult to understand, as the user of those classes might not be aware of which variables need to be instantiated. It also makes tests more difficult to write due to the same reason and due to tests being able to influence each other, which is a potential source of flakiness. Also, be careful that Singletons are an example of global state, and as such, should also be avoided in most cases. Please note that here “Singleton” is referring to the Singleton design pattern, in which a class restricts its instantiation to only one instance. Classes that have a single instance without enforcing it, sometimes referred to as “singletons” without the capital “S”, are recommended instead. Dependency injection should be used to pass the instances to the objects that depend on them.
Avoid static methods: Static methods are procedural code and should be avoided in an object-oriented paradigm, as they don’t provide the seams required for unit testing. Exceptions to this guideline are simple and pure methods, such as Math.min(). However, you might want to avoid the direct use of some other static methods, such as System.currentTimeMillis(), as you won’t be able to replace it with a test double. If you instead have a TimestampSupplier interface or just Supplier<Long>, you can dependency-inject an implementation that uses System.currentTimeMillis() in your production code and use a fake implementation in your test code, which might help you create better assertions in your tests.
Favour composition over inheritance: You should prefer the usage of composition over inheritance. Composition allows your code to better follow the Single Responsibility Principle, makes the code more easy to test and avoids class number explosion. Composition provides more flexibility as the behaviour of the system is modelled by different interfaces that collaborate instead of creating a class hierarchy that distributes behaviour among business-domain classes via inheritance. It also makes the system more flexible as components can be assembled in different ways in runtime, without changing the code. There are some design patterns that help with this, such as the Strategy and Decorator patterns.
A small example
Below is an example of code that is difficult to test, as it’s highly coupled to multiple aspects of the system. This small code sample breaks most of the SOLID principles, the Law of Demeter and the above guidelines. In order to test it, you would have to use a bytecode-manipulating mocking framework (such as PowerMock), which leads to tests that are difficult to write and maintain.
Now, here is another code sample, which although not perfect, is much easier to test. In this example, you can just provide a fake UserDatabase and a StringWriter instead of a FileWriter.
If you would like to learn more about these concepts, I really like and recommend this video of a presentation by Miško Hevery.
Clean Code and Clean Architecture are also two good books by Robert Martin that will teach you about the principles touched here and much more.
I hope that this post gave you some grounds about writing testable code and that by following these principles and guidelines you will be able to have a more clean, maintainable and testable codebase!