요약
애노테이션 사용하자! 그리고 자바가 제공하는 애노테이션은 잘 알아두자.
명명패턴
이름 짓기에 패턴을 만드는 방법을 의미한다. 예시로 Junit 3까지는 테스트 메서드 이름을 testA() 처럼 test로 시작했어야 했다.
단점
- 오타가 나면 안된다
- 실수로 메서드 이름을 tsetSafetyOverride로 지어도 Junit 3은 이 메서드를 테스트 메서드로 인식 못하고 넘어간다. 하지만 개발자는 테스트가 성공해서 넘어간 것으로 오해할 수 있어 문제 발생의 가능성이 존재한다.
- 올바른 프로그램 요소(메서드)에서만 사용되리라 보증할 방법이 없다
- 메서드가 아닌 클래스 이름을 TestSafety로 설정한다 가정하면, 클래스 이름이 test로 시작하지만 메서드가 아닌 클래스에 설정된 이름이기 때문에 이또한 Junit 3가 인지하지 못한다. 하지만 개발자는 테스트에 사용하는 클래스 목적으로 TestSafety라는 이름을 설정했기에, 문제가 발생할 가능성이 또한 존재한다.
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다
- 특정 예외를 던져야 성공하는 테스트인 경우, 예외를 넘겨줘야 하는데 명명패턴에서는 넘길 수 없다. 또한 명명패턴에서는 예외의 이름을 테스트 메서드 이름에 덧붙이는 방법도 있지만, 이는 보기도 나쁘고 깨지기도 쉽다.
→ 위 모든 문제를 애너테이션이 해결할 수 있고 Junit 4부터 애너테이션을 사용할 수 있다.
마커 애너테이션
마커 애너테이션은 아무 매개변수 없이 단순히 대상에 마킹하는 목적으로 사용하는 애너테이션이다.
@Test
/**
* 테스트 메서드임을 선언하는 에너테이션이다.
* 1) 매개변수 없는 2)정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
- @Test를 설정하는 코드이고, @Test를 마커 애너테이션이라고 한다.
- 메타 애너테이션 : 애너테이션 선언에 있는 애너테이션을 의미
- @Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야 함
- @Target(ElementType.METHOD) : @Test가 반드시 메서드 선언에서만 사용되어야 함
- 매개변수 없는 정적 메서드 전용이다.
- 인스턴스 메서드 or 매개변수가 있는 메서드에서는 사용 못한다
- 장점
- 규칙 어길 시 컴파일 오류가 발생해 안정성 보장
마커 애너테이션 예시
public class Sample {
@Test // Test 도구가 체크하는 메서드 & 성공 케이스
public static void m1() {
}
@Test // Test 도구가 체크하는 메서드 & 실패 케이스
public static void m2() {
throw new RuntimeException("실패");
}
@Test // Test 도구가 체크하는 메서드 & 잘못 사용한 케이스(정적 메서드가 아님)
public void m3() {
}
// Test 도구가 체크하지 않는 메서드
public static void m4() {
}
}
- @Test
- m1, m2, m3 메서드는 @Test 애너테이션이 설정되어 있어 Test 도구가 체크할 수 있는 메서드이다
- 이 중에서 성공 케이스와 실패 케이스로 나뉘는데, 실패 케이스는 예외를 throw 한다
- m4()처럼 정적 메서드 임에도 @Test 애너테이션이 설정되어 있지 않다면, Test 도구가 체크하지 않는다
- 정적 메서드
- @Test 애너테이션을 설정하더라도, 설정된 메서드가 static(정적)이 아니면 잘못 사용한 예시다
@Test
이 애너테이션은 클래스에 직접적인 영향은 주지 않고, 플러스 알파로 해당 애너테이션에 관심 있는 도구에게 추가 정보를 제공한다.
- 마커 애너테이션을 처리하는 프로그램
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0; // 실행된 테스트 개수
int passed = 0; // 실행된 것 중 통과한 테스트 개수
Class<?> testClass = Class.forName(args[0]); // 명령줄로부터 완전 정규화된 클래스 불러옴
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) { // @Test 선언한 메서드 차례로 호추
tests++;
try {
method.invoke(null);
passed++;
} catch (InvocationTargetException wrappedException) {
Throwable cause = wrappedException.getCause();
System.out.println(method + " 실패 : " + cause);
} catch (Exception e) {
System.out.println("잘못 사용한 @Test : " + method);
}
}
}
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
- InvocationTargetException
- 테스트 예외를 InvocationTargetException으로 감쌈
- 그래서 테스트 예외를 알아내기 위해 wrappedException.getCause() 호출
- 이 예외가 발생하면 @Test 애너테이션을 잘못 사용한 것을 의미
- 정적 메서드가 아닌 인스턴스 메서드, 매개변수가 있는 메서드, 호출할 수 없는 메서드 등
매개변수 하나를 받는 애너테이션
특정 예외를 던져야만 성공하는 테스트를 지원하기 위해 예외를 매개변수로 받는 애너테이션 @ExceptionTest를 만들어야 한다.
- @ExceptionTest
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
- 애너테이션 매개변수 타입 : Class<? extends Throwable>(한정적 타입 토큰)
- Throwable을 상속한 하위 타입 모두 해당됨을 의미한다
- 모든 예외와 오류 타입을 수용한다
예시
public class Sample2 {
// 성공 케이스
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 1;
i /= i;
}
// 실패 케이스 - @ExceptionTest 에 설정한 Arithmetic 예외가 아닌 다른 예외 발생
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1]; // IndexOutOfBoundException
}
// 예외가 발생하지 않는 케이스
@ExceptionTest(ArithmeticException.class)
public static void m3() { }
}
활용
- 코드
public class RunTests2 {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(ExceptionTest.class)) { // @Test 선언한 메서드 차례로 호추
tests++;
try {
method.invoke(null);
System.out.printf("테스트 %s 실패 : 예외를 던지지 않음%n", method);
} catch (InvocationTargetException wrappedException) {
Throwable exceptionCause = wrappedException.getCause();
Class<? extends Throwable> exceptionType =
method.getAnnotation(ExceptionTest.class).value();
if (exceptionType.isInstance(exceptionCause)) { // 기대한 예외 == 발생한 예외
passed++;
} else { // 기대한 예외 != 발생한 예외
System.out.printf("테스트 %s 실패 : 기대한 예외 %s, 발생한 예외 %s%n", method, exceptionType.getName(), exceptionCause);
}
System.out.println(method + " 실패 : " + exceptionCause);
} catch (Exception e) {
System.out.println("잘못 사용한 @Exceptiontest : " + method);
}
}
}
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
- @Test 애너테이션 실행 코드와 다른 부분
- 애너테이션의 매개변수 값을 추출해 테스트 메서드가 올바른 예외를 던지는지 확인하는데 애너테이션을 사용한다
여러 개의 예외를 받는 애너테이션
- @MultipleExceptionTest
/**
* 배열 매개변수를 받는 애너테이션 타입
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MultipleExceptionTest {
Class<? extends Throwable>[] value();
}
활용
- 코드
public class RunTests3 {
// 매개변수로 예외 배열 넘김
@MultipleExceptionTest({IndexOutOfBoundsException.class,
NullPointerException.class})
public static void doubleBad() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(MultipleExceptionTest.class)) { // @Test 선언한 메서드 차례로 호추
tests++;
try {
method.invoke(null);
System.out.printf("테스트 %s 실패 : 예외를 던지지 않음%n", method);
} catch (InvocationTargetException wrappedException) {
Throwable exceptionCause = wrappedException.getCause();
int oldPassed = passed;
Class<? extends Throwable>[] exceptionTypes =
method.getAnnotation(MultipleExceptionTest.class).value();
for (Class<? extends Throwable> exceptionType : exceptionTypes) {
if (exceptionType.isInstance(exceptionCause)) {
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("테스트 %s 실패 : %s %n", method, exceptionCause);
}
System.out.println(method + " 실패 : " + exceptionCause);
} catch (Exception e) {
System.out.println("잘못 사용한 @Test : " + method);
}
}
}
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
- 여러 개의 값을 받는 애너테이션
- 배열 매개변수
- @Repeatable 메타 애너테이션
- 장점 : 가독성 향상
- 단점
- 애너테이션 선언/처리 코드양 증가
- 처리가 복잡해서 오류 발생 가능성 증가
'Dev Language > EffectiveJava' 카테고리의 다른 글
[EffectiveJava] 익명 클래스보다는 람다를 사용하라 (2) | 2025.02.16 |
---|---|
[EffectiveJava] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2025.02.09 |
[EffectiveJava]비트 필드 대신 EnumSet을 사용하라 (2) | 2025.01.25 |
[EffectiveJava] int 상수 대신 열거 타입(Enum)을 사용하라 (2) | 2025.01.18 |
[EffectiveJava] 이왕이면 제네릭 메서드로 만들라 (0) | 2025.01.11 |