본문 바로가기

.Net Technology/.NET TDD

(9) NUnit 시작하기 - NUnit의 실전 활용 2부

다른 객체와의 결합
 
지금까지의 예제는 NUnit을 사용해볼 정도의 아주 간단한 상황을 가정하였다. 우리는 지금의 지식으로는 TDD를 실무에 적용하기에 무리가 있다. 먼저 통합테스트가 아닌 유닛 테스트를 위해서 “컨트롤의 반전”이라는 IOC(Inversion of Control)의 개념을 이해하고 도입해야 하기 때문이다. 이 개념을 이해하기 위해서 왜 TDD에서 이 지식이 필수로 필요한지에 대해서 앞의 예제를 조금 수정함으로 알아보도록 하자. 
 
먼저 앞에서 살펴본 세금예제에서 아래와 같은 수정사항이 있다고 가정하자. 
 
- 세금 비율을 DB에 저장해서 연도 별로 가져오도록 수정해주세요.
 
충분히 이해할 수 있는 요청 사항일 것이다. 먼저 DB와 연동하기 위해서 가장 대중적인 패턴인 Repository를 생성해 보도록 하겠다. 다시 말해서 TaxRepository라는 클래스를 만들고 실제 DB 연결은 이 클래스에서 기능에 필요한 메서드들을 만들고 여기서 직접 연결하게 될 것이다. 물론, 별도의 클래스를 생성하지 않고 앞에서 만든 Calculate에서 연결할 수 있겠지만 이것은 좋은 객체지향의 패턴이 아닐 뿐더러 테스트 주도 프로그래밍을 하는데 있어서 하나의 유닛을 메서드로 구분하는 패턴을 벗어나게 된다. 이러한 객체지향 패턴들은 다음 장에서 보다 자세히 다룰 예정이다.
 
public class TaxRepository
{
    public int GetTaxRate(TaxYear taxYear)
    {
        //ToDo
        return -1;
    }
}
 
아직 메서드를 구현하지 않았다. 당연히 클래스 구조를 먼저 만들고 테스트 코드를 수정해야 하겠지만 기존에 구현해 놓은 TaxHelper클래스와 Calculate 메서드가 있기 때문에 기존의 로직을 바로 수정하도록 하겠다. 
public class TaxHelper
{
    private readonly TaxYear _taxYear;
    private readonly TaxRepository _taxRepo;
    public TaxHelper(TaxYear taxYear,TaxRepository taxRepo)
    {
        this._taxYear = taxYear;
        this._taxRepo = taxRepo;
    }

    public decimal Calculate(decimal salary)
    {
        int taxRate = _taxRepo.GetTaxRate(_taxYear);
        return salary * (100 - taxRate) / 100;
    }
}
 
위의 코드에서 TaxRepository라는 클래스를 추가 했다. 그리고 Caculate 메서드에서는 간단하게 switch 구문 대신 리파지토리에서 가져오는 데이터를 바인딩 했다. 
 
팁: 리파지토리 패턴
리파지토리는 비지니스 로직과 별도로 구별하여 리파지토리라는 저장소를 통해서 엔티티 모델을 매핑함으로 데이터의 조회와 조작등을 지원하게 된다. 보통 데이터 액세스 레이어와 리파지토리와의 큰 차이는 리파지토리는 저장소가 DB든 웹 서비스이든 원하는대로 결합하여 지원한다는 것이다.
 
이제 테스트 코드를 수정해 보도록 하겠다. 먼저 아래 작성된 잘못된 테스트 코드의 예를 살펴보도록 하자. 
[Test]
public void When2013_ShouldReturn90Percent()
{
    //Arrange
    var taxRepo = new TaxRepository();
    var taxHelper = new TaxHelper(TaxYear.Year2013, taxRepo);
    const int salaryExpected = 900;

    //Act
    var salaryResulted = taxHelper.Calculate(1000);

    //Assert
    Assert.That(salaryResulted,Is.EqualTo(salaryExpected));
}
 
먼저 왜 위의 테스트 코드가 잘못 되었는지 알 수 있다면 기본적으로 유닛 테스트의 유닛에 대한 개념을 잘 정립한 독자일 것이다. 앞에서 설명했듯이 유닛 테스트는 단 하나의 에러 가능성을 가지고 있어야 한다. 하지만이 테스트에서는 TaxRepository의 메서드를 이용하고 있듯이 2개의 에러 가능성을 가지고 있다. 쉽게 이해하기 위해서 다음 [그림18]을 참고해 보도록 하자. 
 
[그림18] 모호한 버그 가능성
 
앞의 [그림18] 처럼 TaxRepository의 GetTaxRate라는 메서드 또한 에러가 날 수 있는 가능성이 있을 뿐더러 지금 테스트 하고 있는 Calculate에서도 에러가 날 수 있는 확률이 있는 것이다. 지금은 에러가 없다고 하더라도 향후에 업데이트에서 에러가 날 경우에 우리는 어느 객체의 에러인지 명확하게 구분할 수 없기 때문에 유닛 테스트의 원칙을 위반하게 된다.
 
여기서 우리가 원하는 것은 TaxRepository의 결과와는 상관없이 TaxHelper의 Calculate 함수만 테스트 하는 것이다. 바로 이러한 요소 때문에 우리는 객체간의 의존성을 없애야 하는 개념이 등장하게 된다. 테스트 뿐만 아니라 보다 튼튼한 객체지향 코드를 위해서 항상 등장하는 개념이다. 이미 작성한 소스코드를 테스트에 맞추어 다시 수정하는 작업을 우리는 리팩토링(Refactoring)이라고 하는데 이 작업에서는 절대 기존에 운영되던 시스템을 망가트려서는 안된다는 원칙이 있다. 물론, 처음부터 충분히 객체지향적인 코드를 생성한다면 리팩토링을 하는 일은 없을 것이다. 보다 객체지향적인 코드를 작성하는 기술에 대해서는 다음 장에서 다룰 예정이다. 
 
객체간의 의존성을 없애기 위해서 우리는 스텁(Stub)이라는 기법을 이용해야 한다. 간단하게 설명해서 테스트를 위해서 스텁 리파지토리를 별도로 생성하는 것이다. 이 리파지토리는 훼이크 리파지토리로 불리기도 한다. 이 작업을위해서 우리는 바로 앞에서 설명한 리팩토링이 필요하다. 우선적으로객체간의 의존성을 없애기 위해서 인터페이스를 도입해야 한다. 다음 [그림19]를 살펴보자. 
 
[그림19] ITaxRepository 인터페이스 역할
 
TaxHelper는 실제 어떤 객체가 주입되어지냐와 상관없이 ITaxRepository에서 정의된 기능만 호출하게 되므로 우리는 실제 기능 실행에 어떤 영향도 주지 않고 처리하는 것이 가능하다. 먼저 아래와 같이 인터페이스를 도입해 보도록 하자.
public interface ITaxRepository
{
    int GetTaxRate(TaxYear taxYear);
}

public class TaxRepository : ITaxRepository
{
    public int GetTaxRate(TaxYear taxYear)
    {
        //ToDo
        return -1;
    }
}
인터페이스를 도입했으면 이제 TaxHelper의 클래스도 다음과 같이 수정해 보도록 하자.
public class TaxHelper
{
    private readonly TaxYear _taxYear;
    private readonly ITaxRepository _taxRepo;
    public TaxHelper(TaxYear taxYear, ITaxRepository taxRepo)
    {
        this._taxYear = taxYear;
        this._taxRepo = taxRepo;
    }

    public decimal Calculate(decimal salary)
    {
        int taxRate = _taxRepo.GetTaxRate(_taxYear);
        return salary * (100 - taxRate) / 100;
    }
}
 
이제 보다 객체 지향적인 코드 설계로 리팩토링을 완료했다. 이제 스텁 인터페이스를 추가하고 실제 TaxRepository가 아닌 StubRepository를 전달 하면서 실제 코드에서 순수 Calculate 유닛만 테스트가 진행되도록 코드를 변경하도록 해보겠다.
public class StubTaxRepository : ITaxRepository
{
    public int TaxRate { get; set; }

    public int GetTaxRate(TaxYear taxYear)
    {
        return TaxRate;
    }
}

