명명 패턴의 대표적인 예로 JUnit 3까지는 테스트 메서드 이름을 test로 시작하지 않으면 이 메서드를 그냥 지나쳤서 테스트를 통과했다고 오해하는 경우도 있었습니다. JUnit 4부터는 이러한 문제점을 해결하기 위해 애너테이션을 전면 도입했습니다. 이번 예제에서는 자동으로 수행되는 간단한 테스트용 애너테이션으로, 예외가 발생하면 해당 테스트를 실패로 처리합니다.
1
2
3
4
5
6
7
8
/**
* 테스트 메서드임을 선언하는 애너테이션
* 매개변수 없는 정적 메서드 전용
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceTest{}
@Retention, @Target과 같이 애너테이션 선언에 다는 애너테이션을 메타애너테이션이라 합니다. 쉽게 설명하면 메타 애너테이션은 애너테이션을 위한 애너테이션이라고 생각하시면 됩니다. @Retention은 애너테이션이 유지되는 기간이며 @Retention(RetentionPolicy.RUNTIME)은 런타임까지 존재한다는 뜻입니다. @Target은 적용 대상을 지정할 때 사용하며 @Target(ElementType.METHOD)은 메서드 선언에만 사용할 수 있다는 뜻입니다. 위 코드에서 “매개변수 없는 정적 메서드 전용"라고 주석을 작성했는데, 적절한 애너테이션 처리기를 직접 구현하지 않으면 컴파일 오류 없이 잘 작동합니다.
publicclassSample{@Testpublicstaticvoidm1(){}// 성공해야 한다
publicstaticvoidm2(){}@Testpublicstaticvoidm3(){// 실패해ㅑ 한다
thrownewRuntimeException("Boom");}publicstaticvoidm4(){}@Testpublicvoidm5(){}// 잘못 사용: 정적 메서드가 아님
@Testpublicstaticvoidm7(){// 실패해야 한다
thrownewRuntimeException("Crash");}publicstaticvoidm8(){}}
@Test 애너테이션은 Sample클래스에 직접적인 영향을 주지 않습니다. 그저 이 애너테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐입니다.
// 마커 애너테이션을 처리하는 코드
publicclassRunTest{publicstaticvoidmain(String[]args)throwsException{inttests=0;intpassed=0;Class<?>testClass=Class.forName(args[0]);for(Methodm:testClass.getDeclaredMethods()){if(m.isAnnotationPresent(Test.class)){tests++;try{m.invoke(null);passed++;}catch(InvocationTargetExceptionwrappedExc){Throwableexc=wrappedExc.getCause();System.out.println(m+" failed: "+exc);}catch(Exceptionexc){System.out.println("Invalid @Test: "+m);}}}System.out.printf("Passed: %d, Failed: %d%n",passed,tests-passed);}}
@Test 애너테이션이 달린 메서드를 차례로 호출합니다. isAnnotationPresent메서드가 실행할 메서드를 찾아줍니다. InvocationTargetException외의 예외가 발생한다면 @Test 애너테이션을 잘못 사용했다는 뜻입니다.인스턴스 메서드, 매개변수가 있는 메서드, 호출할 수 없는 메서드 등에 사용했다는 뜻입니다.
이번에는 특정 예외를 던져야만 성공하는 테스트를 지원하도록 해봅시다.
1
2
3
4
5
6
7
8
/**
* 명시한 예외를 던져야면 성공하는 테스트 메서드용 에너테이
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceExceptionTest{Class<?extendsThrowable>value();}
Throwable을 확장한 클래스의 Class 객체라는 뜻이며, 따라서 모든 예외,오류 타입을 수용합니다. 이는 한정적 타입 토큰을 활용한 사례입니다. 다음은 이 애너테이션을 실제 활용한 모습입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicclassSample2{@ExceptionTest(ArithmeticException.class)publicstaticvoidm1(){inti=0/0;}@ExceptionTest(ArithmeticException.class)publicstaticvoidm2(){int[]a=newint[0];// 실패해야 한다. (다른 예외 발생)
inti=a[1];}@ExceptionTest(ArithmeticException.class)publicstaticvoidm3(){}// 실패해야 한다. (예외 발생 x)
}
이제 이 애너테이션을 다룰 수 있는 코드를 작성해봅시다. (앞서 작성한 코드의 일부분을 수정했습니다)
자바 8에서는 여러 개의 값을 받는 애너테이션을 다른 방식으로 만들 수 있습니다. 배열 매개변수를 사용하는 대신 애너테이션에 @Repeatabl 메타 애너테이션을 다는 방식입니다.
@Repeatabl를 사용 할 때 주의사항이 있습니다. @Repeatable을 단 애너테이션을 반환하는 ‘컨테이너 애너테이션’을 하나 더 정의하고, @Repeatable에 컨테이너 애너테이션에 class객체를 매개변수로 전달해야 합니다. 또 컨테이션 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해아 합니다.