IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

AspectJ와 mock 객체를 이용한 유연한 테스팅
테스트 전용 작동으로 단위 테스트 향상시키기

Level: Intermediate

Nicholas Lesiecki
소프트웨어 엔지니어, eBlox, Inc.
2002년 5월

존경받는 자바 프로그래머이자 XP 커뮤니티 리더인 Nicholas Lesiecki는 테스트 케이스 고립화와 관련한 문제를 소개하고 mock 객체와 AspectJ를 이용하여 정밀하고 강력한 단위 테스트를 개발하는 방법을 설명한다.

Extreme Programming (XP)에 대한 최근의 관심은 포팅가능성이 농후한 문제인 단위 테스팅과 테스트 우선 설계로 옮겨갔다. 소프트웨어 샾들?XP의 관례를 채택함에 따라 많은 개발자들은 품질과 속도 향상을 경험하게 되었다. 하지만 좋은 단위 테스트를 작성하는 것은 시간과 노력을 요구한다. 각각의 단위(unit)는 다른 단위들과 협동하기 때문에 단위 테스트를 작성하는 것은 상당량의 설정 코드를 포함 할 수 있기 때문이다. 이것은 테스트를 값비싼 것으로 만들고 어떤 경우에는 그와 같은 테스트는 구현이 거의 불가능하다.

XP 에서, 단위 테스트(unit test)는 통합 테스트 (integration)와 수락 테스트(acceptance test)를 보충하는 것이다. 이들 두개의 테스트 유형은 개별 팀들이 담당하거나 개별 작동으로서 받아들여진다. 하지만 단위 테스트는 테스트 되어야 할 코드와 동시에 작성된다. 임박한 데드라인과 두통을 수반하는 단위 테스트에 직면하면 엉터리 테스트를 작성하게 되거나 테스트 때문에 괴롭힘을 당하게된다.

이 글을 읽기 전에..

이 글은 AspectJ를 이용한 단위 테스트에 초점을 맞춘다. 이 글을 읽는 독자는 기본적인 단위 테스트 기술을 숙지하고 있어야 한다. AspectJ에 익숙하지 않다면, AspectJ 소개 관련 글을 읽기 바란다 (참고자료). AspectJ 기술은 복잡하지는 않지만 aspect 지향 프로그래밍은 복잡할 수 있다. 예제를 실행하려면 시스템에 Ant를 설치해야 한다.

Mock 객체는 이러한 딜레마를 푸는데 도움이 된다. Mock 객체 테스트는 도메인 의존성을 테스팅에만 사용되는 mock 구현으로 대체한다. 하지만 이러한 전략은 원격 시스템에서의 단위 테스트 같은 특별한 상황에 기술적인 도전이 된다. AspectJ는 전통적인 객체 지향 기술이 실패하는 분야에서 테스트 전용 작동을 대체할 수 있도록 함으로서 단위 테스팅의 또 다른 방향을 제시하고 있다.

이 글에서는 단위 테스트를 작성하는 것이 어렵고도 필요한 상황을 시험하게 된다. EJB 기반 애플리케이션의 클라이언트 컴포넌트에 대한 단위 테스트를 실행하는 것으로 시작하겠다. 원격 클라이언트 객체에 대한 단위 테스트 시 발생할 수 있는 문제를 예제를 들어 설명할 것이다. 이러한 문제들을 해결하기 위해서 AspectJ와 mock 객체에 의존하는 두개의 새로운 테스트 설정을 개발할 것이다.

In코드 샘플을 이용하려면 예제 애플리케이션을 설치해야 한다.

단위 테스트 예제
이 예제는 EJB 클라이언트용 테스트로 구성되어 있다. 이 케이스 스터디에서 발생하는 많은 문제들은 웹 서비스, JDBC, facade를 통한 로컬 애플리케이션의 "원격" 부분을 호출하는 코드에 적용될 수 있다.

서버측 CustomerManager EJB는 두 기능을 수행한다: 고객의 이름을 검사하고 새로운 고객 이름을 원격 시스템에 등록한다. Listing 1은 CustomerManager가 클라이언트에 노출하는 인터페이봉甄?

