Tools and frameworks have often changed their treatment depending on the ** naming pattern **. For example, in JUnit prior to version 4, the test method name must start with test. This method has some drawbacks. If you make a typo, you will implicitly fail. For example, if you type tsetSafetyOverride instead of testSafetyOverride, JUnit3 will not fail, but the test will not run. Also, there is no way to guarantee that the naming pattern is used only for the proper program elements. Another drawback is that there is no good way to associate the argument value with the program element.
Annotations eliminate these drawbacks (annotations have been introduced since JUnit 4). In this chapter, we'll create a simple testing framework to see how annotations work. First, let's look at the annotation that works automatically and fails if an error is sent.
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
In the definition of Test annotation, `@ Retention``` and
`@ Target``` are added, but these are called meta annotations.
@Retention(RetentionPolicy.RUNTIME)Indicates that the Test annotation is retained at run time.
#### **`@Target(ElementType.METHOD)Indicates that the Test annotation is valid only in method declarations (not applicable to fields or classes).`**
```METHOD)Indicates that the Test annotation is valid only in method declarations (not applicable to fields or classes).
The comment in the Test annotation declaration says "Use only on parameterless static methods", but this cannot be detected by the compiler unless you write an annotation processor. See the `` `javax.annotation.processing``` documentation for more information.
The following shows how the Test annotation is actually used.
```java
// Program containing marker annotations
public class Sample {
@Test public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
Test annotations are called ** marker annotations ** because they have no arguments and simply mark the element. If you type Test or use Test annotations other than method declarations, you will get a compile error.
sample
Of the classes, m1 succeeds, m3 and m7 fail, m5 is invalid because it is not a static method, and the other four methods are not annotated with the test and are ignored by the testing framework.
The Test annotation does not directly affect the semantics of the Sample
class, it only gives information to programs that use the Test annotation.
That is, annotations do not affect the semantics of the given code, but the processing of tools such as the sample test runner below.
package tryAny.effectiveJava;
//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
The test runner tool takes the FQDN as an argument and executes the method with the Test annotation by calling `Method.invoke```. If the test method throws an exception, the reflection facility wraps the exception and throws a ```InvocationTargetException```. This tool catches this exception and gets the cause exception with the
getCause``` method. The second catch clause catches the exception due to the abuse of the Test annotation and prints out the appropriate message. The output when the ``
Sample``` class is applied to the test tool is as follows.
public static void tryAny.effectiveJava.Sample.m3() failed: java.lang.RuntimeException: Boom
Invalid @Test: public void tryAny.effectiveJava.Sample.m5()
public static void tryAny.effectiveJava.Sample.m7() failed: java.lang.RuntimeException: Crash
Passed: 1, Failed: 3
Now, let's create a new annotation that will succeed if a specific exception is thrown.
package tryAny.effectiveJava;
//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* * Indicates that the annotated method is a test method that * must throw
* the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
A bounded token (Item33) called Class <? Extends Throwable>` `` is used as an argument of this annotation, and a class that inherits `` `Throwable
can be taken as an argument. Means. The following are specific examples of use.
package tryAny.effectiveJava;
//Program containing annotations with a parameter
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
} // Should fail (no exception)
}
In addition, the test runner tool will be modified as follows to match this.
package tryAny.effectiveJava;
//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
This code extracts the arguments of the ExceptionTest
annotation and checks if the exception raised is of the same type.
Compiling the test program indicates that the exception type specified as an annotation argument is valid. However, the test runner throws a `` `TypeNotPresentException``` if the particular exception type that was at compile time is not at runtime.
We've further improved the code for this exception test to allow you to test if any of the specified exceptions are thrown.
This improvement can be easily done by making the argument type of ExceptionTest
an array.
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
Now the ExceptionTest annotation can take either one argument or multiple arguments. When describing multiple exceptions, it is as follows.
// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
exceptiontest
With the new version, the test runner tool looks like this:
package tryAny.effectiveJava;
//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
int oldPasses = passed;
Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPasses) {
System.out.printf("Test %s failed: %s%n", m, exc);
}
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
}
}
In Java8, annotations that take multiple arguments can be implemented in different ways. Instead of declaring annotations with array arguments, use `` `@ Repeatablemeta annotations so that one element can be annotated multiple times. This
@Repeatable``` meta annotation takes one argument. Its argument is a class object that has one array called containing annotation type as an argument.
The following is an example.
package tryAny.effectiveJava;
//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* * Indicates that the annotated method is a test method that * must throw
* the designated exception to succeed.
*/
// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
package tryAny.effectiveJava;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
The method of annotating is as follows.
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
@repeatable
Care must be taken when processing. Annotations that are used repeatedly are created in combination with annotations that store them.
getannotationsbytype
The method hides this and gives access to both annotations that are used repeatedly and annotations that are not. On the other handisannotationspresent
The method determines that the annotation that is used repeatedly is the annotation that stores it. That is, the type of annotation that is used repeatedlyisannotationspresent
The method can't determine that it's a recurring type.
In order to detect both annotations that are used repeatedly and annotations that are not used, it is necessary to check both the stored annotation and the element annotation.
The following is a rewrite to withstand repeated use of annotations.
// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
@repeatable
You should use it if you feel that using it enhances readability, but you should keep in mind that using it complicates the processing of repetitive annotations.
Although the testing framework in this chapter is simple, it has shown an advantage over the naming pattern of using annotations.
Most programmers do not define annotations themselves. You should use standard Java annotations and annotations provided by IDEs and tools.
Recommended Posts