[TestFixture]
public class TaxCalculatorTest
{
    [Test]
    public void When2013_ShouldReturn90Percent()
    {
        //Arrange
        var taxRepo = new StubTaxRepository();
        taxRepo.TaxRate = 10;
        var taxHelper = new TaxHelper(TaxYear.Year2013, taxRepo);
        const int salaryExpected = 900;

        //Act
        var salaryResulted = taxHelper.Calculate(1000);

        //Assert
        Assert.That(salaryResulted, Is.EqualTo(salaryExpected));
    }
}
 
이렇게 진행할 경우에 우리는 실제로 TaxRepository의 기능은 구현하지 않았음에도 불구 하고 테스트는 성공이 된다. 실제 TaxRepository는 다른 유닛으로 분류되기 때문에 다른 TestFixture를 만들어 테스트 하는 것이 바람직하다. 
 
그렇다면 모든 테스트가 이렇게 추가적인 스텁 클래스를 작성하는 수고를 수반해야 하는 것일까? 여기에서의 예제는 이해를 쉽게 하기 위해서 일일히 클래스를 작성하였다. 하지만 우리는 이런 훼이크 인터페이스들을 자동으로 생성하는 모킹(Mocking) 프레임워크들을 이용하여 테스트 작업을 훨씬 쉽게 이용할 수 있으며 뒷 장에서 이 기법들에 대해서 보다 자세히 살펴보도록 하겠다.
 
 
팁: 추상 클래스의 활용
 
앞의 예제에서 우리는 스텁의 개념에 대해서 살펴봤다. 즉, 페이크 리파지토리를 생성하기 위해서 우리는 인터페이스를 이용해서 설계했다. 하지만 매번 인터페이스를 이용해서 코드를 작성해야 하는 것은 아니다. 만약 우리가 각 기능을 virtual 메서드로 지정하여 상속과 함께 활용해도 우리는 어느 정도 페이크 리파지토리를 생성하는 것이 가능하다. 
예를 들어 만약 인터페이스가 이렇게 단순한 것이 아니라 기능이 매우 복잡하다면 어떨까? 페이크 리파지토리를 만들때 인터페이스들의 복잡한 멤버들과 함수들을 모두 구현해주어야 할 것이다. 정작 테스트에서 필요한 메서드가 하나라고 하더라도 말이다. 이런 번거로움과 비효율을 막기 위해서 우리는 아래와 같이 클래스를 변경할 수 있다.
public class TaxRepository
{
    public virtual int GetTaxRate(TaxYear taxYear)
    {
        //ToDo
        return -1;
    }
}
public class StubTaxRepositoryAbstract : TaxRepository
{
    public int TaxRate { get; set; }
    public override int GetTaxRate(TaxYear taxYear)
    {
        return TaxRate;
    }
}
 
여기서 우리는 TaxRepository를 상속 받은 후에 우리가 필요한 GetTaxRate만 재정의 함으로써 실제의 로직을 없애는 것이 가능하다.
 
 
정리
 
이번 장에서는 유닛테스트의 정의에 대해서 살펴봤다. 유닛테스트의 원칙은 단, 하나의 유닛만 테스트 범위에 포함시켜야 한다. 만약 여러 개의 중복 테스트나 레거시 코드를 작성해야만 하는 코드들은 유닛테스트가 아닌 통합테스트로 작성되어야 한다. 
 
추가 NUnit 프레임워크에 대해서 살펴봤다. 세계적으로 가장 많이 이용되고 있는 테스트 프레임워크이며 비주얼 스튜디오와의 통합을 제공하고 있다. 우리는 테스트를 위해서 최소한의 애트리뷰트들을 알고 있어야 한다. 그리고 테스트 결과를 진단하기 위해서 Assert 클래스에서 제공되는 기능 또한 알고 있어야 테스트 코드의 작성이 가능하다. 하지만 이런 기능은 크게 복잡하지 않다.
 
마지막으로 우리는 스텁(Stub) 객체가 왜 필요한지에 대해서 살펴봤다. 유닛 테스트의 원칙을 지키기 위해서 우리는 의존성을 없애야만 하고 실제 코드들은 의존성들을 고려하여 보다 객체지향적으로 설계되어야 한다.