Listing 1. CustomerManager의 원격 인터페이스

public interface CustomerManager extends EJBObject {

     /**
      * Returns a String[] representing the names of customers in the system
      * over a certain age.
      */
     public String[] getCustomersOver(int ageInYears) throws RemoteException;

     /**
      * Registers a new customer with the system. If the customer already 
      * exists within the system, this method throws a NameExistsException.
      */
     public void register(String name) 
       throws RemoteException, NameExistsException;
}

클라이언트 코드인 ClientBean은 반드시 같은 메소드를 노출하면서 그들의 구현을 CustomerManager에 보낸다. (Listing 2)

Listing 2. EJB 클라이언트 코드

public class ClientBean {
       private Context initialContext;
       private CustomerManager manager;

       /**
        * Includes standard code for referencing an EJB.
        */
       public ClientBean() throws Exception{
           initialContext = new InitialContext();
           Object obj =
                  initialContext.lookup("java:comp/env/ejb/CustomerManager");
           CustomerManagerHome managerHome = (CustomerManagerHome)obj;

           /*Resin uses Burlap instead of RMI-IIOP as its default
            *  network protocol so the usual RMI cast is omitted.
            *  Mock Objects survive the cast just fine.
            */
           manager = managerHome.create();
       }

       public String[] getCustomers(int ageInYears) throws Exception{
           return manager.getCustomersOver(ageInYears);
       }

       public boolean register(String name) {
           try{
               manager.register(name);
               return true;
           }
           catch(Exception e){
               return false;
           }
       }
}

나는 이 단위를 최대한 간단하게 하여 테스트에 집중할 수 있도록 했다. ClientBean의 인터페이스는 CustomerManager의 인터페이스와 약간 다르다. ClientManager와는 다르게 ClientBean's register() 메소드는 부울(boolean)을 리턴하고 고객이 이미 존재하고 있으면 예외를 던지지 않는다. 이것은 좋은 단위 테스트라는 것을 입증하는 것이다.

Listing 3의 코드는 JUnit으로 ClientBean 용 테스트를 구현한 것이다. 세 개의 테스트 메소드가 있는데, 하나는 getCustomers()용 이고 두 개는 register() 용 이다. (성공과 실패의 두 경우). getCustomers()를 맡고 있는 테스트는 55 아이템 리스트를 리턴하고 register()EXISTING_CUSTOMER 에 대해 false를, NEW _CUSTOMER에 대해 true 를 리턴한다.

Listing 3. ClientBean용 단위 테스트

//[...standard JUnit methods omitted...]

public static final String NEW_CUSTOMER = "Bob Smith";
public static final String EXISTING_CUSTOMER = "Philomela Deville";
public static final int MAGIC_AGE = 35;

public void testGetCustomers() throws Exception {
       ClientBean client = new ClientBean();
       String[] results = client.getCustomers(MAGIC_AGE);
       assertEquals("Wrong number of client names returned.",
                     55, results.length);
}

public void testRegisterNewCustomer() throws Exception{
       ClientBean client = new ClientBean();
       //register a customer that does not already exist
       boolean couldRegister = client.register(NEW_CUSTOMER);
       assertTrue("Was not able to register " + NEW_CUSTOMER, couldRegister);
}

public void testRegisterExistingCustomer() throws Exception{
       ClientBean client = new ClientBean();

       //register a customer that DOES exist
       boolean couldNotRegister = ! client.register(EXISTING_CUSTOMER);
       String failureMessage = "Was able to register an existing customer ("
                           + EXISTING_CUSTOMER + "). This should not be " +
                           "possible."
       assertTrue(failureMessage, couldNotRegister);
}

클라이언트가 예견된 결과를 리턴하면 테스트는 통과한다. 이 테스트가 매우 단순한 만큼 같은 절차를 EJB 컴포넌트로의 호출에 기반하여 아웃풋을 만드는 서블릿 같은 좀더 복잡한 클라이언트에 적용하는 방법을 쉽게 상상할 수 있다.

