Eternity's Chit-Chat

aeternum.egloos.com



도메인 특화 언어와 단위 테스트 - 4부(下)[完] Software Quality

테스트 케이스 리팩토링

TEST DATA BUILDER를 구현했으므로 이제 이전에 작성한 테스트 케이스를 리팩토링하자. 먼저 대륙의 수가 정확한 지를 검증하는 ContinentSpecification의 테스트 케이스부터 살펴 보자. ContinentSpecification은 Continent가 명세를 만족하는 지만 검증하므로 ContinentSpecification테스트를 위해서는 Continent픽스처를 제외한 다른 정보들은 불필요하다. Atmosphere나 Ocean은 Planet을 생성하기 위해 요구되는 정보일 뿐 현재의 테스트 결과와는 직접적인 관계가 없다. 따라서 <리스트 15>에 나열된 현재의 테스트 케이스는 핵심적인 정보가 불필요한 정보의 홍수 속에 묻혀 있으므로 테스트를 이해하고 수정하기가 어렵다. 또한 도메인 객체의 생성자를 직접 호출하고 있기 때문에 도메인 객체의 변경에 대해서도 취약하다. Planet은 애플리케이션에 있어 핵심 도메인 객체이기 때문에 여러 테스트 케이스에서 유사한 픽스처 생성 코드가 중복될 가능성이 높다.

<리스트 15> 행성에 포함된 대륙의 개수를 체크하는 테스트 케이스

public class ContinentSpecificationTest {
  @Test
  public void continentSize() {
    Planet planet = new Planet(
                     new Atmosphere(Money.wons(5000),
                       element("N", Ratio.of(0.8)),
                       element("O", Ratio.of(0.2))),
                     Arrays.asList(
                       new Continent("아시아"),
                       new Continent("유럽")),
                     Arrays.asList(
                       new Ocean("태평양", Money.wons(1000)),
                       new Ocean("대서양", Money.wons(1000))));
   
    ContinentSpecification specification = new ContinentSpecification(2);
       
    assertTrue(specification.isSatisfied(planet));
  }
}
<리스트 15>를 TEST DATA BUILDER 패턴을 이용해 리팩토링하면 Continent 이외의 테스트 결과와 관련이 없는 불필요한 정보를 제거할 수 있다. 테스트와 상관 없는 Atmosphere와 Ocean은 테스트 케이스의 초점을 흐르지 않도록 빌더 안으로 숨겨져 있다. 따라서 픽스처를 생성하는 코드가 간략해지고 테스트 결과와 직접적으로 관련 있는 정보만 코드 상에 보여지기 때문에 테스트의 의도를 파악하기가 쉽다.

<리스트 16> 관련된 정보만을 표현하는 TEST DATA BUILDER

public class ContinentSpecificationTest {
  @Test
  public void continentSize() {
    Planet planet = aPlanet().with(aContinent(), aContinent()).build();
 
    ContinentSpecification specification = new ContinentSpecification(2);
       
    assertTrue(specification.isSatisfied(planet));
  }
}
다음은 행성의 가격을 검증하는 PriceSpecification에 대한 테스트 케이스를 나타낸 것이다. ContinentSpecification과 마찬가지로 대륙에 포함된 국가나 대양의 명칭과 같은 불필요한 정보로 인해 테스트와 직접적으로 관련된 중요한 픽스처 정보가 무엇인지를 파악하기가 어렵다.

<리스트 17> 행성의 전체 가격이 20,000원보다 낮거나 같은 지 여부를 테스트

public class PriceSpecificationTest  {
  @Test
  public void price() {
    Planet planet = new Planet(
                     new Atmosphere(Money.wons(5000),
                       element("N", Ratio.of(0.8)),
                       element("O", Ratio.of(0.2))),
                     Arrays.asList(
                       new Continent("아시아",
                         new Nation("대한민국", Money.wons(1000))),
                       new Continent("유럽",
                        new Nation("영국", Money.wons(1000)))),
                     Arrays.asList(
                       new Ocean("태평양", Money.wons(1000)),
                       new Ocean("대서양", Money.wons(1000))));
   
    Specification specification = new PriceSpecification(Money.wons(9000));
   
    assertTrue
(specification.isSatisfied(planet));
  }
}
TEST DATA BUILDER를 이용해서 리팩토링한 테스트 케이스는 제조 가격 이외의 불필요한 정보를 빌더 내부로 숨기고 있다. 따라서 테스트 케이스가 좀 더 간결해지고 이해하기 쉬워졌음을 알 수 있다.

