IT/Programming / / 2023. 4. 27. 09:21

<Effective Java> RULE 35 작명 패턴 대신 어노테이션을 사용하라

반응형

 

JUnit 테스트 프레임 워크 문제점

1. 철자를 틀리면 알아채기 힘들다.

· testSafetyOverride - tsetSafetyOverride

· 테스트에 제외되고 실행된다.

 

2. 특정한 프로그램 요소에만 적용되도록 만들 수 없다.

· testSafetyMechanisms Class

· JUnit은 무시한다.

 

3. 프로그램 요소에 인자를 전달할 마땅한 방법이 없다.

· 특정 예외가 발생해야 성공으로 판정하는 테스트를 지원하고 싶을때 예외 자료형이 인자로 전달해야 하나 보기 않좋고 깨지기 쉽다.

· 메서드에 이름이 포함된 문자열이 예외 이름인지 알 도리가 없다.

 

어노테이션 자료형을 이용

· 자동으로 실핼될 테스트 메서드를 지정

· 메서드 안에서 예외가 발생하면 테스트가 실패했다는것으로 보겠다는 사실을 알 수 있다.

/**
 * 
 * 어노테이션이 붙은 메서드가 테스트 메서드임을 표시
 * 무인자 정적 메서드에만 사용가능
 *
 * @class  Sample.java
 * @author ismyeong
 * @since  2018. 2. 5.
 *
 */
@Retention(RetentionPolicy.RUNTIME)
//테스트가 실행 시간에도 유지되어야 하는 어노테이션.
@Target(ElementType.METHOD)
//Test가 메서드 선언부에만 적용할 수 있는 어노테이션.
public @interface test{
}
 

테스트 실 사용 예

@Test 
public static void m1() {}

public static void m2() {}

@Test 
public static void m3() {
    throw new RuntimeException("Boom");
}

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 어노테이션은 해당 클래스가 동작하는데 직접 적인 영향을 미치진 않는다.

해당 어노테이션에 관심있는 프로그램에게 유용한 정보를 제공할 뿐이다.

 

 

위의 테스트를 실행하는 코드

public static void main(String[] args) throws Exception{
    int tests = 0;
    int passed = 0;
    Class<MarkerAnno> testClass = MarkerAnno.class;
    for(Method m : testClass.getDeclaredMethods()) {
        if(m.isAnnotationPresent(Test.class)) {
             //실행해야 하는 테스트 메서드를 찾는 용도
            tests++;
            try {
                m.invoke(null);
                passed++;
            } catch(InvocationTargetException wrappedExc) {
                //메서드가 예외를 발생시키면 Invocation으로 포장된다.
                //Invocation으로 예외가 발생되지 않았다면 잘못된 Test어노테이션이 있다는 것이다.
                //(객체 메서드, 인자 받는 메서드, 접근 불가능 메서드..)
                Throwable exc = wrappedExc.getCause();
                System.out.println(m + " failed : " + exc);
                
            } catch(Exception exc) {
                System.out.println("INVALID @TEST : " + m);
            }
        }
    }
    System.out.println("PASSED : "+ passed + ", FAILED : " + (tests - passed));
}
 

Test 어노테이션이 붙은 메서드를 전부 찾아내서 리플렉션 기능을 이용하여 실행한다. (Method.invoke 호출)

 

결과

public static void rule35.MarkerAnno.m3() failed : java.lang.RuntimeException: Boom
INVALID @TEST : public void rule35.MarkerAnno.m5()
public static void rule35.MarkerAnno.m7() failed : java.lang.RuntimeException: Crash
PASSED : 1, FAILED : 3
Picked up JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
 

지정된 예외를 발생 시켜야지만 성공하는 테스트

