On a recent project, my team inherited a large, lightly-tested Java/Spring codebase. As we began to modify the code test-first, we ran into two common obstacles that prevent unit testing in Java:
- Class methods
- Objects instantiating other classes (using the
new
keyword)
There are libraries that can help, but we wanted to tread lightly, so they weren’t an option. Instead, we decided to use the “subclass and override” technique from Michael Feathers’s very pragmatic legacy code book. Modifying code to make it more testable felt dirty at first, but the increase in test coverage, and consequently developer confidence, made it worth it.
Class Method Workaround
The following instance method invokes a class method on another class Executors.newScheduledThreadPool
. Since we can’t mock class methods, it’s impossible to unit test.
public class DatadogService {
// state and other behavior
public void start() {
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(POOL_SIZE);
try {
DatadogJob datadogJob = new DatadogJob();
scheduledExecutorService.scheduleAtFixedRate(
datadogJob,
INITIAL_DELAY,
PERIOD,
TIME_UNIT
);
} catch (Exception e) {
System.err.println("Failed to register DatadogJob");
throw new RuntimeException(e);
}
}
}
We need to introduce what Feathers calls a seam: a place where we can alter the behavior of the code. Specifically, we need an object seam, i.e., an overridable method.
Create a Seam
Extracting the class method call into a method gives us a seam. This new method is protected
, communicating that it can/should be overridden.
public class DatadogService {
// state and other behavior
public void start() {
ScheduledExecutorService scheduledExecutorService =
this.newScheduledExecutorService();
try {
DatadogJob datadogJob = new DatadogJob();
scheduledExecutorService.scheduleAtFixedRate(
datadogJob,
INITIAL_DELAY,
PERIOD,
TIME_UNIT
);
} catch (Exception e) {
System.err.println("Failed to register DatadogJob");
throw new RuntimeException(e);
}
}
protected
ScheduledExecutorService newScheduledExecutorService() {
return Executors.newScheduledThreadPool(POOL_SIZE);
}
}
Subclass and Override
In our test, we subclass the class and override the extracted method; returning a mock whose interactions we later verify.
public class DatadogServiceTest {
@Test
public
void startSchedulesFixedRateDatadogJob() {
final ScheduledExecutorService scheduledExecutorService =
mock(ScheduledExecutorService.class);
DatadogService datadogServiceWithMockedScheduledExecutorService =
new DatadogService() {
@Override
protected
ScheduledExecutorService newScheduledExecutorService() {
return scheduledExecutorService;
}
};
datadogServiceWithMockedScheduledExecutorService.start();
verify(scheduledExecutorService).scheduleAtFixedRate(
isA(DatadogJob.class),
eq(30L),
eq(60L),
eq(TimeUnit.SECONDS)
);
}
}
Class Instantiation Workaround
The following class constructor instantiates another class. Since we can’t mock the new
keyword, it’s impossible to unit test.
public class DatadogMetric {
private long value;
private long createdAt;
public DatadogMetric(long value) {
this.value = value;
this.createdAt = new Date().getTime();
}
public long[][] getPoints() {
return new long[][] {{this.createdAt, this.value}};
}
}
We need an object seam, allowing us to alter instantiation.
Create a Seam
Extracting the class instantiation into a method gives us a seam. Again, we use a protected
method to communicate overriding.
public class DatadogMetric {
private long value;
private long createdAt;
public DatadogMetric(long value) {
this.value = value;
this.createdAt = getCurrentTime();
}
public long[][] getPoints() {
return new long[][] {{this.createdAt, this.value}};
}
protected long getCurrentTime() {
return new Date().getTime();
}
}
Subclass and Override
In our test, we subclass the class and override the extracted method; returning a hard-coded value we later verify.
public class DatadogMetricTest {
@Test
public
void getPointsReturnsCreationTimestampValuePair() {
DatadogMetric datadogMetricWithCustomCurrentTime =
new DatadogMetric(250L) {
@Override
protected long getCurrentTime() {
return 1000L;
}
};
long[][] points =
datadogMetricWithCustomCurrentTime.getPoints();
Assert.assertEquals(1, points.length);
Assert.assertArrayEquals(new long[] {1000L, 250L}, points[0]);
}
}
The Role of Dependency Injection in Java
In Java, class methods aren’t inherited, and therefore not overridable; making unit-testing impossible. Class instantiation is implemented via a keyword instead of a class method, also preventing unit-testing. It’s no coincidence that dependency injection rose to prominence during the early days of TDD, when developers were trying to figure out how to unit-test their Java code in isolation.
Even though there are testing libraries to mock class methods, the Java community seems to discourage their use; instead, declaring that class methods are a procedural code smell, and not object-oriented. Although, this view seems to be more of an excuse for a language shortcoming that test frameworks couldn’t overcome.
In summary, if you want to test-drive your Java code:
- Avoid the
new
keyword - Avoid non-utility class methods (classes like Math are ok)
- Use a dependency injection framework