Eternity's Chit-Chat

aeternum.egloos.com



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

FACTORY를 이용한 생성 메서드(CREATION METHOD) 중복 제거
픽스처 생성과 관련해서 테스트 케이스 간의 중복을 제거하는 또 다른 방법은 테스트 케이스의 슈퍼 클래스가 아닌 별도의 독립 클래스로 생성 메서드(CREATION METHOD)를 옮기는 것이다. 독립 클래스는 테스트에 필요한 픽스처를 생성하기 위한 오퍼레이션을 제공하는 일종의 FACTORY다. 이처럼 테스트 케이스 클래스 간의 중복 코드를 제거하기 위해 재사용 가능한 테스트 유틸리티 메서드(TEST UTILITY METHOD)를 제공하는 독립적인 클래스를 테스트 도우미(TEST HELPER)라고 한다.

테스트 케이스 슈퍼클래스(TESTCASE SUPERCLASS)의 경우 테스트 케이스 클래스가 반드시 다른 클래스를 상속받아야 하기 때문에 단일 상속만을 제공하는 Java나 C#과 같은 언어에서는 사용 상의 제약이 따른다. 또한 다양한 픽스처를 생성하기 위해 많은 수의 생성 메서드(CREATION METHOD)를 슈퍼 클래스에 포함시킬 경우 관리와 유지보수에 많은 어려움이 따르게 된다. 개인적으로 테스트 케이스 슈퍼클래스(TESTCASE SUPERCLASS)보다는 테스트 도우미(TEST HELPER)를 선호한다.

지구 검증 소프트웨어의 테스트 케이스가 테스트 도우미(TEST HELPER)를 이용하도록 리팩토링 하기 위해 새로운 클래스인 PlanetFactory를 추가하고 Planet을 생성하는 생성 메서드를 PlanetFactory로 이동시키자. 이동시킨 생성 메서드의 가시성을 public으로 수정하고 PlanetFactory의 인스턴스를 생성하지 않고도 사용 가능하도록 하기 위해 정적 메소드(static method)로 변경하자.

public abstract class PlanetFactory {
    public static Planet createPlanet(Continent ... continents) {
        return new Planet(
                       new Atmosphere(Money.wons(5000),
                           element("N", Ratio.of(0.8)),
                           element("O", Ratio.of(0.2))),
                       Arrays.asList(continents),
                       Arrays.asList(
                           new Ocean("태평양", Money.wons(1000)),
                           new Ocean("대서양", Money.wons(1000))));

    }


    public static Planet createPlanet(Money atmospherePrice,
        List<Money> nationsPrice, List<Money> oceansPrice) {
        return new Planet(
                           createAtmosphere(atmospherePrice),
                           createContinents(nationsPrice),
                           createOceans(oceansPrice));
    }

    private Atmosphere createAtmosphere(Money atmospherePrice) {
        return new Atmosphere(atmospherePrice,
                           element("N", Ratio.of(0.8)),
                           element("O", Ratio.of(0.2)));
    }
 
    private List<Continent> createContinents(List<Money> nationsPrice) {
        List<Continent> result = new ArrayList<Continent>();
        for(Money each : nationsPrice) {
            result.add(new Continent("대륙", new Nation("국가", each)));
        }
        return result;
    }
   
    private List<Ocean> createOceans(List<Money> oceansPrice) {
        List<Ocean> result = new ArrayList<Ocean>();
        for(Money each : oceansPrice) {
            result.add(new Ocean("대양", each));
        }
        return result;
    }

}
이제 테스트 케이스는 픽스처를 생성하기 위해 테스트 도우미(TEST HELPER)생성 메서드(CREATION METHOD)를 호출한다.

public class ContinentSpecificationTest {
    @Test
    public void continentSize() {
        Planet planet = PlanetFactory.createPlanet(
                                   new Continent("아시아"), new Continent("유럽"));
       
        ContinentSpecification specification = new ContinentSpecification(2);
       
        assertTrue(specification.isSatisfied(planet));
    }
}
이와 같이 테스트에 필요한 픽스처의 생성, 수정, 삭제와 관련된 책임을 담당하는 독립적인 테스트 도우미(TEST HELPER)OBJECT MOTHER라고 한다. Peter Schuh과 Stephanie Punke는 "ObjectMother - Easing Test Object Creation in XP"라는 논문에서 OBJECT MOTHER를 다음과 같은 기능을 제공하는 테스트 도우미(TEST HELPER)로 정의하고 있다.
  • 필수적인 속성들을 포함하는 완벽한 상태를 가진 비즈니스 객체를 제공한다.
  • 생명 주기 내의 임의 시점에 요청된 객체를 반환한다.
  • 원하는 상태로 객체를 생성할 수 있는 편리한 방법을 제공한다.
  • 테스트 프로세스 중에 객체의 상태를 변경할 수 있도록 한다.
  • 필요한 경우 테스트 프로세스가 종료될 때 객체 및 관련된 모든 다른 객체들을 제거한다.