<리스트 18> 관련된 정보만을 표현하는 TEST DATA BUILDER

public class PriceSpecificationTest  {
  @Test
  public void price() {
    Planet planet = aPlanet()
                      .with(anAtmosphere().with(wons(5000)))
                      .with(aContinent().with(aNation().with(wons(1000))),
                            aContinent().with(aNation().with(wons(1000))))
                      .with(anOcean().with(wons(1000)),
                            anOcean().with(wons(1000)))                                
                      .build();

    Specification specification = new PriceSpecification(Money.wons(9000));
   
    assertTrue
(specification.isSatisfied(planet));
  }
}
두 가지 테스트 케이스의 리팩토링을 통해 알 수 있는 것처럼 TEST DATA BUILDER는 다양한 픽스처의 상태를 조합해야 하는 상황에서도 유연하게 대처가 가능하다. 도메인 객체의 계층을 따라 빌더의 계층을 구축하고 빌더를 조합함으로써 다양한 상태의 복합 객체를 쉽게 생성할 수 있다.

TEST DATA BUILDER 확장
최근 영속성 메커니즘으로 JPA(Java Persistence API)를 사용하는 프로젝트에 참여했던 적이 있다. 프로젝트에 합류하던 시점에 개발팀은 픽스처 생성 코드를 관리하기 위해 FACTORY 기반의 OBJECT MOTHER 패턴을 사용하고 있었다. 개발팀은 픽스처 생성과 관련된 코드 중복 문제를 해결하기 위해 메모리 객체뿐만 아니라 REPOSITORY 테스트에서 사용할 영속 객체 또한 OBJECT MOTHER를 통해 생성하기를 원했다. 문제는 시간이 흐를수록 OBJECT MOTHER가 급격하게 복잡해져 갔다는 점이다.
OBJECT MOTHER는 메모리 객체를 생성하는 createTransient…() 류의 메서드들과 영속 객체를 생성하는 createPersistent…()류의 메서드들이 얽히고 설킨 상태로 방치되어 있었다. createPersistent…() 메서드들은 내부적으로 createTransient…() 메서드들을 사용하고 있었기 때문에 메서드 간의 강한 결합과 코드 중복으로 인해 테스트 케이스를 관리하기가 점점 어려워져 갔다. 결국 시간이 흐를수록 생성 메서드(CREATION METHOD)의 수는 급격하게 증가하기 시작했으며 픽스처를 생성하는 코드는 이해하기도, 유지보수하기도 어려운 애물단지로 전락하고 말았다. 결국 OBJECT MOTHER를 TEST DATA BUILDER로 대체하기로 결정했다.
먼저 메모리 객체를 생성하는 TEST DATA BUILDER를 구현한 후 createTransient…() 계열의 메서드 대신 빌더를 사용하도록 테스트 케리스를 리팩토링했다(실제 프로젝트에 적용한 방법은 반복되는 코드(boilerplate code)를 최소화하기 위해 약간의 리플렉션과 메타 프로그래밍 기법을 조합했다. 실제 프로젝트에서 사용한 TEST DATA BUILDER는 본 글에서 설명한 코드보다 간단한 방법으로 구현이 가능하지만 기본적인 원리는 거의 동일하다). 영속성 관련 로직을 한 곳에 모아 둘 추상 클래스를 추가한 후 모든 빌더들이 추상 클래스를 상속하도록 했다. CGLIB의 바이트 코드 향상 기법과 DECORATOR 패턴을 이용해 픽스처를 생성하는 build() 메서드의 호출을 인터셉트함으로써 생성된 픽스처가 자동으로 데이터베이스에 저장되도록 만들었다. 애플리케이션 로딩 시에 수집된 클래스의 메타 데이터를 이용해 외래키(Foreign key) 관계를 자동으로 판단하도록 함으로써 영속 객체 생성과 관련된 참조 무결성 이슈도 해결할 수 있었다.
사용 방법 역시 매우 간단했는데 기존의 빌더 호출 코드를 추상 클래스에서 제공하는 persisted() 메서드로 감싸기만 하면 되었다. persisted() 메서드는 DSL 스크립트 작성에 필요한 문맥 변수(CONTEXT VARIABLE)의 범위를 객체 내부로 제한하고 공통 코드를 제공하기 위해 슈퍼 클래스를 사용하는 객체 범위(OBJECT SCOPING)기법을 적용한 것이다.

    Continent continent = persisted(
                                         aContinent()
                                             .with(aNation().with("캐나다"),
                                                     aNation().with("미국")))
                                         .build();

