Writing Testable Clean Code

Zehra Özge Kısaoğlu
DigiGeek
Published in
11 min readDec 27, 2021

Most of the time, developers don’t like to write tests for their codes. The main reason behind this is the code that is highly-coupled and difficult to test. On the other hand, delivering testable code is an important responsibility of a developer to keep maintainability of the code easy.

Mainly, this post will give you some tips and show important principles and guidelines that can help you write easily-testable, more flexible and maintainable code. This will also increase your code quality.

Why do we test our code?

Let’s quickly recap why tests are so important for software development. There are many advantages of writing tests, few of them are the followings:

  • New joiners can write the code quickly because they don’t have to worry about breaking the app as they contribute.
  • Testing provides a kind of documentation as well. A test describes a user story/requirement or a case. Over time, all small pieces/tests compose the entire story about how the application works.
  • You can make changes quickly with new tests and quickly check the current behavior by running the tests. This also helps you feel more confident in the changes that you make.
  • You can find bugs easily and quicker before your customers do. It also saves hours of debugging. And finding bugs at the software development stage with the tests saves time and money.

What makes your code testable?

Basically, writing “clean” code is the key point about testable code. Some principles, laws and points guide us along the way.

SOLID Design Principles

In software development, SOLID is an acronym for five design principles that lend software designs more understandable, flexible, and maintainable, documented by Robert C. Martin (also known as Uncle Bob).

According to SOLID principles, your code should follow the following principles.

Single Responsibility Principle (SRP)

Each software module should have one and only one reason to change

Another wording for this principle (Robert Martin has specified) is:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

This is another way to define cohesion and coupling. We should increase the cohesion between modules that change for the same reasons, and decrease the coupling between those modules that change for different reasons, as this principle says.

This principle can be applied to class and method levels. However, we should be careful not to overdo it. It doesn’t state a module should only do one thing, it is all about the responsibility (i.e, “reason to change”).

As an example that breaks SRP, there are three responsibilities in Employee class below: calculation logic, database access logic, and reporting logic. All of them are unrelated and mixed up within one class.

public class Employee {

public Double calculatePay() {...}
public void saveEmployee() {...}

public void getEmployeeReport() {...}

}

In order to fix this violation, you can split this class into three different classes having each responsibility separately.

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modifications.

Another wording for this principle (Robert Martin has specified) is:

You should be able to extend the behavior of a system without having to modify that system.

In an ideal scenario, while you’re adding new features or extending the functionality of the existing code, you only add new piece of code to the systems with minimal change the existing one. While doing that you are less likely to break what’s already been working.

As a simple example, you can think an AreaCalculator program to calculate the Rectangle‘s area at first.

public class Rectangle {
public Double length;
public Double width;
}
public class AreaCalculator {
public Double calculateRectangleArea(Rectangle rectangle){
return rectangle.length * rectangle.width;
}
}

This code manages but it violates OCP because if you want to add another shape to a program, you should modify the calculateRectangleAre method completely.

Proper code that follows OCP should be as follows.

public interface Shape {
public Double calculateArea();
}

public class Rectangle implements Shape {
Double length;
Double width;

public double calculateArea(){
return length * width;
}
}

public class Circle implements Shape {
public Double radius;

public Double calculateArea(){
return (22 / 7) * radius * radius;
}
}
public class AreaCalculator {

public Double calculateShapeArea(Shape shape) {
return shape.calculateArea();
}
}

Each shape object whose area will be calculated should implement an interface Shape and AreaCalculator should make calculations over interface objects.

Liskov Substitution Principle (LSP)

Objects of a superclass and objects of its subclasses should be interchangeable without breaking the application.

As a classic example, square-rectangle problem is the violation of this principle. In mathematics, every Square “is-a” special type of Rectangle. Therefore, Rectangle and Square can be interchangeable . But in software development, a scenario of ambiguity might occur. Assume you have a Square class that extends Rectangle class. The class or method that accepts Rectangle might not be able to accept Square because the dimensions of a Square cannot be modified independently and this may result in “unexpected” behavior in the application flow. Even though application interfaces seem okay, pre and post conditions may be different for the implementations of two shapes.

