In legacy code, existing static methods often get in the way when trying to write unit tests. In this article, I'll show you "principles (rules) to avoid adding new such code" and "solving cases where existing static methods are blocking you".
An article for writing unit tests for the first time on legacy code.
I can't write unit tests well for methods like LegacyService # execute ()
below.
Since the loadFromDatabase method called from the method under test requires access to the database, an exception will occur when executed from an inaccessible environment.
Even if the database can be accessed, it is difficult to stably acquire the same data from the database, and it takes a long time to execute.
public class LegacyService {
public void execute() {
//Here is the code to be tested
//Code in question
Object data = loadFromDatabase();
//Here is the code to be tested
}
//Static methods that are also called from outside the class
public static Object loadFromDatabase() {
//Processing that accesses the database
}
}
You can replace the process of accessing the database with Mock, but it is difficult because the loadFromDatabase method is declared as a static method. [^ 1]
[^ 1]: It is possible to use a library such as PowerMock, but it has drawbacks such as slow execution, so it is excluded this time.
Also, it seems like changing the loadFromDatabase method to an instance method, but if it's already referenced by another class method, you can't simply change it.
Do not write the process that satisfies the following conditions in the static method.
1.Interact with the database
2.Communicate over the network
3.Access the file system
4.Requires special preferences to run (such as editing a preference file)
(Citation: Legacy Code Improvement Guide / 2.1 What is a unit test?)
Static methods are "constants / functions whose arguments do not change in any execution environment (at least in that major version of the system)" such as functions for finding pi and square root. Should only be used.
But what about existing code that violates the above principles?
In such a case, it is basically necessary to refactor the code under test. Let's divide the commit and fix it step by step.
Unit tests that fail
public class LegacyServiceTest {
@Test
public void test() {
execute();
}
}
Well, here is the production. First, create a loadData method that wraps the loadFromDatabase method in question.
At this time, the method must not be declared private. In addition, @VisibleForTesting is annotated. These reasons will be explained in step 2.
python
import com.google.common.annotations.VisibleForTesting;
public class LegacyService {
public void execute() {
//Here is the code to be tested
Object data = loadFromDatabase();
//Here is the code to be tested
}
//Newly added instance method
@VisibleForTesting
Object loadData() {
//Call the static method in question that originally existed
return loadFromDatabase();
}
//Originally existing static method
public static Object loadFromDatabase() {
//Processing that accesses the database
}
}
In the test code, overwrite the implementation of the loadData method created in step 1 with Mock.
LegacyService JUnit test class
public class LegacyServiceTest extends LegacyService {
@Test
public void test() {
execute();
}
@Override
Object loadData() {
//Mock the implementation here
}
}
To allow this overwriting, step 1 made the visibility of the loadData method package-private (it could be protected). We've increased visibility for the test class and annotated it with @VisibleForTesting to make it easier to understand.
Also, in the above, [Self Shunt approach to make the test class itself a subclass of the test target class](https://t-wada.hatenablog.jp/entry/design-for-testability#%E3%82%A2%E3% 83% 97% E3% 83% AD% E3% 83% BC% E3% 83% 815-% E3% 83% 86% E3% 82% B9% E3% 83% 88% E3% 82% 92% E5% AF % BE% E8% B1% A1% E3% 82% AF% E3% 83% A9% E3% 82% B9% E3% 81% AE% E3% 82% B5% E3% 83% 96% E3% 82% AF % E3% 83% A9% E3% 82% B9% E3% 81% AB% E3% 81% 97% E3% 81% A6% E3% 82% AA% E3% 83% BC% E3% 83% 90% E3 % 83% BC% E3% 83% A9% E3% 82% A4% E3% 83% 89-Self-Shunt). Of course, there is no problem with other writing styles.
For the execute method, which is the method to be tested, replace the original call part of the loadFromDatabase method with the wrapped loadData call.
public class LegacyService {
public void execute() {
//Here is the code to be tested
//Change the call destination to the newly added instance method
Object data = loadData();
//Here is the code to be tested
}
//abridgement
}
Up to this step, the method under test has not been rewritten unnecessarily. Since the range of correction is limited, highly safe refactoring is possible.
Modified class
import com.google.common.annotations.VisibleForTesting;
public class LegacyService {
public void execute() {
//Here is the code to be tested
Object data = loadData();
//Here is the code to be tested
}
//Newly added instance method
@VisibleForTesting
Object loadData() {
return loadFromDatabase();
}
//Originally existing static method
public static Object loadFromDatabase() {
//Processing like accessing DB here
}
}
To write unit tests, don't blindly declare methods statically.
However, even if there is a problem with the existing code, you can write unit tests while limiting the range of influence of refactoring by modifying it in steps.
If there is a problem with the existing code, there are multiple other methods to solve it. More on them in the next article.
Next time: Let's break away from static methods to write unit tests-②DI-