<그림 2> OBJECT MOTHER를 통한 중복 코드 제거

OBJECT MOTHER을 사용하면 클래스 계층 구조와 무관하게 테스트 케이스 클래스로부터 픽스처 생성과 관련된 부담을 제거할 수 있다. 그러나 이같은 장점에도 불구하고 FACTORY 기반의 OBJECT MOTHER는 구축된 시스템의 규모가 증가하면 증가할 수록 유지보수성의 한계에 봉착하게 된다.


OBJECT MOTHER의 한계
객체지향 시스템에서 다양한 실행 경로를 테스트하기 위해서는 픽스처의 상태를 자유롭게 설정할 수 있어야 한다. 많은 실행 에러가 분기문을 적절하게 처리하지 못할 경우에 발생하기 때문에 분기 판단에 사용될 픽스처의 속성을 다양한 상태로 설정할 수 있도록 테스트 케이스를 설계하는 것은 단위 테스트의 신뢰성 측면에서 매우 중요하다.

앞에서 설명한 것처럼 다양한 상태의 픽스처를 생성하는 동시에 테스트 케이스를 단순하게 유지하는 방법은 테스트에 중요한 상태 정보만을 인자로 받는 매개변수화된 생성 메서드(PARAMETERIZED CREATION METHOD)를 사용하는 것이다. 매개변수화된 생성 메서드(PARAMETERIZED CREATION METHOD)를 사용하면 테스트 케이스와 관련된 중요한 정보만을 강조할 수 있다는 장점이 있지만 유지보수 측면에서 한 가지 어려움을 감수해야 한다. 그것은 테스트 케이스에서 필요한 픽스처의 상태에 따라 매개변수화된 생성 메서드(PARAMETERIZED CREATION METHOD)의 수가 급격하게 증가한다는 점이다.

행성에 포함된 대양의 수를 검증하는 새로운 OceanSpecification 클래스를 추가한다고 가정하자.

public class OceanSpecification extends AbstractSpecification {
    private int oceanSize;
   
    public OceanSpecification(int oceanSize) {
        this.oceanSize = oceanSize;
    }
   
    @Override
    public boolean isSatisfied(Planet planet) {
        return planet.getOceans().size() == oceanSize;
    }
}
Planet의 생성자를 직접 호출해서 픽스처를 생성하는 테스트 케이스는 다음과 같이 작성할 수 있다.

public class OceanSpecificationTest {
    @Test
    public void oceanSize() {
        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("아시아")),
                                   Arrays.asList(
                                        new Ocean("태평양", Money.wons(1000)),
                                        new Ocean("대서양", Money.wons(1000))));
       
        OceanSpecification specification = new OceanSpecification(2);
       
        assertTrue(specification.isSatisfied(planet));
    }
}
이 테스트 케이스의 결과와 관련된 Planet의 상태 정보는 Ocean뿐이다. Atmosphere와 Continent 정보는 테스트와는 상관없이 Planet의 생성자가 요구하기 때문에 컴파일 에러를 방지하기 위해 생성하는 것뿐이다. 따라서 Ocean만을 인자로 받고 그 외의 정보는 기본 값으로 할당하는 매개변수화된 생성 메서드를 PlanetFactory에 추가하고 이를 호출함으로써 테스트 케이스의 가독성과 유지보수성을 향상시킬 수 있다.

public class OceanSpecificationTest {
    @Test
    public void oceanSize() {
        Planet planet = PlanetFactory.createPlanet(
                                   new Ocean("태평양", Money.wons(1000)),
                                   new Ocean("대서양", Money.wons(1000)));
       
         OceanSpecification specification = new OceanSpecification(2);
       
         assertTrue(specification.isSatisfied(planet));
    }
}
다음으로 대륙과 대양의 개수를 동시에 체크하는 테스트 케이스를 추가한다. 새로운 테스트 케이스는 Specification의 and() 오퍼레이션을 이용해서 생성된 ContinentSpecification과 OceanSpecification의 복합 Specification을 픽스처로 사용한다.

