When the project is big enough and needs to be maintainable in the long run, it has to rely on automated tests to keep up its quality. Compared to system testing where you test the program as a whole, unit testing has its benefit for being fast (because it only instantiates a small piece of the program) and stable (because it usually mocks out the unstable dependency, e.g. network connection, database connection). Because of this, having automated unit tests becomes extremely important for Object-Oriented programs.
This article compiles multiple posts from Google Testing Blog on how to write more unit-testable code. It explains four common flaws in untestable code, and rules to follow for each of the flaws. At the end of each rule, you will see link(s) to the original Google Testing Blog posts.
Seams is where we prevent the execution of normal code path and is how we achieve isolation of the class under test.
To create seams in your code, you need to acquire your dependency via dependency injection. You inject real object for production and test doubles for testing. For example, if a class depends on some database connection to retrieve some user data, you can inject a fake database connection to always return prepared data immediately without really performing any database operation.
That is the basic idea for how seams can help in unit testing. Below are five rules you can follow to create seams in your code.
Sources: Static Methods are Death to Testability
new
keyword in a constructor or at field declarationAny work you do in a constructor needs to successfully navigate through in every test (not just the direct test, but also any related test which tries to instantiate your class indirectly as part of some larger object graph).
In short, the constructor's job is to assign the dependencies to fields. And that's all.
Sources: Writing Testable Code.
new
keyword freely anywhere in your codeYou should have two kinds of classes in your application.
First, factories, where all new
operators reside in, are in charge of constructing objects, and nothing else.
The other, application classes, are devoid of new
operators. Instead of creating object, they simply ask for them. This makes it easier to test the application logic by replacing the real classes for test doubles.
Sources: Where Have all the "new" Operators Gone? and Writing Testable Code
Value objects are your model objects, like User
, Email
, CreditCard
.
Service objects, on the other hand, are your logic objects, like UserAuthenticator
, MailServer
, CreditCardProcessor
. Compared to value objects,
Clearly, they are very different in their nature and usage. Mixing the two creates a hybrid which has no advantages of value-objects and all the baggage of service-object.
Sources: Writing Testable Code.
Static methods are procedural code. You can write your program in an OO language only using static methods, with one calling another, then it is not an OOP application but a procedural application. There is no way to unit test procedure application because there is no seams for you to divert the normal execution flow.
You can remove the static methods as below:
method(a,b)
becomes a.method(b)
)Sources: Writing Testable Code.
At run-time you can not choose a different inheritance, but you can chose a different composition. This is important for tests as we want to test things in isolation.
Wrongly used inheritance clutters the focus of test because you need to mock out the your parent classes' irrelevant dependencies, too. For example,
Inheriting from AuthenticatedServlet will make your sub-class very hard to test since every test will have to mock out the authentication. But what if AuthenticatedServlet inherits from DbTransactionServlet? (that gets so much harder)
Sources: Static Methods are Death to Testability and Writing Testable Code
System.currentTimeMillis()
, new Date()
or Math.random()
Global states are a bad idea because they make the code hard to understand and reason about.
Moreover, they cause problems for testing because global states are persistent throughout the lifetime of a JVM instance. In unit testing, we have one JVM instance to run all tests (instead one JVM for one test, for performance reason). Thus the global states are persistent from tests to tests. Now, if some tests expect the global states to be in state A, while some other tests expect the global states to be in state B. In this scenario, you cannot run the tests in parallel, otherwise your tests will become flaky (sometimes pass, sometimes fail) because the order of tests matters.
Sources: Writing Testable Code.
Singleton Pattern, despite being a well-know design patter, are global states in sheep's clothing. They have globally accessible getInstance()
method and a private singleton object (which is the global state).
Another problem about Singleton Pattern is that they are globally accessible, thus any method can access them inside the code without explicitly declaring in its API. In other words, the API lied about its dependency!
Note singletons (with lowercase s) are valid and sometimes very useful, which you can enforce their singleton identities by enforcing their constructors are called only once in the application. However, Singleton Pattern (with uppercase S) is almost always causing undesirable global states, which you should avoid. Only few examples of Singleton Pattern, including Constants and Logging, are acceptable since they don't affect the application logic.
Sources: Singletons are Pathological Liars and Writing Testable Code
a.getB().getC().doSomething()
)The Law of Demeter can be summarized as "only talk to your immediate friends".
Previously we mentioned the importance of dependency injection for creating seams in Flaw #1: no seams for isolating the class under test. Here we emphasize on injecting only those direct dependency.
Let's use an example to illustrate why indirect dependency hurts testability:
Say you have an Authenticator class which needs a Config
object, do you pass in a path to the configuration file, or just pass in a Config
object? Since what you need is a Config
object, not a file path, an example of indirect dependency will be you pass in a path to the configuration file, and read a Config
object from the path. To test Authenticator, you have to first write some configuration to a file, pass the file path to Authenticator, and let it read a Config
itself. Versus you can directly create a Config
object and pass it to Authenticator using direct dependency.
Sources: Writing Testable Code.
To extreme, it becomes an anti-pattern, God Object.
These classes are hard to test since there are multiple objects hiding inside of them and as a result you are testing all of the objects at once.
Single Responsibility Principle also requires you to have classes with one concern (one reason to change), because these classes are simpler to understand what they are doing and easier to unit test.
Sources: Where Have all the "new" Operators Gone? and Writing Testable Code
switch
or if
conditions in many placesFor a detailed code example, please refer to this Stack Overflow answer.
From the answer, if you have one switch statement based on an internal field you probably have the same switch in multiple places. This causes problems when you add a new case as you have to update all the switch statements.
By using polymorphism, you get the same functionality and because a new case is a new class you don't have to search your code for things that need to be updated. It is all isolated for each class.
This closely follows with Open Closed Principle because you can ship this abstract parent class (closed for modification) as part of your binary library, but people can still extend the functionality by adding new child classes (open for extension).
Sources: Writing Testable Code.
This article includes ten rules that can help you understand some key concepts, such as seams, dependency injection, global states, singletons and Singleton. Also, I hope you can apply these rules into practice, like writing a program with these rules in mind or reviewing some code your wrote before and see whether you can improve its testability, so you can benefit from the things you learn in this article.