모든 빌더가 픽스처를 생성하기 위해 동일한 구조와 코딩 규칙을 준수하고 있었기 때문에 간단한 규칙의 추가만으로도 영속성을 관리하는 인프라 영역으로 빌더를 쉽게 확장할 수 있었다. 결론적으로 TEST DATA BUILDER 패턴은 기반 인프라스트럭처에 맞추어(또는 특정 도메인의 요구사항에 맞추어) 빌더의 기능을 쉽게 확장할 수 있도록 한다.

TEST DATA BUILDER의 장점

TEST DATA BUILDER 패턴은 테스트 케이스 작성과 관련된 다양한 문제점들을 해결하기 위한 DSL 관점의 접근방법이다. 이 패턴은 생성자를 직접 호출하거나 OBJECT MOTHER 패턴을 이용해 생성 오퍼레이션을 캡슐화하는 방법보다 간결하고 변경에 유연하며 중복 코드의 양을 줄일 수 있다. 또한 모든 빌더들이 동일한 구현 패턴을 따르기 때문에 영속성과 같은 부분으로의 확장이 용이하다.

다음은 TEST DATA BUILDER 패턴의 장점을 설명한 것이다.
  • 애매한 테스트(OBSCURE TEST) - 테스트에서 픽스처와 관련 없는 정보(IRRELEVANT INFORMATION)를 너무 상세하게 많이 보여주거나 미스터리한 손님(Mysterious Guest)과 같이 중요한 정보를 숨기고 보여주지 않을 경우 시스템의 동작에 영향을 미치는 것이 무엇인지 파악하기 어렵다. TEST DATA BUILDER는 테스트와 관련된 정보만 표현하고 불필요한 정보는 빌더 내부로 감춤으로써 각 테스트 케이스에서 중요한 정보가 무엇인지를 쉽게 파악할 수 있도록 한다.
  • 깨지기 쉬운 테스트(FRAGILE TEST) – 테스트 케이스에서 생성자를 직접 호출할 경우 생성자의 시그니처가 변경될 경우 해당 객체들을 참조하는 모든 테스트 케이스를 수정해야 한다. TEST DATA BUILDER는 생성자 호출 부분을 build() 메서드 내부로 캡슐화하고 속성을 설정할 수 있는 유창한 인터페이스만을 외부로 노출하기 때문에 변경에 유연하게 대처할 수 있다.
  • 테스트 코드 중복(TEST CODE DUPLICATION) – 유사한 주제에 대해서 약간씩 다른 시나리오로 테스트를 해야 할 경우 유사한 픽스처 로직이 여러 테스트 케이스에 중복되어 나타나게 된다. 특히 테스트 케이스와 관련이 없는 정보가 중복되는 경우 애매한 테스트가 되고 만다. TEST DATA BUILDER 패턴은 테스트에 중요한 정보만을 노출하고 불필요한 정보는 숨기기 때문에 테스트와 관련이 없는 중복 코드의 양을 크게 줄일 수 있다. 또한 생성 메서드 간에 제거하기 미묘한 중복 코드를 포함하는 OBJECT MOTHER 패턴과 달리 TEST DATA BUILDER는 하나의 빌더로 다양한 상태 조합이 가능하기 때문에 중복 코드의 발생 가능성을 줄일 수 있다.
  • 높은 테스트 유지 비용(HIGH TEST MAINTENANCE COST) – 애매하고, 깨지기 쉬우며, 중복된 코드는 유지보수하기가 어렵다. TEST DATA BUILDER 패턴은 세 가지 문제를 비교적 깔끔하게 해결함으로써 낮은 비용만으로 테스트를 유지할 수 있도록 한다.
  • 테스트를 작성하지 않는 개발자(DEVELOPERS NOT WRITING TEST) – TEST DATA BUILDER 의 경우 테스트 작성과 유지보수에 드는 비용이 상대적으로 적기 때문에 개발자는 적극적으로 테스트 케이스를 작성하려고 노력 한다. TEST DATA BUILDER의 비용과 관련된 문제점은 테스트 케이스 자체의 작성 비용이 아니라 빌더 자체를 작성하는데 드는 비용이 높다는 점이다. 경험에 따르면 빌더는 한 번 작성해 두면 여러 테스트 케이스에서 유용하게 반복적으로 재사용되기 때문에 초기 비용을 감수할만한 가치가 있으며 리플렉션이나 메타 프로그래밍을 이용해 최대한 반복되는 코드(boilerplate code)의 양을 줄일 수 있다는 점이다.

