Blog-Archiv

Samstag, 4. Juli 2020

Unit Tests in Object-Oriented Java with Annotations Language

As an object-oriented software-developer you know what happens when you override a method: the super-implementation will not be executed unless you call it explicitly. But what about annotations on a class or method? Are they inherited to a sub-class, or do I have to repeat all annotations in the override? Sometimes the annotation concepts seem to collide with OO-concepts.

Annotations, as used in Java, are not part of object-oriented thinking. It is a separate programming-language with its own rules. An annotation can not extend another annotation, but it can be annotated by another, and thus aggregate semantic.

What was done through naming-conventions in old times, nowadays is done through annotations. They were introduced to let weave in aspects. Developer-made annotations work through reflection at runtime, mostly without compiler check, what makes them dangerous.

Let's look at unit-testing with JUnit. First it built on naming-conventions (every test method name had to start with "test"), today it works through annotations (every test method has to carry a @Test annotation). By the way, naming-conventions are a mess, because mostly just the author knows them:-) So let's see how messy JUnit annotations are.

Annotations and Inheritance

Unit Test

Following shows a unit-test and its super-class, the super-class containing default-implementations, the sub-class overriding some of them. The @Before annotation teaches the JUnit-4 runner to execute any such annotated method before each test (-method), @After methods would be executed afterwards. The actual test in TestJUnitAnnotations source below is the @Test test() method.

So, what would you guess the output is when run with JUnit-4?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.junit.After;
import org.junit.Before;

abstract class AbstractTestJUnitAnnotations
{
    @Before
    public void init() {
        System.err.println("AbstractTestJUnitAnnotations: @Before init()");
    }

    @Before
    public void setup() {
        System.err.println("AbstractTestJUnitAnnotations: @Before setup()");
    }

    @Before
    public void setUp() {
        System.err.println("AbstractTestJUnitAnnotations: @Before setUp()");
    }

    @After
    public void exit() {
        System.err.println("AbstractTestJUnitAnnotations: @After exit()");
    }

    @After
    public void teardown() {
        System.err.println("AbstractTestJUnitAnnotations: @After teardown()");
    }

    @After
    public void tearDown() {
        System.err.println("AbstractTestJUnitAnnotations: @After tearDown()");
    }
}

I defined six annotated methods, three @Before and three @After. Two of them conform to the old naming-convention setUp() and tearDown(), to see whether they are preferred in any way by JUnit-4. Then I used the Java case-sensitivity in setup() and setUp() to confuse JUnit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TestJUnitAnnotations extends AbstractTestJUnitAnnotations
{
    @Before
    public void init() {
        System.err.println("TestJUnitAnnotations: @Before @Override init()");
    }

    public void setUp() {
        System.err.println("TestJUnitAnnotations: @Before @Override setUp()");
    }

    public void exit() {
        System.err.println("TestJUnitAnnotations: @After @Override exit()");
    }

    @After
    public void tearDown() {
        System.err.println("TestJUnitAnnotations: @After @Override tearDown()");
    }

    
    @Test
    public void test() {
        System.err.println("TestJUnitAnnotations: @Test test()");
    }
}

The extension class overrides just four of the super-class methods, to see whether super-class methods are actually called before sub-class methods. Two of them carry no annotation, to see whether repeating annotations is necessary for JUnit-4, or overriding is enough. I left out any @Override annotation to see whether this plays a role.

JUnit guarantees that the @Before methods in super-class will be executed before those in a sub-class, unless they have been overridden. No rules about the execution order of several @Before methods exist.

Result

Here is the output of TestJUnitAnnotations when run with JUnit-4:

AbstractTestJUnitAnnotations: @Before setup()
TestJUnitAnnotations: @Before @Override setUp()
TestJUnitAnnotations: @Before @Override init()
TestJUnitAnnotations: @Test test()
TestJUnitAnnotations: @After @Override tearDown()
AbstractTestJUnitAnnotations: @After teardown()
TestJUnitAnnotations: @After @Override exit()

What We Learn

When I say "method" in the following, I mean methods annotated by @Before.

  1. First the super-class methods are executed, then those of the sub-class.
    (Reversely, the super-class @After methods will be executed after those in sub-class.)

  2. Only when a sub-class overrides some method, this method may be executed before some other super-method.
    (Reversely, some sub-class @After method may be executed after a super-method.)

  3. The execution order of methods (per class) can not be predicted, neither in super- nor in sub-class.

  4. The old naming conventions like in setUp() and tearDown() don't play a role any more.

  5. Overridden methods in super-class will not be executed unless called explicitly (this is object-oriented!).

  6. The @Override annotation doesn't play a role here (but anyway you should use it!).

  7. For JUnit, you don't need to repeat the @Before or @After annotation in the sub-class override.

The last sentence is true for JUnit-4, but could be different for other libraries, maybe forcing you to duplicate any annotation in a sub-class. You need knowledge about the library that defines the annotations to understand what happens. Inheritance should be indicated by the Java built-in @Inherited annotation on @interface Before, but wasn't done in this case.

Also the fact that "Overridden methods in super-class will not be executed unless called explicitly" is true for JUnit-4 but could be different for other libraries. Yes, annotations make your software a sucking expert world!

Note

The shown test would not work with JUnit-5. There you need @BeforeEach instead of @Before. The test would compile, but the annotations would simply be ignored at runtime. Happy rewriting!

Conclusion

Spring is another framework based on reflection and annotations. You can not understand a Spring Boot application without intimate knowledge of Spring annotations. Object-oriented knowledge about Java is not sufficient when annotations take over execution control.

As annotations work through reflection at runtime, they could turn Java into a script language. You need a 100% test coverage for such software. JavaScript/ES6 programmers may understand what I mean.

Java was called "Smalltalk for the masses". In Smalltalk, unlike in Java, type checks happen at runtime, not at compile time, like it is in script languages. Surely one of the reasons why Smalltalk is not used any more. Will Java go the same way?




Keine Kommentare: