도구나 프레임워크가 특별히 다뤄야 하는 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해 왔다. 예를 들어 테스트 프레임워크인 JUnit3 에서는 테스트 메서드 이름을 test
로 시작하게끔 지어야 했었다.
하지만 이 경우 다음과 같은 문제가 있다.
JUnit4 부터 어노테이션을 이용한 테스트 프레임워크 지원을 시작했다. 이는 위와 같은 단점들을 제거해주며 효과적으로 동작한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)
은 Runtime 에도 어노테이션이 유지되어야 한다는 의미이다.@Target(ElementType.METHOD)
은 @Test
어노테이션이 메서드에서만 선언되어야 한다는 의미이다.public class Sample {
@Test
public static void m2() {}
@Test
public static void m3() {
throw new RuntimeException("Failure");
}
public static void m4() {}
@Test
public void m5() {}
public static void m6() {}
@Test
public static void my() {
throw new RuntimeException("Failure");
}
public static void m8() {}
}
@Test
어노테이션이 Sample 클래스에 직접적인 영향을 주는 것은 아니다. 그저 이 어노테이션에 관심이 있는 프로그램들에게 추가 정보를 줄 뿐이다. @Test
어노테이션에 관심이 있는 프로그램에게 특별한 처리를 할 기회를 준다.public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Sample.class;
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 + " Failure : " + exc);
} catch (Exception exc) {
System.out.println("Wrong Useage @Test : " + m);
}
}
}
System.out.printf("Success: %d, Fauilure: %d%n", passed, tests - passed);
}
}
InvocationTargetException
으로 감싸서 다시 던진다. InvocationTargetException
을 잡아 원래 예외에 담긴 실패 정보를 추출해(getCause) 출력한다.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 0;
i = i / 1;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {
}
}
Class<?> testClass = Sample2.class;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test Failed %s : Did not Throw Exception");
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
if(excType.isInstance(exc))
passed++;
else
System.out.printf("Test %s Failed : Expected %s but %s%n",m, excType.getName(), exc);
} catch (Exception exc) {
System.out.println("Wrong Useage @ExceptionTest : " + m);
}
}
}
Throwable
을 확장한 클래스의 Class 객체를 모두 받을 수 있다. 따라서 모든 예외 타입을 다 수용할 수 있다.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable>[] value();
}
public class Sample3 {
@ExceptionTest({ArithmeticException.class, SomeOtherException.class})
public static void m1() {
int i = 0;
i = i / 1;
}
}
if(m.isAnnotaionPresent(ExceptionTest.class)) {
test++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
int oldPassed = passed;
for(Class<? extends Throwable> excType : excTypes) {
if(excType.isInstance(exc)) {
passed++;
break;
}
}
if(passed == oldPassed) {
System.out.println("테스트 %s 실패: %s %n", m, exc);
}
} catch (Exception e) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
배열
로 예외 타입을 받아서 지정한 예외 클래스 중에 맞는 클래스가 있는지 확인한다.자바 8 에서는 다른 방식으로도 만들 수 있다. @Repeatable
메타 어노테이션을 달면 된다. 단 주의할 점이 있다.
첫째는 @Repeatable
을 단 어노테이션을 반환하는 컨테이너 어노테이션
을 하나 더 정의하고 @Repeatable
에 이 컨테이너 어노테이션
의 Class 객체를 매개변수로 전달해야 한다.
둘째는 컨테이너 어노테이션
은 내부 어노테이션 타입의 배열을 반환하는 value 메소드를 정의해야 한다.
마지막으로, 컨테이너 어노테이션
에는 @Retention
과 @Targer
을 명시해야 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void m1() {...}
반복 가능 어노테이션은 처리할 때 주의가 필요하다. 반복 가능 어노테이션을 여러개 달면 하나만 달았을 때와 구분하기 위해 해당 컨테이너 어노테이션
타입이 적용된다.
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests = m.getAnnotationByType(ExceptionTest.class);
for(ExceptionTest excType : excTypes) {
if(excType.isInstance(exc)) {
passed++;
break;
}
}
if(passed == oldPassed) {
System.out.println("테스트 %s 실패: %s %n", m, exc);
}
}
getAnnotationByType
메서드는 이 둘을 구분하지 않아서 @ExceptionTest
와 @ExceptionTestContainer
를 모두 가져온다.isAnnotationPresent
는 둘을 구분한다.@ExceptionTest
를 여러번 단 다음, isAnnotationPresent
로 ExceptionTest
를 검사하면 @ExceptionTestContainer
로 인식하기 때문에 false가 나온다.@ExceptionTest
를 한번만 단 다음에 isAnnotationPresent
로 ExceptionTestContainer
를 검사하면 @ExceptionTest
가 적용되었기 때문에 false가 나온다.반복 가능 어노테이션을 사용한 경우에는 getAnnotationByType
을 사용해 어노테이션 정보를 가져오는 것이 좋다.