샘플 애플리케이션을 이미 설치했다면 예제 디렉토리의 ant basic 명령어로 잠깐 테스트를 해봐도 좋다.

데이터 의존 테스트의 문제점
위 테스트를 몇 차례 실행한 후에 일관성 없는 결과를 알게된다: 가끔은 테스트가 통과하고 가끔은 그렇지 않다. 비영속성의 원인은 클라이언트의 구현이 아닌 EJB 컴포넌트의 구현에 있다. 예제에서 EJB 컴포넌트는 불확실한 시스템 상황을 시뮬레이팅한다. 테스트 데이터의 비영속성은 단순하고 데이터 중심의 테스트를 구현할 때 실제 문제에 나타난다. 또 다른 큰 문제는 테스트를 중복해야 한다는 데 있다.

데이터 관리
데이터에서 불확실성을 극복하는 쉬운 방법은 데이터의 상태를 관리하는 것이다. 단위 테스트를 실행하기 전에 시스템에 55개의 고객 기록이 있다는 것을 확신한다면 getCustomers() 테스트의 실패는 데이터 자체의 문제라기 보다는 코드의 결함이라는 것을 나타낸다. 하지만 데이터의 상태를 관리하는 것은 그 자체로 문제를 야기할 수 있다. 테스트를 실행하기 전에 시스템이 특정 테스트에 대해 정확한 상태에 있는지를 확인해야 한다. 꾸준히 지켜보지 않으면 한 테스트의 결과는 다음 테스트가 실패하는 방향으로 시스템 상태를 변경할 수 있다.

이 문제에 대해서 공유된 설정 클래스나 배치 인풋 프로세스(batch-input process)를 사용할 수 있다. 하지만 이 두 가지 접근방식은 기반구조에 대한 막대한 투자가 있어야 한다. 여러분의 애플리케이션이 일정한 유형의 저장 상태로 지속된다면 좀더 심각한 문제에 직면한 것이다. 데이터를 저장 시스템에 추가하는 것은 복잡해 질 수 있고 빈번한 삽입과 삭제는 테스트 실행을 늦출 수 있다.

고급 테스트

이 글은 단위 테스트에 초점을 맞추었지만 통합 및 함수 테스트 또한 빠른개발과 고품질에 있어서 중요하다. 고급 테스트는 시스템의 end-to-end 통합을 가능하게 하는 반면 저수준 단위 테스트는 개별 컴포넌트에만 가능하다. 두 개 모두 다른 상황에서 유용하다.

상태 관리에 관한 문제에 맞딱뜨리는 것보다 더 나쁜 것은 그와 같은 관리가 전혀 불가능한 상황에 직면했을 때이다. 여러분은 아마도 제 삼자 서비스를 위해 클라이언트 코드를 테스트 할 때 이와같은 상황에 놓이게 된다. 읽기 전용 서비스는 시스템 상태를 변경하는 기능을 노출하지 않는다. 예를 들어 라이브 프로세싱 큐에 테스트 명령을 보내는 것은 좋은 생각이 아니다.

이중 노력
시스템 상태에 대한 완벽한 제어권이 있다하더라도 상태 기반의 테스팅은 원치 않는 중복의 테스트 노력을 요한다. 같은 테스트를 두번 쓰기를 누가 원하겠는가.

이제 예제 테스트 애플리케이션을 보자. CustomerManager EJB 컴포넌트를 제어한다고 하면, 이미 이것이 올바르게 작동한다는 것을 확인하는 테스트도 갖고 있을 것이다. 내 클라이언트 코드는 새로운 고객을 시스템에 추가할 때 로직이 개입된 어떤 것도 수행하지 않는다; 이것은 간단히 오퍼레이션을 CustomerManager에 보낸다.

