Monday, February 22, 2016

Self-Testing Java Classes

Way back in the COM days when everyone was talking about components and component marketplaces, I first heard of the idea of building tests directly into the production code. That potentially brings together a bunch of different software engineering disciplines that are near and dear to my heart -- testing, reusability, and design.

Last weekend, I finally got time to start playing with an idea I've been kicking around for a long time -- self-testing Java classes. My approach -- still in what I consider the "prototype" stage -- is based on a series of annotations on methods. With Java 8, we can finally stick multiple annotations of the same type on annotation targets. This is fortunate, because you'll see that this is kind of a prerequisite of my implementation. To explain, it's probably best to start from the outside in.

Here are a few simple tests for an absolute-value function:

  @Comparison(inputs = {"50"}, output="50")
  @Comparison(inputs = {"-40"}, output="40", description="Abs of a negative")
  @Comparison(inputs = {"-40"}, output="0", type=Type.GREATER_THAN_OR_EQUALS)
  public int abs(int input) {
    return input >= 0 ? input : -input;
  }

Each @Comparison annotation translates to a unit test case where the given inputs are passed to the annotated method and the specified output is expected. There is an optional comparison type, which defaults to EQUALS.

The next question deals with how these tests are executed. I wrote a custom JUnit Runner that:
  1. Looks for methods in the test class marked with @SelfTestMethod.
  2. Invokes self-testing on the object(s) those methods return.
  3. Reports the results in typical Unit style.
Thus, an entire JUnit test suite can look like this:

@RunWith(SelfTestRunner.class)
public class MathOperationsTest {
  @SelfTestMethod(name = "Default math operations")
  public MathOperations selfTestMathOperations() {
    return new MathOperations();
  }
}

After a run in Eclipse:

 

 Let's talk about where this potentially works well, and where it doesn't. If you have side-effect-free methods (in the style advocated by functional-programming gurus) that don't require a lot of test setup, you might be able to effectively test them this way.

On the other hand, if you need to set up a lot of state before executing the test, or your class requires one or more collaborators that need to be mocked/stubbed/faked, this probably won't be the way to go. And currently there is no support for method inputs that are not Java Strings or primitives (although this is high on my list to address).

That said, I plan to continue extending this work and seeing how many different scenarios I can push it into. Naturally, ideas are welcome, so feel free to reach out here or on Github.

No comments: