2011년 6월 13일 월요일

Custom Annotation을 이용한 Simple Unit Test Tool 만들기

Custom Annotation 이용한 Simple Unit Test Tool 만들기
개요
JDK 5.0 추가된 기능 하나인 Annotation Java Metadata code 추가할 있게 해준다. 최근에 발표된 Java Open Source 라이브러리들을 보면 내부 Metadata Annotation 이용해 처리하고 있다. 문서에서는 Java Annotation 사용자가 정의한 Annotation 이용하여 간단 Framework 만들어 Annotation 어떤 방식으로 이용할 있는지를 보여줌에 목적이 있다.
Sample Framework으로는 Annotation 적용이 가장 쉬운 단위테스트 도구를 작성해 보기로 한다.

Simple Unit Test Tool 기능
Annotation기능내용
@StartUpStart Up Method단위 테스트 Class 내의 Test Methoad 실행전 최초 한번 실행되는 Method
JUnit “@BeforeClass” Annotation 동일한 기능을 한다.
@TearDownTear Down Method단위 테스트 Class 내의 모든 Test Method 실행완료 한번 실행되는 Method
JUnit “@AfterClass” 동일한 기능을 한다.
@TestTest Target단위 테스트 Method임을 지정한다. @Test Annotation 지정되지 않으면 단위 테스트에 포함된 Method라고 하여도 테스트가 실행되지 않는다.
@Test(TestGroup=”min”)Test Group단위 테스트 Method 그룹을 지정하여 특정 그룹만 테스트를 진행할 있다.
@Test(IsActive=AaciveType.INACTIVE)Test Active단위 테스트 Method 활성화, 비활성화 있다. 비활성화 Method 테스트 되지 않는다.
@Test(DataProvider=”testminus”)Test Provider단위 테스트 Method Input Parameter 사용할 DataProvider 이름을 지정
@DataProvider(name=”testminus”)Data Provider단위 테스트 Method Input Parameter 사용될 Method임을 지정 name 지정 해야 하며, @Test 에서 DataProvider 속성으로 지정된 이름을 사용하면 된다.


Simple Unit Test Tool 사용
Test Class : Calculator.class
package com.naver.blog.inter999.math;
public class Calculator {
public double Plus(double x, double y) {
return x+y;
}
public double Minus(double x, double y) {
return x-y;
}
public double Multiplied(double x, double y) {
return x*y;
}
public double Divided(double x, double y) {
return x/y;
}
}


Unit Test Case : TestCalculator
package com.naver.blog.inter999.math.test;
import java.util.ArrayList;
import java.util.List;
import com.naver.blog.inter999.math.Calculator;
import com.naver.blog.inter999.simpletesttool.annotation.DataProvider;
import com.naver.blog.inter999.simpletesttool.annotation.StartUp;
import com.naver.blog.inter999.simpletesttool.annotation.TearDown;
import com.naver.blog.inter999.simpletesttool.annotation.Test;
import com.naver.blog.inter999.simpletesttool.annotation.Test.ActiveType;
public class TestCalculator {
Calculator calculator = null;
@StartUp // 1
public void StartUp() {
calculator = new Calculator();
System.out.println("Start Up");
}
@TearDown // 2
public void TearDown() {
calculator = null;
System.out.println("Tear Down");
}
@DataProvider(name="testplus") //3
public List<Object[]> PlusDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(10);
Double y = new Double(20);
Object[] obj = {x,y};
Double x2 = new Double(15);
Double y2 = new Double(25);
Object[] obj2 = {x2,y2};
provider.add(obj);
provider.add(obj2);
return provider;
}
@DataProvider(name="testminus") //4
public List<Object[]> MinusDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(100);
Double y = new Double(20);
Object[] obj = {x,y};
provider.add(obj);
return provider;
}
@DataProvider(name="testmultiplied")
public List<Object[]> MultipliedDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(100);
Double y = new Double(20);
Object[] obj = {x,y};
provider.add(obj);
return provider;
}
@DataProvider(name="testdivided")
public List<Object[]> DividedDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(15);
Double y = new Double(3);
Object[] obj = {x,y};
provider.add(obj);
return provider;
}
@Test(DataProvider="testplus", TestGroup="min") // 5
public void TestPlus(Double x, Double y) {
Double result = calculator.Plus(x, y);
System.out.println(result);
}
@Test(DataProvider="testminus", TestGroup="min")
public void TestMinus(Double x, Double y) {
Double result = calculator.Minus(x, y);
System.out.println(result);
}
@Test(DataProvider="testmultiplied", IsActive = ActiveType.INACTIVE) //6
public void TestMultiplied(Double x, Double y) {
Double result = calculator.Multiplied(x, y);
System.out.println(result);
}
@Test(DataProvider="testdivided")
public void TestDivided(Double x, Double y) {
Double result = calculator.Divided(x, y);
System.out.println(result);
}
}