같은 데이터에 대해 다른 응답을 주기 위해 CustomerManager의 구현을 변화시킨다면 변화를 트래킹하기 위해 두가 지 테스트를 바꾸어야 한다. 이것은 중복 테스트의 기미가 보인다. 다행히 중복이 불필요하다. ClientBeanCustomerManager와 정확하게 통신하고 있다는 것만 확인할 수 있다면, ClientBean 이 원하는 대로 작동하고 있다는 충분한 확신이 된다. Mock 객체 테스팅으로 이러한 종류의 확인 작업을 정확히 수행할 수 있다.

Mock 객체 테스팅
Mock 객체들은 지나치게 많은 단위 테스트를 방지한다. Mock 객체 테스트들은 mock 구현과의 실제적인 협력자이다. mock 구현은 테스트 된 클래스와 collaborator가 정확하게 교류하고 있다는 것을 쉽게 확인해준다. 간단한 예제를 통해 이것의 어떻게 작동하는지를 설명하겠다.

우리가 테스트하는 코드는 클라이언트-서버 데이터 관리 시스템에서 객체 리스트를 지운다. Listing 4는 테스트하고 있는 메소드이다:

Listing 4. 테스트 메소드

       public interface Deletable {
           void delete();
       }

       public class Deleter {

           public static void delete(Collection deletables){
               for(Iterator it = deletables.iterator(); it.hasNext();){
                   ((Deletable)it.next()).delete();
               }
           }
       }

쉬운 단위 테스트는 실제 Deletable 을 만들고 그런다음 Deleter.delete()을 호출한 후 이것이 사라진다는 것을 확인한다. mock 객체를 이용하여 Deleter를 테스트하기 위해 Deletable을 구현하는 mock 객체를 작성해야 한다. (Listing 5):

Listing 5. mock 객체 테스트

       public class MockDeletable implements Deletable{

           private boolean deleteCalled;

           public void delete(){
               deleteCalled = true;
           }

           public void verify(){
               if(!deleteCalled){
                   throw new Error("Delete was not called.");
               }
           }
       }

다음에는 Deleter의 단위 테스트에 mock 객체를 사용한다(Listing 6):

Listing 6. mock 객체를 사용하는 테스트 메소드

       public void testDelete() {
           MockDeletable mock1 = new MockDeletable();
           MockDeletable mock2 = new MockDeletable();

           ArrayList mocks = new ArrayList();
           mocks.add(mock1);
           mocks.add(mock2);

           Deleter.delete(mocks);

           mock1.verify();
           mock2.verify();
       }

실행할 때 이 테스트는 Deleter 가 성공적으로 컬렉션의 각 객체들에 대해 delete() 을 성공적으로 호출했다는 것을 확인한다. 이러한 방식으로, mock 객체 테스트는 테스트된 클래스의 주변을 정밀하게 제어하고 단위가 그들과 정확히 교류한다는 것을 확인한다.

mock 객체의 한계
객체 지향 프로그래밍은 테스트된 클래스가 실행될 때 mock 객체 테스트의 영향을 제한한다. 예를들어, 약간 다른 delete() 메소드를 테스트한다면 우리의 테스트는 그렇게 쉽게 mock 객체들을 제공할 수 없다. 다음 메소드는 mock 객체를 이용하여 테스트하기가 어렵다:

Listing 7. 어려운 메소드

public static void deleteAllObjectMatching(String criteria){
           Collection deletables = fetchThemFromSomewhere(criteria);
           for(Iterator it = deletables.iterator(); it.hasNext();){
               ((Deletable)it.next()).delete();
           }
       }

mock 객체 테스팅 메소드의 지지자는 위와 같은 메소드는 좀더 "mock 친화적인" 것이 될 수 있도록 위와 같은 메소드는 리팩토리 되어야 한다고 주장한다. 리팩토링은 좀더 유연한 디자인인 cleaner가 된다. 잘 설계된 시스템의 경우 각 단위는 정의가 잘된 인터페이스를 통해 이것의 콘텍스트와 교류한다.

하지만 잘 설계된 시스템일지라도 테스트가 콘텍스트에 쉽게 영향을 줄 수 없는 경우가 있다. 이러한 현상은 코드가 전역적으로 액세스 가능한 리소스에서 호출할 때 마다 발생한다. 예를들어 정적 메소드로의 호출은 확인 또는 대체가 어렵다.

Mock 객체는 테스팅이 도메인 클래스와 공용 인터페이스를 공유하는 테스트 클래스 간의 수동 대체에 기반하기 때문에 글로벌 리소스와 협력할 수 없다. 정적 메소드 호출은 오버라이드 될 수 없기 때문에 호출은 인스턴스 메소드가 할 수 있는 방식으로 리다이렉트 될 수 없다.

모든 Deletable 에서 메소드로 전달할 수 있다(Listing 4); 하지만 실제 이 장소에서 다른 클래스를 로딩할 때의 단점은 자바 언어를 사용할 경우 정적 메소드 호출을 mock 메소드 호출로 대체할 수 없다.

refactoring 예제
몇몇 refactoring으로 여러분의 애플리케이션 코드를 쉽게 테스트 할 수 있는 솔루션으로 조정할 수 있다. 하지만 항상 그런것만은 아니다. 테스팅을 가능하게 하는 Refactoring은 결과 코드의 유지와 이해가 더 어렵다면 이치에 맞지 않는다.

EJB 코드는 쉬운 mock 테스팅이 가능한 상태로 리팩토링 하는 것이 부분적으로 가능하다. 예를 들어, mock 친화적인 refactoring의 한 유형은 다음과 같은 코드 유형을 변화시킨다:


//in EJBNumber1
public void doSomething(){
    EJBNumber2 collaborator = lookupEJBNumber2();
    //do something with collaborator
}

다음 유형으로:



public void doSomething(EJBNumber2 collaborator){
    //do something with collaborator
}

표준 객체 지향 시스템에서 refactoring 예제는 호출자가 주어진 유닛에 collaborator를 제공할 수 있도록 함으로서 유연성을 향상시킨다. 그와 같은 refactoring은 EJB 기반 시스템에서 가능하다. 퍼포먼스적 측면에서 원격 EJB 클라이언트는 가능하면 원격 메소드 호출을 피해야 한다. 두 번째 접근방식은 클라이언트가 우선 살펴보고 그런다음 EJBNumber2의 인스턴스를 만들어야 한다.

게다가, 클라이언트 레이어가 EJBNumber2의 존재 같은 구현 상세를 반드시 알필요가 없는 경우 잘 설계된 EJB 시스템은 "계층적인(layered)" 접근방식을 취하는 경향이 있다. EJB 인스턴스를 얻는 유명한 방식은 JNDI 콘텍스트에서 팩토리를 보고난 후 팩토리상에 생성 메소드를 호출하는 것이다. 이러한 전략은 EJB 애플리케이션에 많은 유연성을 가져다준다. 애플리케이션 전개자들은 전개할 때, 완전히 다른 EJBNumber2 의 구현을 바꿀 수 있기 때문에 시스템 작동은 쉽게 맞춰진다. JNDI 바인딩은 런타임시 쉽게 변경되지 않는다. 따라서 mock 객체 테스터들은 EJBNumber2 용 mock에서 바꾸기 위해 재전개를 할 것인지 아니면 전체 테스팅 모델을 포기해야 할 지를 선택해야 한다.

다행히, AspectJ가 있다.

유연성을 추가한 AspectJ
AspectJ는 콘텍스트에 민감한 작동 변경을 테스트 케이스 기반으로 제공한다. mock 객체의 사용을 금해야하는 상황에도 그렇다. AspectJ의 join-point 모델은 aspect라고 하는 모듈이 프로그램의 실행 시점을 확인할 수 있도록 한다.

Aspect는 pointcuts을 통한 프로그램의 제어 흐름에서 포인트를 확인한다. pointcut은 프로그램 실행 시 일련의 포인트를 집어내고 aspect가 그러한 joinpoint과 관련된 것을 실행하는 코드라는 것을 정의할 수 있도록 한다. 간단한 pointcut을 사용하여, 모든 매개변수가 특정 신호와 매치되는 JNDI lookup을 선택할 수 있다. 하지만 우리가 무엇을 하든지 간에 aspect는 테스트 코드에서 나타나는 것만 lookup한다는 것을 확인해야 한다. 이를 위해, cflow() pointcut을 사용한다. cflow는 프로그램 실행에서 다른 joinpoint의 콘텍스트 내에서 발생하는 모든 포인트를 집어낸다.

다음 코드는 예제 애플리케이션이 cflow-based pointcut을 사용할 수 있도록 어떻게 변경될 수 있는지를 보여준다.


pointcut inTest() : execution(public void ClientBeanTest.test*());

/*then, later*/ cflow(inTest()) && //other conditions

이 라인들은 테스트 콘텍스트를 정의한 것이다. 첫 번째 라인은 아무것도 리턴하지 않고 공용 액세스를 가지고 있으며 테스트 라는 단어로 시작하는 ClientBeanTest 클래스의 모든 메소드 실행에 대해 inTest() 라는 이름을 부여한다. cflow(inTest()) 수식은 그와 같은 메소드 실행과 리턴사이의 모든 joinpoint를 집어낸다. 따라서 cflow(inTest()) ClientBeanTest 실행에 대한 메소드를 테스트하고 있는 동안" 이라는 것을 의미한다.

샘플 애플리케이션의 테스트 슈트는 두 개의 다른 설정으로 구현될 수 있다. 각각 다른 aspect를 사용한다. 첫 번째 설정은 실제 CustomerManager를 mock 객체로 대체한다. 두 번째 설정은 객체들을 대체하는 것이 아니라 클라이언트 빈에 의한 EJB 컴포넌트에 이루어진 호출을 선택적으로 대체한다. 두 경우 모두 aspect는 보여주기를 관리하면서 클라이언트가 CustomerManager로 부터 예견된 결과를 받을 수 있음을 확인한다. 이러한 결과들을 체크함으로서 ClientBeanTest 는 클라이언트가 EJB 컴포넌트를 정확히 사용하고 있다는 것을 확인할 수 있다.

EJB lookup을 대체하기 위해 aspect 사용하기
Listing 8의 첫 번째 설정은 ObjectReplacement라고 하는 aspect를 예제 애플리케이션에 적용한다. 이것은 Context.lookup(String) 메소드에 이루어진 모든 호출의 결과를 대체함으로서 작동한다.

Listing 8. ObjectReplacement aspect

import javax.naming.Context;

public aspect ObjectReplacement{

       /**
        * Defines a set of test methods.
        */
       pointcut inTest() : execution(public void ClientBeanTest.*());

       /**
        * Selects calls to Context.lookup occurring within test methods.
        */
       pointcut jndiLookup(String name) :
                cflow(inTest()) &&
                call(Object Context.lookup(String)) &&
                args(name);
                

       /**
        * This advice executes *instead of* Context.lookup
        */
       Object around(String name) : jndiLookup(name){

           if("java:comp/env/ejb/CustomerManager".equals(name)){
               return new MockCustomerManagerHome();
           }
           else{
               throw new Error("ClientBean should not lookup any EJBs " +
                               "except CustomerManager");
           }
       }
}

jndiLookup pointcut은 Context.lookup()과 관련된 호출을 확인하기 위해 이전에 논의된 pointcut을 사용한다. jndiLookup pointcut을 정의한 후에, lookup 대신 실행하는 코드를 정의할 수 있다.

"advice"에 대하여
AspectJ는 joinpoint에서 실행되는 코드를 설명하는 데 advice 라는 용어를 사용한다. ObjectReplacement aspect는 하나의 advice (파란색 부분)를 채택한다. advice는 "JNDI Lookup을 만나면, 메소드 호출을 진행하는 대신 mock 객체를 리턴하라."고 명령한다. 일단 mock 객체가 클라이언트로 리턴되면, aspect의 작동은 완성되고 mock 객체들이 인계받는다. MockCustomerManagerHomecreate() 메소드로의 모든 호출에서 custormer 매니저의 mock 버전을 리턴한다. mock은 정확한 시점에서 프로그램에 합법적으로 들어가기 위해 홈 인터페이스를 구현해야 하기 때문에 mock 또한 CustomerHome의 수퍼인터페이스인 EJBHome의 메소드를 구현한다. (Listing 9).

