원문 : http://www-128.ibm.com/developerworks/kr/library/j-aopwork11/

AOP@Work: 애스펙트의 단위 테스트#

크로스커팅 작동을 확인하는 8 가지 새로운 패턴

<div class="note"> 저자정보 : Nicholas Lesiecki, 소프트웨어 엔지니어/프로그래밍 강사, Google

작성일 : 2005년 11월 01일 </div>

AOP에서 테스트 작성이 전보다 쉬워졌다. 어떻게 이것이 가능할까? Nicholas Lesiecki가 aspect 지향 코드를 테스팅 할 때의 이점을 소개하고 AspectJ에서 크로스커팅 작동을 테스트하는 패턴도 소개한다.

지난 5년 동안 테스트가 광범위하게 채택된 이유는 눈에 띌만한 생산성과 결과 코드의 품질에 기인했다. aspect-oriented programming(AOP)가 나오기 전까지는 보안, 트랜잭션 관리, 영속성 같은 특정 유형의 테스트를 작성하는 것은 어려운 일이었다. 왜? 이 작동은 모듈화가 잘 되지 않았기 때문에 테스트할 단위가 없을 경우 단위 테스트를 작성하는 것은 어려운 일이었다. AOP의 대중화로 인해 목표 시스템에서 구현과 독립된 크로스커팅 문제를 검사하는 테스트를 작성하는 것이 가능해 졌다.

이 글에서는 aspect로 구현된 크로스커팅 작동을 테스트하는 기술을 소개할 것이다. aspect의 단위 테스트에 초점을 맞추겠지만 객체 지향 애플리케이션을 구현할 때 도움이 되는 다른 패턴들도 설명하겠다. aspect의 테스팅은 객체의 테스팅과 많은 부분들이 닮아있다.

AspectJ를 개발할 때 경험했던 것을 토대로 이 글을 썼다. 많은 개념들은 다른 AOP 구현들에게도 적용될 수 있다. 다운로드 섹션에서 소스 코드를 다운로드 하고, 참고자료 섹션에서 AspectJ와 AJDT를 다운로드 할 수 있다.

aspect 코드의 단위 테스팅#

그림 1의 다이어그램은 자동화된 테스트 수트의 모습이다. 개별 클래스들을 위한 테스트가 기반을 형성하고 있다. 이 부분에서는 많은 테스트가 다루어지고 오류의 고립이 빨리 실행된다. 상단에는 통합 및 엔드투엔트 시스템 테스트가 있다. 이것은 이 단위들이 동시에 작동하는지를 확인한다. 이러한 레이어들은(잘 구현되고 빠르게 실행된다면) 애플리케이션 작동에 있어서 신뢰도를 높일 수 있다.

이 피라미드의 베이스에 있는 단위 테스트는 여러 가지 이유로 중요하다. 우선, 통합 테스트에서 다시 만들어내기 어렵거나 귀찮은 케이스들을 시뮬레이트 하는데 도움이 된다. 둘째, 여기에는 소량의 코드만 포함되기 때문에 더 빨리 실행된다. (그리고 자주 실행할 수 있다.) 세 번째로 이들은 각 단위의 인터페이스와 요구 사항들을 고려하는데 도움이 된다. 좋은 단위 테스트는 단위들 간 약결합을 장려하고 있다. 약결합은 테스트 장치에서 테스트를 실행 시키는 요구사항 이기도 하다.

그림 1. 테스트 레이어 TestingPyramid.jpg

그렇다면 크로스커팅(crosscutting) 작동은? 사용자의 요구 사항을 가정해 보자. "ATM 클래스에 대한 연산을 실행하기 전에 콜러의 보안 증명서를 체크한다." 이 요구사항에 대해 통합 테스트를 작성해야 한다. 하지만 aspect 지향 개발 환경이 아니라면 단위 테스트를 작성하거나 "연산 전에 보안을 검사하는" 작동을 고립화하기 어렵다. 하지만 aspect로 개발하면 특정 포인트컷(pointcut)에 매치되는 어떤 연산에도 적용되는 어드바이스로 작동을 표현할 수 있다. 이제 이 작동은 하나의 단위로서 퍼스트-클래스 구현을 갖고 있고 이것을 고립된 상태에서 테스트하고, IDE를 사용하여 시각화 할 수 있다.


aspect 코드가 실패하는 곳#

객체의 단위 테스팅에 쓰이는 기술을 연구하기 전에 오류의 유형들을 간략하게 설명하겠다. 크로스커팅 작동은 두 개의 주요 컴포넌트로 나뉜다. 작동이 수행하는 것(크로스커팅 기능(crosscutting functionality))과 그 작동이 적용되는 곳(크로스커팅 스팩(crosscutting specification))이다. ATM 예제로 돌아가서 크로스커팅 기능은 콜러의 보안 증명서를 검사한다. 크로스커팅 스팩은 ATM 클래스에 대한 모든 퍼블릭 메소드에 검사를 적용한다.

