티스토리 뷰

728x90
반응형

명명패턴보다 애너테이션을 사용하라


  • JUnit은 버전 3까지 테스트 메서드 이름을 test로 시작하게끔 하였습니다.

 

🧨 단점

  • 오타에 취약합니다.
    • JUnit 3버전에서는 테스트 메서드명을 tsetSafeOverride로 지어버리면 무시하고 테스트를 진행하지 않습니다.
  • 올바른 프로그램 요소에서만 사용되리라는 보장이 없습니다.
    • 메서드가 아닌 클래스명을 TestSafeOverride로 지어 내부의 메서드가 테스트되길 기대할 수 있지만 JUnit은 클래스명에는 관심이 없으므로 테스트가 실행되지 않습니다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없습니다.
    • 특정 예외를 던져야 발생하는 테스트가 있다는 가정하에 기대하는 예외 타입을 테스트에 매개변수로 전달해야하는데 방법이 마땅치 않습니다.

 

💡 해결책 - 애너테이션(마커 애너테이션)

  • JUnit4에서 도입된 애너테이션은 명명패턴의 단점을 해결해줍니다.
  • 해당 KDGTest 애너테이션에 다른 애너테이션이 선언되어 있는데 이를 메타 애너테이션이라고 합니다.
  • KDGTest 애너테이션은 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻에서 마커 애너테이션이라고 합니다.
  • 추가적인 애너테이션의 종류가 궁금하시다면 애너테이션 살펴보기 >> 이동
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface KDGTest {

}

 

💡 사용 예시

  • 성공 1, 실패 2, 잘못 사용 1, 무시 4
  • 해당 @KDGTest 애너테이션은 Sample 클래스의 의미에 직접적으로 영향을 주지 않습니다. 그저 애너테이션에 관심이 있는 프로그램에게 추가 정보를 제공할 뿐입니다. 
  • 즉 애너테이션이 사용된 코드를 두면 해당 애너테이션에 관심이 있는 IDE에서 특별한 처리를 할 기회를 준다는 것입니다.
public class Sample {

    @KDGTest
    public static void m1() {} // 성공

    public static void m2() {} // 무시

    @KDGTest
    public static void m3() {
        throw new RuntimeException("fail"); // 실패
    }

    public static void m4() {} // 무시

    @KDGTest
    public void m5() {} // 무시 (잘못 사용된 예: static 메서드가 아님)

    public static void m6() {} // 무시

    @KDGTest
    public static void m7() {
        throw new RuntimeException("fail"); // 실패
    }

    public static void m8() {} // 무시
}

public class RunTests {
    public static void main(String[] args) throws ClassNotFoundException{
        int tests = 0;
        int passed = 0;

        Class<?> testClass = Class.forName("com.effectivejava.study.chapter06.Item39.Sample");

        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(KDGTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException e) {
                    Throwable exc = e.getCause();
                    System.out.println(m + " 실패: " + exc);
                } catch (Exception e) {
                    System.out.println("잘못 사용한 @KDGTest: " + m);
                }
            }
        }
        System.out.printf("성공: %d, 실패: %d%n", passed, tests-passed);
    }
}

 

💡 예외를 던지는 애너테이션

  • 특정 예외를 던져야만 성공하는 테스트를 만들기 위해 @ExceptionTest 애너테이션을 만들었습니다.
  • 타입이 Class<? extends Throwable>인 매개변수를 가집니다.
@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 / i; // divide by zero. ArithmeticException 예외를 발생시킴 -> 성공
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m2() {
        int[] ints = new int[0];
        int i = ints[0]; // IndexOutOfBoundsException 발생 -> ArithmeticException가 아니므로 실패
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m3() {} // 아무 Exception도 발생하지 않음 -> 실패
}

public class RunTestsV2 {
    public static void main(String[] args) throws ClassNotFoundException{
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("com.effectivejava.study.chapter06.Item39.Sample2");
        for (Method m : testClass.getDeclaredMethods()) {
            tests++;

            try {
                m.invoke(null);
                System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
            } catch (InvocationTargetException e) {
                Throwable exc = e.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 e) {
                System.out.println("잘못 사용한 @ExceptionTest: " + m);
            }
        }
        System.out.printf("성공: %d, 실패: %d%n", passed, tests-passed);
    }
}

 

💡 여러 예외를 던지는 애너테이션

  • 위에서는 매개변수가 하나인 애너테이션 예제를 보았습니다. 그런데 매개변수가 하나가 아닌 둘 이상인 경우는 어떻게 해야할까요?
// 배열 사용
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestV2 {
    
    Class<? extends Throwable>[] value();
}

 

💡 사용 예시

public class Sample3 {

    @ExceptionTestV2({
            IndexOutOfBoundsException.class,
            NullPointerException.class
    })
    public static void m1() {
        List<String> list = new ArrayList<>();

        list.addAll(6, null);
    }
}

public class RunTestsV3 {
    public static void main(String[] args) throws ClassNotFoundException{
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("com.effectivejava.study.chapter06.Item39.Sample3");
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(ExceptionTestV2.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
                } catch (InvocationTargetException e) {
                    Throwable exc = e.getCause();
                    int oldPassed = passed;
                    Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTestV2.class).value();
                    for (Class<? extends Throwable> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed) {
                        System.out.printf("테스트 %s 실패: %s %n", m, exc);
                    }
                } catch (Exception e) {
                    System.out.println("잘못 사용한 @ExceptionTestV2: " + m);
                }
            }
        }
        System.out.printf("성공: %d, 실패: %d%n", passed, tests-passed);
    }
}

 

✔️ 정리

  • 애너테이션이 나온 이상 많은 상황에서 명명 패턴보다 애너테이션 활용이 좋습니다.
  • 명명 패턴의 단점들을 모두 해결해줍니다.

 

 

728x90
반응형