/**
 *
 * 이 어노테이션이 붙은 메서드는 테스트 메서드이며, 
 * 테스트에 성공하려면 지정된예외를 발생시켜야 한다.
 *
 * @class  Sample.java
 * @author ismyeong
 * @since  2018. 2. 7.
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
    Class<? extends Exception> value();
    //한정적 자료형 토큰
    //Exception을 계승한 클래스에 대한 Class 객체
}
 

인자를 받는 어노테이션 사용 예제

//인자를 받는 어노테이션의 사용 예제
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];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() {//실패 해야함 예외가 발생하지 않음
    }
}
 

테스트 실행기 예제

public static void main(String[] args) throws Exception{
    int tests = 0;
    int passed = 0;
    Class<Sample2> testClass = Sample2.class;
    for(Method m : testClass.getDeclaredMethods()) {
        if(m.isAnnotationPresent(ExceptionTest.class)) {
            tests++;
            try {
                m.invoke(null);
                System.out.println("Test " + m + " failed : no exception");
            }catch (InvocationTargetException wrappedEx) {
                Throwable exc = wrappedEx.getCause();
                Class<? extends Exception> excType = 
                        m.getAnnotation(ExceptionTest.class).value();
                if(excType.isInstance(exc)) {
                    passed++;
                }else {
                    System.out.println("Test "+ m +" failed : expected "+ excType.getName() + ", got "+exc);
                }
            }catch(Exception exc) {
                System.out.println("INVALID @Test : " + m);
            }
        }
    }
    System.out.println("PASSED : "+ passed + ", FAILED : " + (tests - passed));
}
 

어노테이션 인자로 전달된 값을 꺼내서 테스트 메서드 내부에서 발생한 예외 자료형을 검사하는데 사용

명시적 형변환 시행하는 코드가 없기 때문에 ClassCastException이 발생할 위험은 없다.

 

결과

Test public static void rule35.Sample2.m2() failed : expected java.lang.ArithmeticException, got java.lang.ArrayIndexOutOfBoundsException: 1
Test public static void rule35.Sample2.m3() failed : no exception
PASSED : 1, FAILED : 2
 

어노테이션에 배열을 인자로 전달하는 방법

/**
 * 
 * 배열을 인자로 받는 어노테이션 자료형
 *
 * @class  Sample.java
 * @author ismyeong
 * @since  2018. 2. 7.
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
    Class<? extends Exception>[] value();
}
 
//배열을 인자로 받는 어노테이션 사용 예
class Sample3{
    @ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
    public static void doublyBad() {
        List<String> list = new ArrayList<String>();
        
        //자바 명세는 아래와 같이 addAll을 호출하면
        //indexOutOfBoundException이나 NullPointer Exception이 발생한다고 명시되어 있다.
        list.addAll(5,null);
    }
}
 

테스트 실행기

public static void main(String[] args) throws Exception{
    int tests = 0;
    int passed = 0;
    Class<Sample3> testClass = Sample3.class;
    for(Method m : testClass.getDeclaredMethods()) {
        if(m.isAnnotationPresent(ExceptionTest.class)) {
            tests++;
            try {
                m.invoke(null);
                System.out.println("Test " + m + " failed : no exception");
            }catch (InvocationTargetException wrappedEx) {
                Throwable exc = wrappedEx.getCause();
                Class<? extends Exception>[] excTypes = 
                        m.getAnnotation(ExceptionTest.class).value();
                int oldPassed = passed;
                for(Class<? extends Exception> excType : excTypes) {
                    if(excType.isInstance(exc)) {
                        passed++;
                        break;
                    }
                }
                if(passed == oldPassed) {
                    System.out.println("Test "+ m +" failed : "+exc);
                }
            }catch(Exception exc) {
                System.out.println("INVALID @Test : " + m);
            }
        }
    }
    System.out.println("PASSED : "+ passed + ", FAILED : " + (tests - passed));
}
 

결과

PASSED : 1, FAILED : 0
 

결론

· 어노테이션이 있으므로 더이상은 작명 패턴에 기대면 안된다.

· 모든 프로그래머는 자바 플랫폼이 제공하는 어노테이션 자료형들을 사용하도록 해야한다.

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유