들어가며
JUnit4부터 애너테이션을 이용한 테스트 프레임워크 지원을 시작했다. 이는 기존의 명명패턴의 단점을 제거해주며 효과적으로 동작한다. 다음은 Test라는 애너테이션을 정의한 코드이다.
1 2 3 4 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test {}
@Retention
은 @Test
가 런타임에도 유지되어야 한다는 의미이다. @Target
은 @Test
가 반드시 메소드 선언에만 사용되야 한다는 의미이다. 다음은 애너테이션을 적용한 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 클래스의 의미에 직접적인 영향을 주진 않는다. 그저 이 애너테이션에 관심있는 프로그램에게 추가 정보를 줄 뿐이다. 즉, 이 에너테이션에 관심이 있는 프로그램에게 특별한 처리를 할 기회를 준다. 다음 코드를 보자.
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 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으로 감싸서 던져준다. 그리고 이 예외를 잡아 실패한 원래 예외를 던져준다. 다음은 특정 예외를 던져야만 성공하는 테스트를 위한 애너테이션 코드이다.
1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable > value(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 () { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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); } } }
이 애너티이션의 타입은 Throwalbe을 확장한 클래스의 Class 객체를 모두 받을 수 있으며 따라서 모든 예외 타입을 다 수용한다. 여러개의 예외를 받는 에너테이션은 어떻게 만들까? 다음 코드를 보자.
1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable >[] value(); }
1 2 3 4 5 6 7 public class Sample3 { @ExceptionTest({ArithmeticException.class, SomeOtherException.class}) public static void m1 () { int i = 0 ; i = i / 1 ; } }
1 Class<? extends Throwable >[] excType = m.getAnnotation(ExceptionTest.class).value();
자바 8에서는 다른 방식으로도 만들 수 있다. @Repeatable
메타 애너테이션을 달면 된다. 단 주의할점이 있다. 첫째로 @Repetable
을 단 애너테이션을 반환하는 컨테이너 애너테이션 을 하나 더 정의하고 @Repetable
에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다. 둘째로 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메소드를 정의해야 한다. 마지막으로 보전 정책과 적용 대상을 명시해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Repetable(ExceptionTestContainer.class) public @interface ExceptionTest { Class<? extends Throwable >[] value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { ExceptionTest[] value(); }
1 2 3 @ExceptionTest(SomeException1.class) @ExceptionTest(SomeException2.class) public static void someMethod () { ... }
처리도 주의를 요한다. 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 컨테이너 애너테이션 타입이 적용 된다. getAnnotationsByType 메소드는 이 둘을 구분하지 않아 반복가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져오지만 isAnnotationPresent 메소드는 이를 구분한다. 따라서 반복가능 애너테이션을 여러번 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사하면 아니라고 말한다. 컨테이너가 달렸기 때문이다. 그래서 애너테이션을 여러 번 단 메소드를 무시하고 건너뛴다. 같은 이유로 isAnnotationPreset으로 컨테이너 애너테이션이 달렸는지 검사하면 반복 가능 애너테이션을 단 하나만 단 메소드들은 무시하고 지나친다. 이를 방지하려면 이 둘을 따로 처리해야 한다.
1 if (m.isAnnotationPresent(ExceptionTest.class), m.isAnnotationPresent(ExceptionTestController.class)) { ... }