Listing 9. MockCustomerManagerHome

public class MockCustomerManagerHome implements CustomerManagerHome{

       public CustomerManager create() 
         throws RemoteException, CreateException {
           return new MockCustomerManager();
       }


       public javax.ejb.EJBMetaData getEJBMetaData() throws RemoteException {
           throw new Error("Mock. Not implemented.");
       }

//other super methods likewise
[...]

MockCustomerManager는 단순하다. 수퍼인터페이스 작동을 위해 스텁 메소드를 정의하고 ClientBean이 사용하는 메소드를 간단하게 구현한다. (Listing 10).

Listing 10. MockCustomerManager에 대한 Mocked 메소드

public void register(String name) NameExistsException {
     if( ! name.equals(ClientBeanTest.NEW_CUSTOMER)){
         throw new NameExistsException(name + " already exists!");
     }
}

public String[] getCustomersOver(int years) {
     String[] customers = new String[55];
     for(int i = 0; i < customers.length; i++){
         customers[i] = "Customer Number " + i;
     }
     return customers;
}

성숙한 mock 객체들은 테스트 작동을 쉽게 커스터마이징 할 수 있도록 하는 hook를 제공한다.

aspect를 사용하여 EJB 컴포넌트에 호출 대체시키기
EJB 전개 단계를 건너뛰는 것으로 개발을 쉽게 할 수 있지만 최종 목표에 근접하게 복제된 환경에서 코드를 테스트 한다는 이점이 있다. 애플리케이션을 완전히 통합하고 전개된 애플리케이션에 대해 테스트를 실행하면 설정 문제를 조기에 해결할 수 있다. 이것은 Cactus의 철학으로서, Cactus는 오픈 소스의 서버측 테스팅 프레임웍이다.

아래의 예제 애플리케이션의 설정은 애플리케이션 서버에서 테스트 실행에 Cactus를 사용한다. 이를 통해 ClientManager EJB가 정확히 설정되었고 컨테이너 내부의 다른 컴포넌트에 의해 액세스 될 수 있다는 것을 테스트를 통해 확인할 수 있다. AspectJ는 이러한 스타일의 반통합된 테스팅을 보완한다.

CallReplacement aspect는 테스팅 컨텍스트의 정의로 시작한다. getCustomersOver()register() 메소드에 상응하는 pointcut을 지정한다. (Listing 11):

Listing 11. CustomerManager에 대한 테스트 호출 선택하기

public aspect CallReplacement{

       pointcut inTest() : execution(public void ClientBeanTest.test*());

       pointcut callToRegister(String name) :
                       cflow(inTest()) &&
                       call(void CustomerManager.register(String)) &&
                       args(name);

       pointcut callToGetCustomersOver() :
                       cflow(inTest()) &&
                       call(String[] CustomerManager.getCustomersOver(int));
       //[...]

aspect는 around advice를 각각의 관련 메소드 호출에 대해 정의한다. getCustomersOver() 또는 register()로의 호출이 ClientBeanTest 내에서 발생하면, 관련 advice가 실행된다. (Listing 12):

Listing 12. Advice가 테스트 내에서 메소드 호출을 대체한다.

       void around(String name) throws NameExistsException:
callToRegister(name) {
           if(!name.equals(ClientBeanTest.NEW_CUSTOMER)){
               throw new NameExistsException(name + " already exists!");
           }

       }

       Object around() : callToGetCustomersOver() {
           String[] customers = new String[55];
           for(int i = 0; i < customers.length; i++){
               customers[i] = "Customer Number " + i;
           }
           return customers;
       }
     

두 번째 설정은 테스트 코드를 다소 단순하게 한다.

Pluggable 테스트 설정
AspectJ는 이러한 두 개의 설정들을 즉시 바꿀 수 있도록 한다. aspect는 그들에 대해 전혀 지식이 없는 클래스에게 영향을 줄 수 있기 때문에, 컴파일 시 다른 aspect를 지정하면 런타임 시 시스템에 완전히 다른 작동 결과를 가져온다. 샘플 애플리케이션은 이를 이용한 것이다:

Listing 13. Ant는 다른 설정을 목표로하고 있다.

       <target name="objectReplacement" description="...">
         <antcall target="compileAndRunTests">
           <param name="argfile"
                        value="${src}/ajtest/objectReplacement.lst"/>
         </antcall>
       </target>

       [contents of objectReplacement.lst]
       @base.lst;[A reference to files included in both configurations]
       MockCustomerManagerHome.java
       MockCustomerManager.java
       ObjectReplacement.java.

       <target name="callReplacement" description="...">
         <antcall target="deployAndRunTests">
           <param name="argfile"
                        value="${src}/ajtest/callReplacement.lst"/>
         </antcall>
       </target>

       [contents of callReplacement.lst]
       @base.lst
       CallReplacement.java
       RunOnServer.java

Ant 스크립트는 argfile 속성을 AspectJ 컴파일러로 보낸다. AspectJ 컴파일러는 어떤 소스를 구현에 포함할 지를 결정하기 위해 이 파일을 사용한다. argfileobjectReplacement 에서 callReplacement로 변경함으로서 구현은 간단한 재컴파일로 테스팅 전략을 변경할 수 있다.

Cactus에 플러그인하기

예제 애플리케이션은 Cactus와 번들된다. 이것은 애플리케이션 서버 내에서 테스트를 실행하는 데 사용된다. Cactus를 사용하기 위해서, 테스트 클래스는 org.apache.cactus.ServletTestCase (일반적인 junit.framework.TestCase대신)을 확장해야한다. 이러한 베이스 클래스는 자동적으로 애플리케이션 서버에 전개된 테스트들과 통신한다. "callReplacement" 버전의 테스트는 서버를 필요로하지만 "objectReplacement" 버전은 그렇지 않다. AspectJ의 다른 기능을 사용하여 테스트 클래스 서버가 인식되도록 한다. ClientBeanTest의 소스 버전은 TestCase를 확장한다. 테스트가 서버측에서 실행되길 원하면, 구현 설정에 다음의 aspect를 추가한다:

public aspect RunOnServer{

declare parents : ClientBeanTest extends ServletTestCase;
}

이 aspect를 추가함으로서, ClientBeanTestTestCase 대신 ServletTestCase를 확장했다.

 

결론
테스트 개발 비용을 절감하기 위해 단위 테스트는 고립된 상태에서 실행해야 한다. Mock 객체 테스팅은 각각의 단위를 고립화 한다. 하지만 객체 지향 기술은 collaborate 코드를 성공적으로 대체할 수 없다. AspectJ의 기능은 이러한 상황에서 코드를 깔끔하게 대체한다.

참고자료

AspectJ

Unit-testing 툴 & 기술

추가 참고자료

목 차:
단위 테스트 예제
데이터 의존 테스트의 문제점
Mock 객체 테스팅
mock 객체의 한계
유연성을 추가한 AspectJ
Pluggable 테스트 설정
결론
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Improve modularity with aspect-oriented programming
Automating the build and test process
Incremental development with Ant and JUnit
XP distilled
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Nicholas Lesiecki는 dot.com 붐 시대에 자바 프로그래밍의 세계에 입문하였고 그 이후 XP와 같은 민첩한 프로세스에서 오픈 소스 구축 및 테스팅 툴을 응용하기 위한 방법을 담은 매뉴얼인 Java Tools for Extreme Programming의 출판과 함께 XP와 자바 커뮤니티에서 탁월한 명성을 쌓아 왔다. 그는 현재 eBlox, Inc.의 고급 온라인 카탈로그 시스템인 storeBlox의 개발을 이끌고 있다. 그는 Tucson JUG에서 빈번하게 연설할 뿐 아니라 Jakarta의 Cactus 프로젝트 (서버측 단위 테스트 프레임워크)에 적극적으로 참여하고 있다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의