Another example breaking this rule can be methods throwing UnsupportedOperationException for some specific subclass implementations. To solve this problem, inheritance hierarchy should be reviewed and redesigned. BookDelivery example shows the LSP principle violation and proposes a solution.

Shortly, violations of LSP can cause undefined behavior which means it works okay during development but blows up in production.

Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use.

As simple as it sounds, it’s also so easy to ignore. We should stay away from implementations with methods that throw NotSupportedException or UnsupportedOperationException, which indicate that a certain operation is not supported in some specific implementations.

This principle has two points.

  • You should be careful about introducing new methods to an existing interface. The methods you are introducing should be relevant to all classes who are already conforming the interface. Implementing the methods in irrelavant classes and returning NotSupportedException is not the solution.
  • You should be careful about adding an interface to a class. All the methods in the interface should be relevant to the class you are adding. None of the methods of the interface should be implemented as throwing NotSupportedException.

That being said, there’s one simple thing you can do to follow ISP, which is introducing new interfaces as much as possible.

For example, think about an interface called RestaurantInterface which has many methods for online, phone and walkin customer orders. If an online customer service implements this interface, it must also implement phone and walkin order service methods which are “not supported” here. This is violation example of ISP and should be solved by “segregating” interfaces.

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 (concrete implementations) should depend upon abstractions.

This principle is so important, because it ensures loosely coupled designs. It forces developers to design every dependency to target an interface or an abstract class instead of an actual class. Thinking and designing this way ensures you not to interfere high-level modules as you’re working with low-level modules.

Here is an example of a PasswordReminder that connects to a MySQL database.

public class MySQLConnection {
public String connect() {
// handle the database connection
return "Database connection";
}
}

public class PasswordReminder {
private MySQLConnection dbConnection;

public PasswordReminder(MySQLConnection dbConnection) {
this.dbConnection = dbConnection;
}
}

MySQLConnection is the low-level module and PasswordReminder is high level. Above implementation violates DIP because here PasswordReminder is being forced to depend on MySQLConnection. But PasswordReminder shouldn’t be interested in DB connection.

With below change to the code, both MySQLConnection and PasswordReminder depend on abstraction and this follows DIP.

public interface DBConnectionInterface {
public String connect();
}
public class MySQLConnection implements DBConnectionInterface {
public String connect() {
// handle the database connection
return "Database connection";
}
}

public class PasswordReminder {
private DBConnectionInterface dbConnection;

public PasswordReminder(DBConnectionInterface dbConnection) {
this.dbConnection = dbConnection;
}
}

Impurity and Testability

Non-determinism and side effects have similar influence on the codebase. When used carelessly, they lead your code to be less maintainable, less understandable, not reusable, more tightly coupled and less testable.

Methods that are both deterministic and “side-effect-free” are much easier to test and reuse to build larger programs. These functions are called pure functions. Writing tests for pure functions are very easy. All you do is pass a few arguments and verify that they are correct. No side effect, no complexity, yes determinism.

Consider the code below which is such a perfect illustration of a pure function.

public int Sum(int a, int b){
return a + b;
}

The only data it can access are the values that are passed parameters. It doesn’t cause any external change, it doesn’t mutate any variable, it is so simple.

What about the impure function example as the following.

public static String GetTimeOfDay()
{
LocalDateTime time = LocalDateTime.now();
if (time.getHour() >= 0 && time.getHour() < 6)
{
return "Night";
}
if (time.getHour() >= 6 && time.getHour() < 12)
{
return "Morning";
}
if (time.getHour() >= 12 && time.getHour() < 18)
{
return "Afternoon";
}
return "Evening";
}

GetTimeOfDay method accesses data from the source rather than parameters. It is non-deterministic, if you call it a million times, you will get different results very likely for each call.

Such non-deterministic behavior makes it very complex to test the internal logic of the GetTimeOfDay() method. Below assertion would be true if and only if you make changes in system date and time in your test setup.

Assert.AreEqual("Morning", GetTimeOfDay());

Impurity is harmful and toxic. If a method foo() depends on another method bar() which has side-effects and non-deterministic, then foo() becomes also “impure”. When you adapt this problem to complex real-life applications, we may encounter corruption of the codebase full of smells, anti-patterns and ugly unpleasant codes which is eventually untestable.