TEST DATA BUILDER를 사용하라

복잡한 도메인을 다루는 애플리케이션에서 테스트 케이스에 사용할 픽스처를 관리하는 데 애를 먹고 있다면 TEST DATA BUILDER 패턴을 적용해 보길 권한다. TEST DATA BUILDER 패턴을 사용하면 테스트에 부차적인 픽스처 생성이 아니라 테스트의 본질인 테스트의 목적에 집중할 수 있게 된다. 더 중요한 점은 테스트 케이스 작성이 쉽고 재미있어 진다는 점이다.

단위 테스트에 중독되는 이유는 단위 테스트가 진행중인 작업의 설계와 안전성에 대한 빠른 피드백을 제공하기 때문이다. 빠른 피드백은 단순하고 직관적이며 아름다운 테스트 코드로부터 나온다. TEST DATA BUILDER는 이 모든 것을 가능하게 함으로써 테스트를 즐거운 작업으로 만든다.

테스트를 직관적이고 아름답게 만드는 TEST DATA BUILDER의 접근 방법은 근본적으로 테스트를 위한 언어를 만드는 DSL의 전통으로부터 기인한다. DSL의 가장 큰 특징은 제한된 도메인에 최적화된 단순하고, 간결하며, 유창한 언어를 제공한다는 것이다. TEST DATA BUILDER는 DSL의 이런 특징을 계승한다. 빌더는 테스트 케이스를 단순하고, 간결하게 만들며 유창한 언어를 제공함으로써 적은 코드로도 풍부한 정보를 신속하게 전달할 수 있도록 한다.

Steve Freeman과 Net Pryce는 TEST DATA BUILDER 패턴의 장점을 아래와 같이 명확하고 직관적인 어조로 설명한다.

TEST DATA BUILDER를 적용함으로써 테스트를 표현력 있게 작성하고 변경에 대해 탄력적으로 코드를 유지할 수 있다는 점을 발견했다. 첫째, 새로운 객체를 만들 때 대부분의 문법적인 잡음을 감싼다. 둘째, 간단한 경우를 단순하게 만들고, 특수한 경우를 과할 정도로 복잡하게 만들지 않는다. 셋째, 테스트를 객체의 구조 변경으로부터 보호한다. 생성자에 인자를 추가할 경우 적합한 빌더와 새로운 인자를 필요로 하는 테스트만 변경하면 된다.
-    Steve Freeman, Net Pryce, Growing Object-Oriented Software, Guided by Tests
결론은 간단하다. 현재 복잡한 픽스처 생성 문제로 골치가 아프거나 이해하기 어려운 테스트 케이스로 인해 품질이 저하되고 리팩토링이 어려운 곤란한 상황에 처해 있다면 TEST DATA BUILDER의 사용을 고민해 보자. TEST DATA BUILDER는 테스트 작성을 고통이 아니라 즐거움으로 바꿔줄 것이다.

핑백