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
.
- 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.) - 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.) - The execution order of methods (per class) can not be predicted, neither in super- nor in sub-class.
- The old naming conventions like in
setUp()
andtearDown()
don't play a role any more. - Overridden methods in super-class will not be executed unless called explicitly (this is object-oriented!).
- The
@Override
annotation doesn't play a role here (but anyway you should use it!). - 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:
Kommentar veröffentlichen