Let's break away from static methods to write unit tests # 1 Principle / Wrap method

Overview

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.

problem

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.

Principles (rules) for solving problems

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.

How to solve problems with existing code

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.

Step 0: Prepare a failing unit test

Unit tests that fail


public class LegacyServiceTest {
    @Test
    public void test() {
        execute();
    }
}

Step 1: Create an instance method to wrap

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
    }
}

Step 2: Mock the implementation of Step 1 with your test code

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.

Step 3: Change the implementation of the method under test

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
    }   
}

Summary

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-

Recommended Posts

Let's break away from static methods to write unit tests # 1 Principle / Wrap method
Let's break away from static methods to write unit tests # 2 DI (Dependency Injection)
Let's write how to make API with SpringBoot + Docker from 0