실제 구현의 경우 기능과 스팩 모두를(다시 말해서, 어드바이스와 포인트 컷을) 검사해야 한다. 이 예제를 진행하면서 주어진 테스트 패턴이 크로스커팅 기능 또는 스팩, 아니면 두 가지 모두를 검사하는지 여부를 강조 할 것이다.

포인트컷, 어드바이스, 그리고 이들을 지원하는 코드를 테스트하는 것에 초점을 맞출 것이다. 인터타입(intertype) 선언(그리고 기타 aspect 기능)도 확실히 테스트가 가능하다. 이 글에서 제시한 몇몇 기술들을 약간 변경하여 여기에 적용할 수 있다. 공간을 절약하기 위해 이 글에서는 다루지 않기로 한다.


테스트 패턴의 카탈로그#

나는 객체 지향 코드를 테스팅 할 패턴의 카탈로그들로 이 글을 구성했다.

  • 통합된 단위들의 테스트: 이 섹션은 통합된 시스템의 조각을 테스트하는 패턴을 제공한다. (aspect와 비 aspect 클래스를 함께 테스트한다.) 이 기술은 aspect를 사용하지 않고 중요한 툴이 있을 경우 크로스커팅을 할 수 있는 유일한 방법이다.
  • 시각적 툴 사용하기: 여기에 설명된 두 개의 패턴들은 Eclipse용 AspectJ의 IDE 지원을 활용한다. 애플리케이션의 크로스커팅 구조를 검사하기 위해 시각적 툴을 사용하는 것은 엄밀히 말해서 테스팅 기술이 아니다. 하지만 애플리케이션의 크로스커팅 문제를 이해하고 이에 대해 자신감을 가질 수 있도록 도와준다.
  • 델리게이션 사용하기: 이 섹션은 앞서 언급된 두 개의 실패 유형들을 나누는데 도움이 되는 두 개의 패턴들을 설명한다. 어드바이스에서 몇 개의 로직을 헬퍼 클래스(또는 메소드)로 팩토링하여 크로스커팅 스팩과 독립적으로 애플리케이션의 크로스커팅 작동을 검사하는 테스트를 작성할 수 있다.
  • Mock 타겟 사용하기: 마지막 섹션에는 "목 타겟(mock target)"을 도입하는 세 개의 패턴들이 포함된다. 이것은 실제 어드바이스 타겟을 모방하고, aspect를 실제 타겟에 통합하지 않고 조인 포인트(join point) 매칭과 어드바이스 작동 모두를 테스트할 수 있는 클래스이다.

Highlighter aspect#

이 카탈로그의 패턴들을 설명하기 위해 검색-용어 하이라이팅(사용자의 검색 단어를 검색 결과에 강조(highlighting))를 구현하는 aspect를 사용한다. 나는 이전 작업과 매우 비슷하게 aspect를 구현했다. 우리 시스템은 결과 요약 페이지, 상세 페이지, 기타 많은 애플리케이션의 수 많은 부분들에 있는 단어를 하이라이트 했어야 했다. 이것이 너무 많은 부분들을 포함하기 때문에 aspect가 이상적인 후보가 되었다. 이 글에서 제공하는 것은 하나의 클래스를 크로스커팅 하는 것이지만 원리는 같다. Listing 1에는 Highlighter aspect의 구현이다.

Listing 1. Highlighter가 하이라이팅 작동을 정의한다.

