명명 패턴은 테스트 메소드 이름을 test + xxx 처럼 짓는 것을 의미한다.

명명 패턴의 단점은 다음과 같다

  • 오타가 나면 안된다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다
  • 메소드가 아닌 클래스이름을 Test로 짓고 사용하면 JUnit은 구별하지 못한다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다는 것이다.JUnit이 구분을 하지 못한다.
  • 특정 예외를 던져야하는 테스트에서 예외 타입을 테스트에 매개변수로 던지고 싶지만

annotation을 통해 모든 문제를 해결할 수 있다.

 

Test라는 이름의 어노테이션을 정의한다고 해보자

예외가 발생하면 해당 테스트를 실패로 처리한다.

import java.lang.annotation.*;

/**
 * 테스트 메서드임을 선언하는 애너테이션이다.
 * 매개변수 없는 정적 메서드 전용이다.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

@Test 어노테이션에 @Retention과 @Target 어노테이션이 선언되어 있는데

이처럼 어노테이션 선언에 다는 어노테이션을 메타어노테이션이라 한다.

 

Retention은 유지라는 의미로

@Retention(RetentionPolicy.RUNTIME) 메타 어노테이션은 @Test가 런타임에도 유지되어야 함을 나타내며

이 메타어노테이션이 없으면 테스트 도구는 @Test를 인식할 수 없다.

 

@Target(ElementType.METHOD) 메타 어노테이션은 @Test가 반드시 메소드 선언에만 사용되야 함을 나타내며

클래스 선언이나 필드 선언등 다른 프로그램 요소에는 달 수 없다는 것을 의미한다.

 

다음 코드는 @Test 어노테이션을 적용한 모습이며 아무 매개변수 업이 단순히 대상에 마킹한다는 뜻에서

마커 어노테이션이라 한다.

 

이 어노테이션을 사용하면 Test 이름에 오타를 내거나 메소드 선언 외에 프로그램 요소에 달면 컴파일 오류를 내준다.

public class Sample {
    @Test public static void m1() { }// 성공해야 한다.
    public static void m2() { }

    @Test public static void m3() {// 실패해야 한다.
        throw new RuntimeException("실패");
    }

    public static void m4() { }  // 테스트가 아니다.

    @Test public void m5() { }   // 잘못 사용한 예: 정적 메서드가 아니다.

    public static void m6() { }

    @Test public static void m7() {    // 실패해야 한다.
        throw new RuntimeException("Crash");
    }

    public static void m8() { }
}

우리가 Test 어노테이션을 만들때 정적 메소드 전용이라 주석으로 표시해놨지만

실제 별도의 로직을 구현해놓은것이 아니라 m5()와 같은 오류가 발생할 수 있다.

 

@Test 어노테이션이 Sample 클래스의 의미에 직접적인 영향을 주지는 않는다. 그저 이 어노테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐이다.

 

 

다음 예시를 봐보자

import java.lang.reflect.*;

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 + " 실패: " + exc);
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }
        System.out.printf("성공: %d, 실패: %d%n",
                passed, tests - passed);
    }
}

이 테스트 러너는 명령줄로부터 완전 정규화된 클래스 이름을 받아,

그 클래스에서 @Test 어노테이션이 달린 메소드를 차례로 호출한다.

 

isAnnotationPresent가 실행할 메소드를 찾아주는 메소드이다.

 

만약 테스트가 예외를 던지면 java reflection이 InvocationTargetException으로 감싸서 다시 던진다.

이로써 예외에 담긴 실패 정보를 추출해(getCause) 출력한다.

 

 

하지만 이 코드는 모든 예외에 대해 받아들이는데 특정 예외에만 반응하게는 어떻게 만들까?

import java.lang.annotation.*;

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value(); //매개변수타입은 Class<...>이다.
}

이 어노테이션은 Throwable을 확장한 클래스의 Class 각체라는 뜻이며 모든 예외 타입을 수용한다.

import java.util.*;

// 코드 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽)
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {  // 성공해야 한다.
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // 실패해야 한다. (다른 예외 발생)
        int[] a = new int[0];
        int i = a[1]; //아마 IndexOutofRange일것이다.
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 실패해야 한다. (예외가 발생하지 않음)
}

이제 이 어노테이션을 다루는 테스트 도구이다.

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(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%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(
                                "테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
                                m, excType.getName(), exc);
                    }
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @ExceptionTest: " + m);
                }
            }
        }

        System.out.printf("성공: %d, 실패: %d%n",
                passed, tests - passed);
    }
}

이 코드는 어노테이션 매개변수의 값을 추출하여 테스트 메소드가 올바른 예외를 던지는지 확인하는데 사용한다.

예외를 매개변수 하나가 아닌 배열로 받을 수도 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}

자바 8에서는 여러개의 값을 받는 어놑에ㅣ션을 다른 방식으로도 만들 수 있는데

배열 매개변수를 사용하는 대신 어노테이션에 @Repeatable 메타어노테이션을 다는 방식이다.

 

주의할점은

  • @Repeatable을 단 어노테이션을 반환하는 컨테이너 어노테이션을 하나더 정의해야하고
  • @Repeatable에 이 컨테이너 어노테이션의 class 객체를 매개변수로 전달해야 한다.
  • 컨테이너 어노테이션은 내부 어노테이션 타입의 배열을 반환하는 value 메소드를 정의해야한다.
  • 컨테이너 어노테이션 타입에는 적절한 보존 정책(Retention)과 적용대상(Target)을 명시해야한다.
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

정리

이러한 예시들을 통해 우리는 어노테이션으로 처리할 수 있는 일을

명명패턴으로 사용할 필요가 없다는 것을 알 수 있다.

 

어노테이션의 정의를 한번더 살펴보고 이번 아이템을 마무리한다.

 

어노테이션 : 데이터를 위한 데이터, 즉 메타 데이터(Meta data)이며 주석역할을 하는 동시에 추가적인 기능을 제공한다.

컴파일 과정에서 추가적으로 처리할 작업을 알려주는 컴퓨터를 위한 주석인 것이다.

+ Recent posts