public class AndSpecificationTest {
    @Test
    public void continentAndOceanSize() {
        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))));
       
        Specification specification = new ContinentSpecification(2)
                                                     .and(new OceanSpecification(2));

         assertTrue(specification.isSatisfied(planet));
    }
}
위 테스트 케이스에서 결과에 영향을 미치는 정보는 Continent와 Ocean의 상태 정보이며 Atmosphere의 상태는 테스트 결과와 무관하다. 따라서 PlanetFactory에 Continent와 Ocean 정보를 파라미터로 받는 매개변수화된 생성 메서드(PARAMETERIZED CREATION METHOD)를 추가하고 이를 이용해 픽스처를 생성하도록 테스트 케이스를 리팩토링 하자.

public class AndSpecificationTest {
    @Test
    public void continentAndOceanSize() {
        Planet planet = PlanetFactory.createPlanet(
                                   Arrays.asList(
                                       new Continent("아시아"),
                                       new Continent("유럽")),
                                   Arrays.asList(
                                       new Ocean("태평양", Money.wons(1000)),
                                       new Ocean("대서양", Money.wons(1000))));
       
         Specification specification = new ContinentSpecification(2)
                                                       .and(new OceanSpecification(2));
       
         assertTrue(specification.isSatisfied(planet));
    }
}
위와 같이 여러 테스트 케이스로부터 픽스처 생성 로직을 PlanetFactory로 옮겨감에 따라 PlanetFactory에는 시그니처는 다르지만 구현 상으로는 유사한 매개변수화된 생성 메서드(PARAMETERIZED CREATION METHOD)가 기하급수적으로 늘어나게 된다.

public abstract class PlanetFactory {
    public static Planet createPlanet(Continent ... continents) {
        ....
    }
   
    public static Planet createPlanet(Money atmospherePrice, List<Money> nationsPrice,
        List<Money> oceansPrice) {

        ....
    }

    public static Planet createPlanet(Ocean ... oceans) {
        ....
    }
   
    public static Planet createPlanet(List<Continent> continents, List<Ocean> oceans) {
        ....
    }
}
PlanetFactory의 예에서 알 수 있는 것처럼 새로운 테스트 케이스를 추가할 때마다 요구되는 픽스처의 상태가 다양하게 변하기 때문에 시간이 지날수록 PlanetFactory에 추가되는 생성 메서드(CREATION METHOD)의 수는 폭발적으로 늘어난다. 또한 상태 조합이 복잡해 질수록 PlanetFactory에 포함된 생성 메서드(CREATION METHOD) 간의 중복 코드 역시 함께 증가하게 된다.

생성 메서드(CREATION METHOD)의 증가에 따른 코드 중복을 해결하는 한 가지 방법은 하나의 잘못된 속성(ONE BAD ATTRIBUTE) 패턴을 사용하는 것이다. 유효한 상태를 가진 기본 픽스처를 생성한 후 일부 속성만 원하는 상태로 수정함으로써 코드 중복을 제거하는 방법을 하나의 잘못된 속성(ONE BAD ATTRIBUTE) 패턴이라고 한다.

우선 픽스처의 모든 속성을 기본값으로 할당하는 createDefault()와 같은 이름을 가진 기본 생성 메서드(CREATION METHOD)를 추가한다. 다른 생성 메서드(CREATION METHOD)들은 기본 생성 메서드(CREATION METHOD)를 호출한 후 반환되는 기본 픽스처의 일부 속성을 원하는 값으로 수정한다.

public abstract class PlanetFactory {
    public static Planet createDefault() {
        return 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))));
    }
}

테스트 메소드에서 직접 사용되는 생성 메서드(CREATION METHOD)에서는 기본 생성 메서드(CREATION METHOD)인 createDefault()에서 반환되는 객체의 일부 속성을 각각의 경우에 맞게 재설정한다.

public abstract class PlanetFactory {
    public static Planet createPlanet(Continent ... continents) {
        Planet result = createDefault();
        result.setContinents(Arrays.asList(continents));
        return result;
    }
   
    public static Planet createPlanet(Ocean ... oceans) {
        Planet result = createDefault();
        result.setOceans(Arrays.asList(oceans));
        return result;
    }
   