public aspect Highlighter{

  /* ITDs to manage highlighted words */
  private Collection<String> Highlightable.highlightedWords;
  
  public Collection<String> Highlightable.getHighlightedWords() {
    return highlightedWords;
  }
  public void Highlightable.setHighlightedWords(Collection<String> 
    highlightedWords){
    this.highlightedWords = highlightedWords;
  }
  
  public pointcut highlightedTextProperties() :
    
      execution(public String getProduct())
    || execution(public String getTitle())
    || execution(public String getSummary())
    );

  
  String around(Highlightable highlightable
    highlightedTextProperties() && this(highlightable
  {
    String highlighted = proceed(highlightable);
    for (String word : highlightable.getHighlightedWords()) {
      Pattern pattern = patternForWord(word);
      Matcher matcher = pattern.matcher(highlighted);
      highlighted = matcher.replaceAll("<span class=
        \"bold\">$0</span>");
    }
    return highlighted;
  }
  
 private Pattern patternForWord(String word) {
  return Pattern.compile("\\b\\Q" + word + "\\E\\b",
   Pattern.CASE_INSENSITIVE);
 }  
}

Highlighter aspect는 조인 포인트(join point)의 리턴 값을 캡쳐하고 이를 하이라이팅 버전으로 대체한다. 이것은 Highlightable 인터페이스 전역의 인터타입 필드에 저장된 하이라이팅 단어 컬렉션에 기반하여 어떤 단어를 하이라이트 할 것인지를 선택한다. Highlightable 인터페이스를 하이라이팅 작동에 참여해야 하는 어떤 클래스에도 적용할 수 있다. 클래스 선언 또는 declare parents 문장을 사용한다.

나는 이 예제의 초기 버전에 매우 간단한 포인트컷을 선택했다. 나중에 몇 가지 테스팅 패턴을 설명하면서 이 포인트컷을 다시 작성할 것이다.


I. 통합된 단위들의 테스트#

Addresses: 크로스커팅 기능과 스팩

요약: 머리말에서 설명했듯이 aspect는 통합 테스트에 순응한다. 이 패턴은 매우 단순하다. 작동이 aspect로 구현되지 않았다면 시스템에 대한 테스트를 작성한다. 다시 말해서, 객체를 함께 모으고, 상태를 설정하고, 메소드를 호출하여 결과를 확인한다. 여기에서 핵심은 aspect가 잘못 작동하거나 의도한 조인 포인트에 적용되지 않을 경우 실패 할 테스트를 작성하는 것이다. aspect가 많은 조인 포인트에 영향을 주도록 하려면 몇 가지 대표적인 예들을 선택한다.

예: Highlighter용 통합 테스트#

Listing 2에서 주목할 것은 이 테스트는 aspect 없는 애플리케이션용 테스트처럼 작동한다. 객체를 함께 모으고, 상태를 설정하고, 메소드를 호출하여 결과를 검색한다.

Listing 2. Highlighter용 통합 테스트

public class HighlightSearchResultsIntegrationTest extends TestCase {
  Collection<String> words;

  private SearchResult result;

  public void setUp() throws Exception {
    super.setUp();
    words = new ArrayList<String>();
    words.add("big");
    words.add("grrr");

    result = new SearchResult();
    result.setTitle("I am a big bear!");
    result.setSummary("grrr growl!");
    result.setHighlightedWords(words);
  }

  public void testHighlighting() {
    String expected = "I am a <span class=\"bold\">big</span> bear!";
    assertEquals(expected, result.getTitle());
    expected = "<span class=\"bold\">grrr</span> growl!";
    assertEquals(expected, result.getSummary());
  }
}

장점과 단점#

통합 테스트는 AOP를 사용하든, 사용하지 않든 비용화 효과가 비슷하다. 각각의 경우 가장 큰 이점은 코드의 의도를 확인할 수 있다는 점이다.(제목과 요약이 알맞게 하이라이트 된다.) 리팩토링을 수행할 때 도움이 된다. 컴포넌트가 인터랙트 할 때에만 나타나는 버그도 없앤다.

통합 테스트에만 의존하면 많은 문제들이 생긴다. HighlightSearchResultsIntegrationTest가 실패한다면, aspect가 전혀 실행되지 않기 때문이거나, 혹은 어드바이스 로직이 버그를 갖고 있기 때문이다. 또는 다른 개입된 클래스(SearchResult) 때문이다. 사실 나도 이와 같은 상황을 통합 테스트 예제의 코드를 개발할 때 겪었다. 20분 동안 aspect가 왜 실행되지 않는지를 고민했다. 정규식과 관련된 모호한 문제가 있다는 것만 알아냈다.

통합 테스트에는 보다 복합적인 설정과 선언이 필요하다. 이것 때문에 하나의 aspect를 고립하는 테스트 보다 작성이 어려워진다. 또한 코드가 알맞게 핸들 해야 하는 모든 경우를 시뮬레이션 하는 통합 테스트를 사용하기도 힘들다.

많은 클래스들의 크로스커팅 작동은 통합 테스트에 특정 문제를 일으킨다. 애플리케이션의 모든 클래스에 대해 지속적인 예외 핸들링이 필요하다고 가정해 보자. 새로운 작동을 위해 모든 클래스들을 테스트할 필요는 없을 것이다. 대표적인 샘플을 선택하고 싶을 것이다. 하지만 특정 도메인 클래스(Customer 클래스)를 선택하여 이것에 대한 에러 핸들링 aspect를 테스트하면 테스트의 의도를 모호하게 만들 수도 있다. 테스트가 Customer의 작동을 확인하는 것인지, 아니면 애플리케이션의 에러 핸들링을 확인하는 것인지 모호하다.


II. 시각적 툴 사용하기#

광범위한 크로스커팅 문제를 테스팅 할 때 어려운 점은 너무나 많은 조인 포인트들을 권고할 수 있다는 점이다. 모든 매치들을 실행 및 체크한다는 것은 고통 그 자체이다. 따라서 다음 두 개의 패턴들은 AJDT 같은 툴에서 사용할 수 있는 크로스커팅 뷰의 수동 검사 기능으로 정상 테스트를 보완할 때의 이점을 보여주고 있다. (AspectJ와 AJDT를 결합하면 시각화 지원이 가능하다. JBoss AOP와 JBoss IDE의 조합 역시 좋은 시각화 툴을 제공한다.)

Pattern 1. 크로스커팅을 시각적으로 검사하기#

Addresses: 크로스커팅 스팩

요약: AJDT의 크로스-레퍼런스 뷰를 사용한다. 리스트가 완전하고 생략될 조인 포인트가 없다는 것을 직접 확인한다.

예: 원치 않는 매치 구분하기

검색 결과의 제목, 제품, 요약을 강조한다고 가정해 보자. Listing 1에서 했던 것처럼 각 메소드를 세는 대신, 강력한 포인트 컷이 될 것으로 기대하는 것을 작성한다. (참고자료의 Adrian Colyer 블로그 참조)

public pointcut highlightedTextProperties() :

   execution(public String get*())
   && ! execution(public * Highlightable.*(..))
);

