coding dependency_injection java labs tdd

Test After in Java: Subclass and Override

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