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
결론
· 어노테이션이 있으므로 더이상은 작명 패턴에 기대면 안된다.
· 모든 프로그래머는 자바 플랫폼이 제공하는 어노테이션 자료형들을 사용하도록 해야한다.
'IT > Programming' 카테고리의 다른 글
<Effective Java> RULE 33 ordinal을 배열첨자로 사용하는 대신 EnumMap을 이용하라 (0) | 2023.04.27 |
---|---|
<Effective Java> RULE 34 확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라 (0) | 2023.04.27 |
<Effective Java> RULE 36 Override 어노테이션은 일관되게 사용하라 (0) | 2023.04.27 |
<Effective Java> RULE 37 자료형을 정의할 때 표식 인터페이스를 사용하라 (0) | 2023.04.27 |
<Effective Java> RULE 38 인자의 유효성을 검사하라 (0) | 2023.04.27 |