    public static Planet createPlanet(List<Continent> continents, List<Ocean> oceans) {
         Planet result = createDefault();
         result.setContinents(continents);
         result.setOceans(oceans);
         return result;
     }
     ....
}
그러나 이와 같은 FACTORY 기반의 테스트 도우미(TEST HELPER)에서 하나의 잘못된 속성(ONE BAD ATTRIBUTE) 패턴을 적용하는 방법에는 다음과 같은 단점이 존재한다.
  • 불변 객체(Immutable Object) 생성 불가능 - 하나의 잘못된 속성(ONE BAD ATTRIBUTE) 패턴을 적용하기 위해서는 기본 생성 메서드가 반환하는 객체의 상태를 변경할 수 있어야 한다. 따라서 객체의 상태를 변경할 수 없는 VALUE OBJECT에 대해서는 하나의 잘못된 속성 패턴을 적용할 수 없다.
  • 캡슐화 저해 - 비록 상태 변경이 가능한 ENTITY라고 하더라도 클래스에 해당 속성을 변경할 수 있는 SETTER가 반드시 존재해야 한다. 비록 테스트를 위해 가시성을 패키지 수준으로 낮춘다고 하더라도 속성 별로 SETTER를 무분별하게 제공하는 것은 객체의 캡슐화를 저해한다.
하나의 잘못된 속성(ONE BAD ATTRIBUTE) 패턴을 사용해 코드 중복을 제거하더라도 FACTORY 기반의 OBJECT MOTHER 패턴은 근본적으로 다음과 같은 문제를 가지고 있다.
  • 생성 메서드(CREATION METHOD)의 폭발적 증가 – 비록 픽스처 생성 코드에 대한 중복은 제거할 수 있을지 몰라도 테스트에 필요한 픽스처의 속성 조합에 따른 생성 메서드(CREATION METHOD)의 폭발적 증가를 막을 수 있는 방법은 없다. 테스트의 수가 많아질수록 어떤 생성 메서드(CREATION METHOD)를 사용해야 하는 지조차 파악하기 힘들게 된다.
  • 클래스 인터페이스 변경으로 인한 파급 효과 – 객체를 생성하는 코드가 FACTORY 내부로 캡슐화되기 때문에 생성자의 시그니처 변경으로 인한 테스트 케이스의 수정은 막을 수 있다. 그러나 속성의 조합을 기반으로 생성 메서드(CREATION METHOD)가 추가되기 때문에 이미 존재하는 속성을 제거하거나 새로운 속성을 추가할 경우 생성 메서드(CREATION METHOD)뿐만 아니라 생성 메서드(CREATION METHOD)를 호출하는 모든 테스트 케이스의 코드를 수정해야 한다.
일정 수준의 복잡도를 갖춘 시스템의 단위 테스트를 작성할 경우 앞서 살펴본 바와 같이 동일한 타입의 객체가 하나 이상의 테스트 케이스에서 픽스처로 사용되는 것이 일반적이다. 따라서 OBJECT MOTHER와 같은 FACTORY 기반의 테스트 도우미(TEST HELPER)를 사용할 경우 여러 테스트 케이스에서 픽스처 생성 코드가 중복되는 문제는 해결할 수 있지만 테스트를 통해 검증해야 하는 픽스처의 상태 변이가 다양해질수록 테스트 코드의 유지보수를 어렵게 만드는 장애물로 변해버리고 만다.

Steve Freeman과 Net Pryce는 “Growing Object-Oriented Software, Guided by Tests”에서 OBJECT MOTHER의 단점을 해결할 수 있는 TEST DATA BUILDER라는 패턴을 제시하고 있다. TEST DATA BUILDER의 기본 아이디어는 FACTORY 패턴이 아닌 BUILDER 패턴을 기반으로 픽스처 생성 코드를 설계하는 것이다. 그러나 TEST DATA BUILDER의 진정한 핵심은 테스트를 위한 전용 언어인 '도메인 특화 언어(Domain-Specific Language, DSL)'를 구축하는 것이다. 따라서 짧게나마 도메인 특화 언어가 무엇인지에 관해 살펴볼 필요가 있다.

핑백

  • 이터너티님의 도메인 특화 언어와 단위 테스트 소개 &laquo; 괴발개발 2015-03-14 00:18:51 #

    ... 와 단위 테스트 &#8211; 1부(上) 도메인 특화 언어와 단위 테스트 &#8211; 1부(下) 도메인 특화 언어와 단위 테스트 &#8211; 2부(上) 도메인 특화 언어와 단위 테스트 &#8211; 2부(下) 도메인 특화 언어와 단위 테스트 &#8211; 3부(上) 도메인 특화 언어와 단위 테스트 &#8211; 3부(下) 도메인 특화 언어와 단위 테스트 ... more