You can’t avoid impurity completely. At some point, any real-life application will read and manipulate state by getting interacted with the databases, environments, web services, configuration files or external systems. Instead of aiming to get rid of all impurity, you can limit and reduce impurity factors to prevent your codebase. You can decouple hard-coded dependencies as much as possible to be able to test those elements separately.

Some Useful Points & Guidelines

In addition to SOLID principles, there are some other points you should take into consideration when writing testable code. Here, few of them are stated.

Write cleaner functions

There are many ways and aspects of writing clean functions. Small functions having no side effects, less arguments, one level of abstraction are just a few examples. This is another detailed topic that can be examined separately. But, it should be known that writing clean functions is a nice practice that eases writing tests.

Avoid circular dependencies

If two or more modules (directly or indirectly) depend on each other to function properly, a circular dependency occurs. This is against the Dependency Inversion Principle (DIP). If this case happens, you should refactor your code and re-think a new way to stop circular dependencies.

Minimize global declarations

Global state causes code to become more complex and difficult to understand. After some time, it becomes hard to find where the variable was initialized and set to. Debugging global variables and related things is another pain. Global declarations make tests more difficult to write due to the same reasons.

Singletons are an example of global state, so you should be careful about it and avoid Singletons most of the time. Dependency injection should be preferred to pass the instances to the objects.

Don’t mix object construction with application logic

You should have two types of classes in your application: factories and application classes.

Factories are the places where all new operators reside in, and they should be only responsible for constructing objects and dependencies.

On the other hand, application classes are those having all the business logic . Instead of creating objects, they simply ask factories for objects to create or get. This makes it easier to test the application logic by replacing the real classes for test doubles.

You should avoid new operators outside of factories, except that the creation of data objects with only getters and setters can be made freely.

Use Dependency Injection

Dependency Injection is a technique in which dependencies are passed to the objects that need them. The main aim is to achieve separation of concerns of construction and use of objects. This can increase readability and reusability of code.

Practically, a class should not fetch dependencies by creating them or through framework or using global state (e.g, Singletons). You should ideally pass the dependencies to your classes through its constructor. This makes it easier to replace dependencies by test doubles during writing tests.

The example below shows the code that is difficult to test. Here, ClassB is dependency of ClassA and ClassA is highly coupled to the application context to obtain ClassB .

public class ClassA {    public int calculateTenPercent() {     
return App.getClassB().calculate() * 0.1d;
}
}

Instead of fetching ClassB by itself, you can pass it to ClassA within its constructor. This is more testable, all you need to do is simply mock ClassB in your test and give it to ClassA .

public class ClassA {
ClassB classB;
public ClassA(ClassB classBParam) {
this.classB = classBParam;
}
public int calculateTenPercent() {
return classB.calculate() * 0.1d;
}
}

Avoid static methods

The key to testing is the presence of seams (places where you can alter the normal execution flow). However, static methods (i.e, procedural codes) has no seams, it is clear at compile-time which method calls which other method. You can not know how to test procedural code, because there is no instantiation, the code and data are separate . For those reasons, you should avoid using static methods in an OO program, static methods are death to testability.

Exceptions to this principle are simple and pure methods, such as Math.min(). However, you might want to avoid the direct use of some other static methods like System.currentTimeMillis(), as you won’t be able to replace it with a test double. Instead, if you can use it through Supplier interface, you can also fake its implementation in your test.

Favor composition over inheritance

At run-time you can not choose a different inheritance, but you can choose a different composition. This is important point for tests as we want to test things in isolation. Also, composition allows your code to better follow the Single Responsibility Principle which makes your code more easy to test.

If you prefer to use inheritance, during the tests you need to mock out the parent classes and parent classes’ irrelevant dependencies as well. This will make your tests very hard to write and also clutter the focus of the test.

Favor polymorphism over conditionals

You should prefer polymorphism if your switch statements or conditional statements are in increase or on repeat. Polymorphism will break your complex class into several smaller simpler classes. This helps testing since a simpler/smaller class is easier to test.

This post includes some tips about writing testable code. The trick is translating these abstract concepts into concrete decisions in your code to improve its testability.

By following these principles and guidelines, you have not only testable, but also cleaner and more maintainable code!

Happy coding!

--

--