1. @StartUp
@StartUp // 1
public void StartUp() {
calculator = new Calculator();
System.out.println("Start Up");
}

Test Method 실행 최초 한번 실행되는 Method 지정, 여기서는 Calculator Instance 생성해 준다. @StartUp Annotation Test Case 하나만 지정이 가능하다.

2. @TearDown
@TearDown // 2
public void TearDown() {
calculator = null;
System.out.println("Tear Down");
}

Test Method 종료되고 Test Case 종료 되기 직전에 호출되는 Method, 여기서는 Calculator Instance null 치환해 주고 있다. @TearDown Annotation Test Case 하나만 지정이 가능하다.

3. @DataProvider
@DataProvider(name="testplus") //3
public List<Object[]> PlusDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(10);
Double y = new Double(20);
Object[] obj = {x,y};
Double x2 = new Double(15);
Double y2 = new Double(25);
Object[] obj2 = {x2,y2};
provider.add(obj);
provider.add(obj2);
return provider;
}

Test Method Parameter 사용할 Data 생성하는 Annotation으로 name 속성 값을 필수로 지정하여야 한다. 위의 DataProvider “TestPlus” Method에서 사용할 Data 생성한다. List<Object[]> List Size 만큼 해당 Test Method 실행된다. 위에서는 List 2개의 Object[] 포함되어 있어 TestPlus Method 2 실행된다.

4. @DataProvider
@DataProvider(name="testminus") //4
public List<Object[]> MinusDataProvider() {
List<Object[]> provider = new ArrayList<Object[]>();
Double x = new Double(100);
Double y = new Double(20);
Object[] obj = {x,y};
provider.add(obj);
return provider;
}

“TestMinus” Method에서 사용할 DataProvider

5. @Test
@Test(DataProvider="testplus", TestGroup="min") // 5
public void TestPlus(Double x, Double y) {
Double result = calculator.Plus(x, y);
System.out.println(result);
}

@Test Annotation으로 “TestPlus” Method 테스트 대상임을 지정하고, 사용할 DataProvider “testplus” 임을 지정, TestGroup이름을 “min”으로 지정한다.
Tes tCase 실행시킬 테스트 대상 그룹을 지정하여 특정 Test Method 실행할 있다.

6. @Test
@Test(DataProvider="testmultiplied", IsActive = ActiveType.INACTIVE) //6
public void TestMultiplied(Double x, Double y) {
Double result = calculator.Multiplied(x, y);
System.out.println(result);
}

@Test Annotation 속성에 IsActive=ActiveType.INACTIVE 지정하면 Test Method 비활성화 되어 테스트 제외된다.

Unit Test Case 실행
사용 방법 : RunTest [targetClass] [testGroup or null]
사용 : RunTest com.naver.blog.inter999.math.test.TestCalculator min
- TestCase : “com.naver.blog.inter999.math.test.TestCalculator”
- TestGroup : min

Simple Unit Test Tool 만들기
Custom Annotation 정의
1. @Startup
package com.naver.blog.inter999.simpletesttool.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface StartUp {
}

- “Retention(RetentionPolicy.RUNTIME)” : StartUp Annotation VM 의해서 유지되므로 Run-Time시에 읽어질 있다는 것을 의미
- “ElementType.METHOD” : Method 선언에만 사용할 있다는 것을 의미

2. @TearDown
package com.naver.blog.inter999.simpletesttool.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TearDown {
}


3. @DataProvider
package com.naver.blog.inter999.simpletesttool.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataProvider {
String name();
}

- String name() : DataProvider Annotation “name”속성을 가지며 필수 정의 항목임.

4. @Test
package com.naver.blog.inter999.simpletesttool.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
public enum ActiveType {ACTIVE, INACTIVE}
String TestGroup() default "";
ActiveType IsActive() default ActiveType.ACTIVE;
String DataProvider() default "";
}

- ActiveType {ACTIVE, INACTIVE} : AcitiveType Value에는 ACTIVE, INACTIVE 있음.
- String TestGroup() default “” : Test Annotation TestGroup 속성을 가지고 있고 해당 속성은 필수 정의 값이 아님
- ActiveType IsActive() default ActiveType.ACTIVE : IsActive 속성을 가지고 있으며 정의 하지 않을 경우 기본값으로 ActiveTeyp.ACTIVE 값을 가진다.
- String DataProvider() default “” : DataProvider 속성을 가지며 해당 속성은 필수 입력 값이 아님.