AJDT의 크로스 레퍼런스 뷰를 사용하여 포인트컷을 검사하면 그림 2가 나타난다.

그림 2. AJDT 크로스 레퍼런스 뷰의 네 가지 조인 포인트 IncludesWebsite.jpg

추가 매치가 있다. (SearchResult.getWebsite()) 알다시피, Website는 하이라이트 될 것이 아니었기 때문에 포인트컷을 다시 작성하여 의도하지 않은 매치를 배제시킨다.

장점과 단점

AJDT의 크로스 레퍼런스 뷰를 사용하여 크로스커팅 스팩을 검사하면 세 가지 이점이 있다. 우선, 크로스 레퍼런스 뷰는 aspect를 개발할 때 즉각적인 피드백을 제공한다. 두 번째, 테스트 하기 힘든 결과를 쉽게 탐지할 수 있다. (getWebsite()가 하이라이트 되지 않았다는 것을 확인했던 테스트를 작성하기 위해 getWebsite()가 에러의 소스가 될 것인지를 확인하거나 SearchResult에 대한 모든 String getter를 검사해야 했다. 에러 가능성이 낮을수록 사전에 테스트하기 더 힘들다.) 세 번째로, 자동으로 생성된 뷰는 코드에서 확인하기에는 지루한 케이스들을 확인할 수 있다. 예를 들어, 검색 하이라이터는 20개의 조인 포인트들에 영향을 미치기 때문에 크로스 레퍼런스 뷰를 검사하는 것이 각 조인포인트에 대한 테스트를 작성하는 것 보다 더 쉽다.

단점이라고 한다면 검사가 자동화 될 수 없다는 점이다. 훈련이 필요하다. 성질 급한 프로그래머는 그림 2를 검사해도 버그를 찾을 수 없다. (다음 패턴은 이 문제에 대한 부분적인 솔루션이다.) 또 다른 문제는 크로스커팅 뷰가 정적 조인 포인트에만 근거하여 매치를 보여준다는 것이다. 다시 말해서, cflow() 또는 if() 같은 런타임 체크에 의존하는 포인트컷이 있다면 런타임 시 매치할 조인 포인트는 볼 수 없을 것이다

Pattern 2. 크로스커팅 비교 툴을 사용한 변경 사항 검사#

Addresses: 크로스커팅 스팩

요약: AJDT의 크로스커팅 비교 기능을 사용하면 리팩토링 또는 또 다른 코드 변경 전에 프로젝트의 크로스커팅 맵을 저장할 수 있다. 수정을 완료한 후에 또 다른 맵을 저장할 수 있다. (비교할 맵을 매일 저장할 수도 있다.)크로스커팅 비교 툴에서 맵들을 비교하여 조인 포인트에 원치 않는 수정이 가해졌는지를 검사한다. 오직 AJDT만 크로스커팅 비교 툴을 제공한다.

예: pointcut 재작성

이전 예제의 문제를 해결하기 위해 Java 5 주석을 사용하여 pointcut을 수정하기로 결정했다고 가정해 보자.

public pointcut highlightedTextProperties() :
       execution(@Highlighted public String Highlightable+.*())

주석을 다음과 같이 소스에 추가한다.

  @Highlighted
  public String getTitle() {
    return title;
  }

변경 전후의 프로젝트 스냅샷을 비교한다. (그림 3) 여러분도 보다시피, 리팩토링으로 getWebsite()에 대한 어드바이스 매치가 제거되었고 getSummary()에 대한 매치 역시 제거되었다.

그림 3. 크로스커팅 변경 툴에 나타난 변경 결과 RemovedSummaryAndWebsite.jpg

장점과 단점

이 기술은 이전 기술보다 더 세련된 기술이다. 변경 사항들을 보여주는 것 만으로도 크로스커팅 비교 툴은 정보 상실을 방지할 수 있다. 또한 크로스 레퍼런스 뷰에서는 분석하고자 하는 어드바이스나 클래스를 선택해야 했지만 크로스커팅 비교 툴로는 전체 프로젝트에서 변경 사항들을 검사할 수 있다.

하지만 크로스커팅 비교 뷰는 aspect가 많은 조인 포인트에 영향을 미칠 경우에는 기능이 강등된다. 모든 퍼블릭 메소드를 기록하는 aspect를 가정해 보자. 이 aspect는 개발이 끝날 때 마다 십여 개의 새로운 변경 사항들을 크로스커팅 뷰에 추가하기 때문에 다른 더 중요한 변경 사항들을 보기 힘들다. 특정 aspect의 변경사항에 대한 경고를 만들고 다른 aspect와 관련된 변경 사항들을 무시하도록 크로스커팅 비교 툴을 설정할 수 있다면 이상적이다.


III. 델리게이션 사용하기#

