Is there a philosophical reason why jUnit 5 uses annotations rather than classes or interfaces?

47 Views Asked by At

I'm a little perplexed here as I look at jUnit 5/Jupiter (and 4).

It seems that the philosophy is to use annotations and, at runtime, to use those annotations to collect and execute test cases from the classpath.

Did the developers of jUnit have a philosophical problem with the use of interfaces and/or superclasses as a means of identifying and executing tests? My concern is that when writing unit tests, the fact that a class is being used as a test is pretty much a first-class attribute of that class. This is the sort of thing that I expect to see in a class hierarchy. The usual way of sharing code and structure through composition is through inheritance, not annotation.

Now I'm mainly a database guy and it's been a decade since I coded serious Java, so I know I'm not expert in annotations or their use or value. So if it's possible to structure the Javadoc of my code to collect up all the jUnit test classes—which I suppose use @Assertion annotations—I might be happy with that.

But for now the design approach and philosophy is eluding me. Can someone explain?

1

There are 1 best solutions below

0
rzwitserloot On

What you 'want' doesn't actually work. Or rather, it is not obvious how it would. What do you propose? Let's say I have this current test class:

class ExampleTest {
  @BeforeClass static void init() {
    someCodeThatShouldOnlyRunOnceEverNotOncePerTest();
  }

  @Test void testFoo() {
    helperMethod();
  }

  void helperMethod() {
     helperCodeHere();
  }

  @Test(expect = IllegalArgumentException.class) void bar() {}
}

This example class contains lots of things that do not translate (easily) into some sort of type hierarchy:

  • static members simply don't 'do' inheritance. There is no way to declare an abstract class or interface such that a class that extends/implements it is forced to create some sort of static method, or can 'override' one in the sense that a caller can call it without resorting to reflection. So, how do you do @BeforeClass? REASON 1: You couldn't have code that runs once across multiple related tests. The way the java ecosystem does inheritable, non-reflective per-class properties is with.. Factory. It'd be quite a drag that all test code consists only of 2 methods: One for the class-level init (in JUnit5 terms, @BeforeAll; @BeforeClass is JUnit4), and one that returns an instance of the actual class that contains the actual test methods.

  • We have a helper method here. How does the test framework figure out that testFoo is a test method, but helperMethod is not? For that matter, how does it even know that testFoo is something that needs testing in the first place? If this was a type hierarchy, we'd need @Override void test() {} as the one method you override. REASON 2: You'd either be restricted to just a single testcase per test class, or the framework needs to use reflection. In which case it also needs to adopt the anti-pattern (in that it is highly un-java-like) of magic names: If the method starts with test it's a test case, if it doesn't, it's a helper method. You complain that the annotation approach doesn't feel like proper OO, proper java. Reflection is highly irregular and very anti-OO. Assigning magic to certain patterns in the name of methods even more so. Note that this is how junit really worked prior to annotations (reflection, and magic names).

  • How does one indicate that a test is supposed to throw something, and is to be considered a failure if it does not? In JUnit 5, lambdas exist, so it's simply assertThrows(IllegalArgumentException.class, () -> codeThatShouldThrow());, but annotations predate lambdas by a decade, as does junit. JUnit4 used the style as shown in the snippet above, and couldn't use lambdas for it because they didn't exist. REASON 3: Because testing for expected exceptions required a rather unwieldy try/catch block where the end of the try block is a fail, and the catch block is empty with a comment explaining you want it to happen.

Perhaps you have really good answers to these dilemmas. In which case, by all means, share, or, write your own framework. Annotations as ways to structure and 'label' code is not exactly without its problems, it's the best amongst a limited set of ways to go. If you have a neat way to sidestep it all, you should try.

Note that TestNG is 'the other' java testing framework. It also uses annotations, and was explicitly created because its author was unhappy with JUnit's API and approach. In other words, you wouldn't be the first, but the other fellow (Cédric Beust, a somewhat famous java programmer who has also authored a few other fairly commonly used third party deps such as JCommander) didn't find a way away from annotations.