RunTest Application
package com.naver.blog.inter999.simpletesttool;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import com.naver.blog.inter999.simpletesttool.annotation.DataProvider;
import com.naver.blog.inter999.simpletesttool.annotation.StartUp;
import com.naver.blog.inter999.simpletesttool.annotation.TearDown;
import com.naver.blog.inter999.simpletesttool.annotation.Test;
import com.naver.blog.inter999.simpletesttool.annotation.Test.ActiveType;
public class RunTest {
public static void getStartUp(Object targetObject)
throws IllegalArgumentException,
IllegalAccessException,
InvocationTargetException {
for (Method m : targetObject.getClass().getMethods()) {
if (m.isAnnotationPresent(StartUp.class)) {
m.invoke(targetObject);
}
}
}
public static void getTearDown(Object targetObject)
throws IllegalArgumentException,
IllegalAccessException,
InvocationTargetException {
for (Method m : targetObject.getClass().getMethods()) {
if (m.isAnnotationPresent(TearDown.class)) {
m.invoke(targetObject);
}
}
}
public static List<Object[]> getProvider(Object targetObject, String providerName)
throws IllegalArgumentException,
IllegalAccessException,
InvocationTargetException {
List<Object[]> providerData = null;
for (Method m : targetObject.getClass().getMethods()) {
if (m.isAnnotationPresent(DataProvider.class)) {
if (((DataProvider) m.getAnnotation(DataProvider.class)).name()
.equals(providerName)) {
providerData = (List<Object[]>) m.invoke(targetObject);
}
}
}
return providerData;
}
public static void main(String[] args) throws Exception {
if(args.length < 1) {
System.err.println("USE : RunTest [targetClass] [TestGroup] or null");
System.err.println("EX : RunTest com.naver.blog.inter999.math.test.TestCalculator min");
}
String testGroup = "";
if(args.length == 2) {
testGroup = args[1];
}
Class<?> targetClass = Class.forName(args[0]);
Object targetObject = targetClass.newInstance();
getStartUp(targetObject);
for (Method m : targetClass.getMethods()) {
if (m.isAnnotationPresent(Test.class)) {
Test t = (Test) m.getAnnotation(Test.class);
if(t.IsActive().equals(ActiveType.ACTIVE)) {
if(t.TestGroup().equals(testGroup) || testGroup.equals("")) {
System.out.print("Method : ");
System.out.println(m.getName());
String providerName = t.DataProvider();
List<Object[]> dataProvider = getProvider(targetObject,
providerName);
for (Object[] obj : dataProvider) {
m.invoke(targetObject, obj);
}
}
}
}
}
getTearDown(targetObject);
}
}

Class<?> targetClass = Class.forName(args[0]);
Object targetObject = targetClass.newInstance(); // 1
getStartUp(targetObject); // 2
for (Method m : targetObject.getClass().getMethods()) {
if (m.isAnnotationPresent(StartUp.class)) {
m.invoke(targetObject); // 3
}
}


1. args[0] 전달된 TestCase Name으로 TestCase Class Instance 생성한다.
2. @StartUp Annotation Check 한다.
3. TestCase @StartUp 지정된 Method 찾아서 실행 시킨다.
for (Method m : targetClass.getMethods()) {
if (m.isAnnotationPresent(Test.class)) { // 1
Test t = (Test) m.getAnnotation(Test.class);
if(t.IsActive().equals(ActiveType.ACTIVE)) { // 2
if(t.TestGroup().equals(testGroup) ||
testGroup.equals("")) { // 3
System.out.print("Method : ");
System.out.println(m.getName());
String providerName = t.DataProvider();
if(providerName != null) {
List<Object[]> dataProvider = // 4
getProvider(targetObject, providerName);
for (Object[] obj : dataProvider) {
m.invoke(targetObject, obj);
}
} else {
m.invoke(targetObject);
}
}
}
}
}


1. @Test Annotation 정의된 모든 Test Method 찾는다.
2. Test Method 활성화(ACTIVE) 상태를 확인한다.
3. 실행 TestGroup 파라미터로 전달 받았다면 해당 TestGroup 아니면 모든 Test Method 실행한다.
4. DataProvider 정의 되어있을 경우 해당 DataProvider에게서 Test Method InputParameter 가져온다.

결론
Simple Unit Test Tool Sample이기 때문에 많은 기능을 포함하고 있지 않지만 Custom Annotation 만으로도 우리가 흔히 사용하는 JUnit 유사한 단위 테스트 도구를 만들 있었다. Annotation Meta Data 정의 언어 이기 때문에 독립적으로 사용에 있어서는 제약이 많을 있다. 하지만 AOP 프레임워크와 병행 사용을 고려한다면 발전된 관점지향 개발이 가능할 꺼라 생각한다.

댓글 없음:

댓글 쓰기

블록체인 개요 및 오픈소스 동향

블록체인(block chain) 블록체인은 공공 거래장부이며 가상 화폐로 거래할때 발생할때 발생할 수 있는 해킹을 막는 기술. 분산 데이터베이스의 한 형태로, 지속적으로 성장하는 데이터 기록 리스트로서 분산 노드의 운영자에 의한 임의 조작이 불가...