aspect는 종종 일반 객체들을 사용하여 자신의 크로스커팅 작동을 구현한다. 이렇게 하면 크로스커팅 스팩과 개별적으로 작동을 테스트 할 수 있다. 다음의 두 패턴은 델리게이션과 목(mock) 객체들을 사용하여 aspect를 검사하는 방법이다.

Pattern 1. 위임된 어드바이스 로직 테스트하기#

Addresses: 크로스커팅 기능

요약: 어드바이스 로직 일부 또는 전체를 직접 테스트 할 수 있는 또 다른 클래스로 위임한다. (작동을 퍼블릭 메소드에 위임할 수도 있다.)

예: 하이라이팅 로직을 또 다른 클래스로 옮기기

하이라이팅 로직을 고립된 상태에서 테스트 하려면 이것을 전용 유틸리티 클래스로 옮긴다.

  private HighlightUtil highlightUtil = new CssHighlightUtil();
  
  public void setHighlightUtil(HighlightUtil highlightUtil){
  this.highlightUtil = highlightUtil;
  }
  
  String around(Highlightable highlightable
      highlightedTextProperties() && this(highlightable
  {
    String result = proceed(highlightable);
    return highlightUtil.highlight(result, highlightable.getHighlightedWords());
  }

하이라이팅 로직을 추출하여 이것에 대한 단위 테스트를 작성할 수 있다. (HighlightUtil 클래스에 대해 메소드를 호출)

장점과 단점

이 기술은 도메인 로직에서 엣지 케이스를 자극하기가 쉽다. 또한 버그 고립화도 돕는다. 이 헬퍼용 테스트가 실패하면 aspect가 아닌 바로 이것 탓이라는 것을 안다. 마지막으로 로직을 위임하면 컨선(concern)들이 분리된다. 예를 들어 이것을 또 다른 클래스로 추출함으로서 텍스트 하이라이팅은 시스템의 다른 부분들이 aspect와 독립적으로 사용할 수 있는 연산이 된다. 마찬가지로 aspect는 대안 하이라이팅 전략(HTML용 CSS 하이라이팅)을 사용할 수 있는 유연성을 획득한다.

부정적인 측면으로는 이 기술은 로직이 추출하기 어려우면 작동하지 않는다는 점이다. 예를 들어, 간단한 로직을 직렬식이 되도록 하는 것이 최선이다. 어떤 aspect는 이들이 참조하는 클래스에 대해 로컬 또는 ITD에 상태를 저장한다. 상태 저장은 종종 aspect의 로직에 중요한 부분을 형성하고 헬퍼로 깨끗하게 이동될 수 없다.

Pattern 2. 목(mock) 객체를 사용하여 어드바이스 실행을 기록하기

Addresses: 크로스커팅 스팩과 기능

요약: 이 기술은 이전 기술을 보완한다. 또 다른 클래스로 어드바이스 작동을 추출했다면 헬퍼 객체용 목 객체를 대체할 수 있고 어드바이스가 조인 포인트에서 실행되는지를 확인한다. 또한 이 어드바이스가 정확한 컨텍스트를 헬퍼로 전달할 수 있다. 어드바이스 매개변수에서 직접 전달하거나 이전에 저장된 상태에서 전달한다.

주: 참고자료 섹션에 목 객체에 대한 소개 링크가 제공된다.

예: mock HighlightUtil을 사용하여 Highlighting 객체 테스트하기

우리는 이미 실제 텍스트 하이라이팅을 핸들하기 위해 또 다른 클래스로 aspect를 위임하는 방법을 보았다. 이것은 하이라이터의 다른 구현을 테스트 하는 동안 aspect로 투입하는 경로를 따른다. Listing 3의 코드는 JMock 라이브러리를 활용한다. (참고자료)

Listing 3. JMock을 사용하여 aspect로부터 호출 테스트하기

public class DelegatedHighlightingUnitTest extends MockObjectTestCase {

  Collection<String> words;
  private HighlightUtil original;
  private SearchResult result;
  private Mock mockUtil;

  public void setUp() throws Exception {
    super.setUp();

    setUpMockHighlightUtil();

    words = Collections.singleton("big");

    result = new SearchResult();
    result.setTitle("I am a big bear!");
    result.setHighlightedWords(words);
  }

  private void setUpMockHighlightUtil() {
    original = HighlightResults.aspectOf().getHighlightUtil();
    mockUtil = mock(HighlightUtil.class);
    HighlightResults.aspectOf().setHighlightUtil((HighlightUtil)mockUtil.proxy());
  }


  public void testHighlightUtilAppliedToTitleOfSearchResult() {
    mockUtil.expects(once())
      .method("highlight")
      .with(eq("I am a big bear!"), eq(words));
    result.getTitle();
  }
}

setUp() 메소드는 목 객체의 인스턴스를 만들고 이것을 aspect로 삽입한다. 테스트 메소드는 목에게 두 개의 인자(getTitle()의 리턴 값과 SearchResult에 저장된 단어 리스트)들을 취하는 "highlight"라는 이름을 가진 메소드로의 호출을 기다릴 것을 명령한다. 일단 기대치가 설정되면 이 테스트는 getTitle() 메소드를 호출한다. 이것은 aspect를 실행하고 예상된 대로 목에 호출한다. 목이 호출을 받지 못하면 테스트는 자동으로 실패한다.

setUp() 메소드는 원래 HighlightUtil로 의 레퍼런스를 저장한다. 이 aspect는 싱글톤이기 때문이다. 이 때문에 분해하는 동안 목 투입의 결과를 실행취소 하는 것이 중요하다. 그렇지 않으면 목은 aspect에 남아있게 되고 다른 테스트에 영향을 미치게 된다. 이 aspect의 정확한 분해는 아래와 같다.

  @Override
  protected void tearDown() throws Exception {
    try {
      HighlightResults.aspectOf().setHighlightUtil(original);
    finally {
      super.tearDown();
    }
  }

장점과 단점

이 패턴은 이전 것을 보완한다. 이것은 크로스커팅 작동 보다는 크로스커팅 스팩과 aspect의 컨텍스트 핸들링을 테스트한다. aspect의 결과에서 간접적인 부작용을 체크하여 부담을 줄일 수 있기 때문에 조인-포인트 매핑과 컨텍스트-패싱 작동에서 코너 케이스를 쉽게 자극할 수 있다.

로직을 위임할 때의 장점과 단점을 분석하고 목을 사용하는 테스팅은 기술을 객체에 적용하든 aspect에 적용하든 비슷하다. 두 경우 모두 컨선을 분리하여 각 컨선을 보다 고립된 방식으로 검사할 수 있다.

목을 투입할 때 aspect만의 고유의 문제가 있다. 싱글톤 aspect(디폴트)를 사용한다면 aspect의 필드의 어떤 변경사항이라도 테스트의 끝에서는 실행 취소 된다. (그렇지 않으면, 목은 남아있게 되고 나머지 시스템에 영향을 미치게 된다.) 분해 로직은 구현과 기억이 까다롭다. 테스트 클린업 aspect를 작성하는 것은 개념상으로는 간단하지만 이 글에서는 설명하지 않겠다.


IV. Mock 타겟 사용하기

마지막 섹션에서는 aspect 테스트 작성에 유용한 테스트 헬퍼의 유형을 기술할 때 고안해 냈던 용어를 소개하겠다. 바로 목 타겟(mock targets)이 바로 그것이다. 이전 aspect에서는 목 객체는 테스트하고자 하는 몇몇 클래스용 협업자를 모방했던 클래스를 의미했다. 이와 비슷하게 목 타겟은 테스트를 시도하고자 하는 몇몇 aspect의 정식 어드바이스 타켓을 모방한다.

목 타겟을 만들려면 제품 환경에서 참조하고 싶은 것과 비슷한 작동과 구조를 갖고 있는 클래스를 작동한다. 예를 들어, 게터에 의해 리턴된 텍스트의 하이라이팅에 관심이 있다면 다음과 같이 목 객체를 작성할 수 있다.

//an inner class of the enclosing test case
public class HighlightMockTarget implements Highlightable {
  public String getSomeString() {
      return "I am a big bear!";
  }
}

그런 다음, 타겟과 정확하게 인터랙팅 하는지를 확인 할 테스트를 작성한다. (Listing 4)

Listing 4. Interacting with a mock target to test advice 참조를 테스트 할 목 객체와의 인터랙팅

public void setUp() throws Exception {
  super.setUp();

  setUpMockHighlightUtil();

  words = Collections.singleton("big");
  mockTarget = new HighlightMockTarget();
  mockTarget.setHighlightedWords(words);

}

//mock setup/tearDown omitted

public void testHighlighting() {
  mockUtil.expects(once())
    .method("highlight")
    .with(eq("I am a big bear!"), eq(words))
    .will(returnValue("highlighted text"));
  String shouldBeHighlighted = mockTarget.getSomeString();
  assertEquals(shouldBeHighlighted, "highlighted text");
}

이 예제에서 나는 목 타겟과 목 객체를 결합했다. (Section III, Pattern 2 참조). 목 타겟은 다음 세 가지 기술들을 입증한다.

Pattern 1. 추상 aspect를 확장하고 포인트컷을 제공하여 어드바이스를 테스트하기

Addresses: 크로스커팅 기능

요약: 사전작업(Prework): 필요할 경우, aspect를 재작성 하여 추상 aspect와 구체 aspect로 나눈다. 이것을 확장하여 한 개 이상의 포인트 컷을 구체화한다. 일단 추상 객체를 갖고 있다면 테스트 클래스 안에 목 타겟을 만든다.

추상 aspect를 확장할 테스트 aspect를 구현한다. 이 테스트 aspect가 목 타겟을 분명하게 목표하고 있는 포인트 컷을 제공한다. 이 테스트는 aspect의 어드바이스가 어드바이스의 부작용을 검색하거나 목 객체를 사용하면 좋다.

예: AbstractHighlighter 확장하기

이미 이전 섹션에서 테스트 코드를 작성했다고 가정해 보자. 이 테스트가 성공하려면 Highlighter aspect를 추상 aspect와 하위 aspect(subaspect)로 분리해야 한다.

public abstract aspect AbstractHighlighter {
  public abstract pointcut highlightedTextProperties();

  //... aspect continues
}

public aspect HighlightResults extends AbstractHighlighter {
  public pointcut highlightedTextProperties() :
  (
  //...define pointcut as before
  );
}

AbstractHighlighter aspect를 다시 테스트 케이스용 aspect로 확장한다. 아래는 이 테스트 케이스의 정적인 내부 aspect이다.

private static aspect HighlightsTestClass extends AbstractHighlighter {
   public pointcut highlightedTextProperties() :
     execution(public String HighlightMockTarget.*(..));
}

aspect는 목 타겟에 대한 모든 메소드 실행을 선택하여 highlightedTextProperties 포인트컷을 구체화한다.

장점과 단점

확실히, 이 테스트는 꾸며낸 상황이다. 가짜 객체에 가짜 객체를 테스팅 한다. 하지만 진짜 포인트컷은 테스팅하지 않음을 드러낸다. 추상 aspect에서 정의된 어드바이스와 ITD 코드를 확인할 수도 있다. 예제에서 이 테스트는 어드바이스가 ITD로부터 데이터를 정확하게 마샬링하는지를 검사한다. 원래 조인 포인트의 리턴 값이, 이것을 유틸리티 클래스로 전달하면서 새로운 결과를 리턴한다. 이것은 만만치 않은 작동이다. 목 객체를 사용하면 테스트 리더(reader)가 실제 타겟과 aspect의 작동에 대해 생각할 수 없기 때문에 테스트가 더 깨끗해진다. 이러한 종류의 테스트는 aspect 라이브러리용 단위 테스트를 작성한다면 특별히 유용하다. 왜냐하면 aspect가 개별 애플리케이션으로 짜여질 때 까지 실제 타겟이 없기 때문이다.

aspect를 나누어서 이 패턴을 사용한다면 보다 확장성 있게 될 것이다. 이 시스템의 새로운 부분들이 하이라이팅 작동에 참여해야 한다면 이들은 새롭게 추상화 된 객체를 확장하고 새로운 상황을 다루는 포인트컷을 정의할 수 있다. 사실 추상 aspect는 이것이 참조하는 시스템으로부터 연결이 해제된다.

Pattern 2. 목 타겟을 이용하여 포인트컷 매칭 테스트하기

Addresses: 크로스커팅 스팩과 기능

요약: 이 기술은 이전 기술과 밀접한 관련이 있다. 추상 객체를 확장하는 대신 목 타겟을 작성하여 이것이 테스팅 되는 aspect의 포인트컷과 매치하도록 한다. aspect가 목 타겟을 참조하는지의 여부를 체크하여 포인트컷이 정확한지를 테스트 할 수 있다. 테스트하고 싶은 포인트컷이 과도하게 구체적이면 이것을 재작성하여 목 타겟이 어드바이스로 쉽게 "subscribe(기부)"하도록 한다

예: 마커 인터페이스에 기반한 포인트컷 테스팅

하이라이팅 aspect를 추상화 하는 대신 포인트컷을 재작성 하여 이것이 Highlightable 인터페이스 상의 메소드 실행과 매치하도록 한다.

public pointcut highlightedTextProperties() :
  execution(public String Highlightable+.get*());

이 광범위한 포인트컷은 Highlightable에 대한 String getter와 매치한다. 이 포인트컷은 특정 클래스를 세지 않기 때문에 목 타겟의 getSomeString() 메소드와 이미 매치한다. 테스트의 나머지는 같다.

변형: 주석 사용하기

Java 5.0 메타데이터에 기반하여 부분적으로 매치하는 포인트컷을 작성할 수 있다. 예를 들어 아래와 같은 개정된 포인트컷은 @Highlighted 주석으로 꾸며진 메소드 실행을 매치한다.

public pointcut HighlightedTextProperties() :  
  execution(@Highlighted public String Highlightable+.*());

//you can apply the annotation in the source, or using the declare-annotation form
declare @method public String SearchResult+.getTitle(..@Highlighted;
declare @method public String SearchResult+.getProduct(..@Highlighted;

이 주석을 getSomeString() 메소드에 추가하여 목 타겟이 새로운 포인트컷과 매치하도록 한다.

  @Highlighted
  public String getSomeString() {
    return "I am a big bear!";
  }

장점과 단점

이 기술 역시 aspect 작동의 테스팅을 타겟 애플리케이션의 작동과 완전히 분리하여 독립적인 테스트가 되도록 한다. 목 타겟을 수용하도록 포인트컷이 작성되지 않으면 이들을 재작성 함으로서 보다 디커플된 애스팩트로 끝날 수 있다. aspect가 테스트 클래스 내의 목 타겟에 영향을 미치도록 함으로서 실제 클래스가 aspect의 작동에 참여하는 것이 쉬워질 수 있다.

Pattern 3. 복잡한 포인트컷 확인하기(특별 케이스)

Addresses: 크로스커팅 스팩과 기능

요약: 이전 목 타겟은 간단했지만 복잡한 조인 포인트(cflow()) 또는 조인 포인트의 시퀀스를 시뮬레이트 할 목 타겟을 작성할 수 있다.

예: cflow 시뮬레이트

다운로드 된 리포트용 하이라이팅을 꺼야한다. highlightExceptions 포인트컷을 추가하여 ReportGenerator에 의해 호출된 게터들을 배제한다.

public pointcut highlightedTextProperties() :
  execution(public String Highlightable+.get*())
  && !highlightExceptions();
  
public pointcut highlightExceptions() 
  cflow(execution(* ReportGenerator+.*(..)));


그런다음 HighlightMockTarget을 호출했던 목 ReportGenerator를 작성하여 어떤 하이라이팅도 없다는 것을 테스트한다.

private class MockGenerator implements ReportGenerator {
  public void write(OutputStream streamthrows IOException {
    mockTarget.getSomeString();
  }
}

public void testNoHighlight() throws Exception {
  mockUtil.expects(never()).method("highlight");
  MockGenerator accessor = new MockGenerator();
  accessor.write(null);
}

하지만 보다 복잡한 매칭 상황 (somePointcut() && ! cflowbelow(somePointcut()))에도 비슷한 목 타겟들을 만들 수 있다. 시각화 툴은 cflow() 같은 런타임 체크를 사용하는 포인트컷을 위한 매칭에 대해 좋은 정보를 주지 않는다. 몇 가지 대표적인 목 타겟으로 포인트컷을 검사하는 것이 좋다.


결론

좋은 테스트 수트가 없는 코드는 전형적으로 버그가 많이 생기고 변경하기도 힘들며 팩토링도 형편 없다.

aspect를 테스트하는 것은 객체를 테스트 하는 것과 많이 비슷하다. 두 경우 모두 작동을 컴포넌트로 나누어 개별적으로 테스트 해야 한다. 핵심 개념은, 크로스커팅 작동을 두 개의 다른 영역으로 나누는 것이다. 우선, 크로스커팅 스팩이 있다. 이곳에서 여러분은 컨선이 영향을 미칠 부분이 프로그램의 어떤 부분인지를 물어야 한다. 두 번째로 기능이 있다. 여기에서는 그러한 포인트에 무슨 일이 일어나는지를 물어야 한다. 객체들만 사용한다면 이들 두 영역은 꼬인다. 컨선이 애플리케이션을 통해 그 자체로 혼합된다. 하지만 aspect를 사용해서는 고립화 할 영역을 한 개 또는 두 개를 목표로 정할 수 있다.

aspect를 테스트 가능한 것으로 작성하면 디자인 효과를 누릴 수 있다. 객체 지향 코드를 테스팅 가능한 것으로 팩토링 하는 것과 쌍벽을 이룬다. 예를 들어 나의 어드바이스 바디를 개별적으로 테스트 가능한 클래스로 옮기면 애플리케이션을 크로스커팅하는 방식을 이해할 필요 없이 작동을 분석할 수 있다. 포인트컷을 변경하여 목 타겟에 접근할 수 있도록 하면 시스템의 테스트 되지 않은 부분으로 좀더 접근할 수 있다. 두 경우 모두 시스템 유연성과 연결성을 높인다.

aspect 지향 프로그램은 테스트 될 수 없다라는 소문을 들었다. 그 소문은 사라졌지만 나는 여전히 이것이 도전이라고 생각한다. 이 글이 aspect를 테스트하는 방법 뿐만 아니라 크로스커팅을 테스트 할 때 첫 번째로 aspect를 사용하는 것이 낫다라는 개념을 갖게 하기 바란다.

감사의 말

Ron Bodkin, Wes Isberg, Gregor Kiczales, Patrick Chanezon에게 감사의 말을 전합니다.


참고자료

교육

  • Unit testing with mock objects (Alex Chaffee and William Pietri, developerWorks,November 2002)
  • Hacking with Harrop...
  • I don't want to know that ...
  • Enhance design patterns with AspectJ (Nicholas Lesiecki, developerWorks, April 2005)
  • Design with pointcuts to avoid pattern density (Wes Isberg, developerWorks, June 2005)
  • Virtual Mocking ... with jMock (May 28, 2005)
  • AOP@Work: Programming tips for aspect-oriented developers.
  • The Java technology zone

제품 및 기술 얻기

  • AspectJ home page

토론

  • Participate in the discussion forum.
  • developerWorks blogs

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
jpg
IncludesWebsite.jpg 30.4 kB 1 03-Jan-2006 15:40 이동국
jpg
RemovedSummaryAndWebsite.jpg 32.5 kB 1 03-Jan-2006 15:51 이동국
jpg
TestingPyramid.jpg 21.8 kB 1 03-Jan-2006 15:40 이동국
« This page (revision-4) was last changed on 06-Apr-2006 09:45 by 이동국  
G’day (anonymous guest) My Prefs

Referenced by
AspectOrientedProgra...

JSPWiki v2.8.4