In the previous Let's break away from static methods to write unit tests # 1 Principle / Wrap method, we explained the following two points.
In this article, I will introduce a method called DI (Dependency Injection) as another method for modifying existing code, using the case of calling static methods of other classes as an example.
Note that this article does not mention DI container frameworks such as the Spring Framework.
I can't write unit tests well for methods like LegacyClient # execute ()
below.
In the process of assigning the values of the variables host
and port
in the method, the instance method of the GlobalControl
class is accessed, but it requires access to the database. Therefore, an exception will occur when executed from an inaccessible environment.
LegacyClient.java
public class LegacyClient {
public void execute() {
//Here is the code to be tested
//Code in question
String host = GlobalControl.getInstance().get("host", "localhost");
int port = GlobalControl.getInstance().getInt("port", 8080);
//Here is the code to be tested
}
}
GlobalControl.java
public final class GlobalControl {
private static final GlobalControl instance = new GlobalControl();
public static GlobalControl getInstance() {
return instance;
}
public String get(String key, String defaultValue) {
//Processing that accesses the database
}
public int getInt(String key, int defaultValue) {
//Processing that accesses the database
}
}
In the previous article, I showed you how to wrap a method and solve it. The method can be applied in this case as well.
However, this method requires the creation of multiple wrap methods, which makes the code redundant.
Need to create multiple wrap methods
import com.google.common.annotations.VisibleForTesting;
public class LegacyClient {
public void execute() {
//Here is the code to be tested
//Wrapped the method in question
String host = get("host", "localhost");
int port = getInt("port", 8080);
//Here is the code to be tested
}
//Wrap method to overwrite in test class 1
@VisibleForTesting
String get(String key, String defaultValue) {
return GlobalControl.getInstance().get(key, defaultValue);
}
//Wrap method overwriting with test class 2
@VisibleForTesting
int getInt(String key, int defaultValue) {
return GlobalControl.getInstance().getInt(key, defaultValue);
}
}
Therefore, this time we will try to solve it by DI.
Just like last time, let's divide the commit and fix it step by step.
Unit tests that fail
public class LegacyClientTest {
@Test
public void test() {
new LegacyClient().execute();
}
}
Now that the unit tests are ready, we'll start the actual fix.
Extract the instance method declared in the GlobalControl
class in question (using the refactoring function of the IDE, etc.) to the interface.
Interface extracted from GlobalControl class
public interface GlobalControlHolder {
public String get(String key, String defaultValue);
public int getInt(String key, int defaultValue);
}
Give the extracted interface
public final class GlobalControl implements GlobalControlHolder {
private static final GlobalControl instance = new GlobalControl();
public static GlobalControl getInstance() {
return instance;
}
@Override
public String get(String key, String defaultValue) {
//Processing that accesses the database
}
@Override
public int getInt(String key, int defaultValue) {
//Processing that accesses the database
}
}
GlobalControl
Make sure to get the instance at the time of instance creation (constructor) instead of getting it in the method. Also, the member variables at this time should be declared in the interface GlobalControlHolder
extracted earlier.
Then, in the execute method, rewrite it so that it refers to the member variable globalControl
.
LegacyClient.java
public class LegacyClient {
//Move the instance from local to a member variable and keep it
private final GlobalControlHolder globalControl;
public LegacyClient() {
this.globalControl = GlobalControl.getInstance();
}
public void execute() {
//Here is the code to be tested
//Member variables"globalControl"Rewritten to refer to
String host = globalControl.get("host", "localhost");
int port = globalControl.getInt("port", 8080);
//Here is the code to be tested
}
}
The constructor created in step 2 had a GlobalControlHolder
instance in the constructor. In addition to that, add another constructor that takes GlobalControlHolder
as an argument.
LegacyClient.java
public class LegacyClient {
private final GlobalControlHolder globalControl;
public LegacyClient() {
this.globalControl = GlobalControl.getInstance();
}
//Constructor for DI
public LegacyClient(GlobalControlHolder globalControl) {
this.globalControl = globalControl;
}
public void execute() {
//Here is the code to be tested
String host = globalControl.get("host", "localhost");
int port = globalControl.getInt("port", 8080);
//Here is the code to be tested
}
}
This allows the caller to pass a GlobalControl
instance.
Injecting a dependent module (this time GlobalControl
) from the caller using the constructor is called ** constructor injection **.
In the test code, create a Mock class that implements the GlobalControlHolder
interface, and make it new and pass it to the constructor of LegacyClient
.
Mock class for test code
public class MockGlobalControl implements GlobalControlHolder {
@Override
public String get(String key, String defaultValue) {
//Returns the default value
return defaultValue;
}
@Override
public int getInt(String key, int defaultValue) {
//Returns the default value
return defaultValue;
}
}
Modified test code
public class LegacyClientTest {
@Test
public void test() {
//Inject Mock classes for test code
new LegacyClient(new MockGlobalControl()).execute();
}
}
This allows unit tests to replace dependent modules with implementations that do not access the database.
DI (Dependency Injection) is effective as a solution when calling a static method of another class.
Also, as in the previous time, you can write unit tests while limiting the range of influence of refactoring by modifying the steps separately.
Recommended Posts