본문 바로가기

.Net Technology/.NET TDD

(8) NUnit 시작하기 - NUnit의 실전 활용 1부

지금 살펴본 NUnit을 사용해보는 것을 목적으로 TDD 기반의 애플리케이션을 만들어 보도록 하겠다. 먼저 만들고자 하는 프로그램은 아래와 같다.
 
연봉에서 현재 나라에 내야 하는 Tax를 뺀 연봉의 실 수령액을 계산해서 반환해주는 프로그램을 만드시오. 단, Tax 계산 년도가 2013년일때는 10%를 2014년일때는 20%를 적용하시오.
 
위의 문제는 간단하게 연봉을 전달해주면 거기서 Tax를 제외한 실수령 액을 계산해주면 되는 것이다. 처음 1장에서 설명했던 TDD의 개발 프로세스를 생각해보자. 먼저 우리는 프로그램 골격을 먼저 만든 후에 테스트 코드를 작성한다. 이 때 우리는 모든 가능한 테스트 요소들을 생각해서 각 케이스 별로 테스트를 만들어야 한다. 그렇게 테스트 코드가 작성되었으면 우리는 실전 코드를 작성하면 되는 것이다. 먼저 프로그램 골격부터 완성하도록 하자. 
 
프로그램 설계하기
 
먼저 필자는 클래스 라이브러리 프로젝트를 선택해서 실행하도록 하겠다. 프로젝트 이름은 TaxCalculator로 지정하였으며 아래와 같은 클래스를 생성하였다. 추가적으로 년도를 지정할 수 있는 TaxYear라는 enum을 추가하였다.
 
public enum TaxYear { 
    Year2013,
    Year2014
}

public class TaxCalculator
{
    private readonly TaxYear _taxYear;

    public TaxCalculator(TaxYear taxYear)
    {
        this._taxYear = taxYear;
    }
    public decimal Calculate(decimal salary)
    {
        //Todo – 2013 = 10%, 2014 = 20%
        return -1;
    }
}
 
Calculate라는 메서드에서 우리는 연봉을 넘겨 줄 경우에 객체를 인스턴스화 시키면서 할당한 년도의 세율을 공제한 연봉을 넘겨주면 된다. 
 
팁: readonly 와 const의 차이
readonly와 const의 큰 차이는 언제 값이 초기화 되느냐이다. 위의 코드로 예측할 수 있듯이 readonly의 값은 객체가 생성될 때 값이 할당되지만 const의 경우 객체의 초기화와 상관없이 프로그램이 실행될 때 초기화 된다. 때문에 const는 프로그램 전체에 미치는 값을 할당할 때 쓰는 것이 맞지만 readonly는 객체에 따라서 값이 달라질 수 있는 경우에 사용하는 것이 바람직하다.
 
 
테스트 코드 작성하기
 
위의 프로그램에서는 실제 핵심 로직을 제외한 프로그램의 구조들을 작성하였다. 이제 Calculate 메서드에서 어떤 요소들을 테스트 해야 하는지 예상해서 작성해야 한다. 한번 각자 무엇을 테스트할지 한번 생각해보도록 하자. 필자라면 아래와 같은 테스트 요소들을 생각해 볼 것이다.
 
- 2013년도 연봉 1000을 넣었을 때 900을 반환
- 2014년도 연봉 1000을 넣었을 때 800을 반환
 
여기까지는 누구나 생각할 수 있는 요소일 것이다. 하지만 개발자는 모든 예외 상황들까지 테스트 해야 되기 때문에 보다 정교한 코드를 작성해야 한다. 예를 들면 이런것이다. 만약 연봉이 없어서 0을 넣었을 경우에 어떤 년도이든 예외없이 0을 반환해야 한다는 것이다. 왜냐하면 숫자를 0으로 나누거나 할 경우에 당연히 에러가 발생하기 때문에 넣는 테스트이기도 하다. 아래에 하나의 요소를 더 추가 하였다.
 
- 2013년도 연봉 1000을 넣었을 때 900을 반환
- 2014년도 연봉 1000을 넣었을 때 800을 반환
- 연봉이 0일 경우에 어떤 년도에 상관없이 0을 반환
 
 
그럼 이렇게 세 가지 요소들을 테스트하는 세 개의 테스트를 만들어 보도록 하겠다. 필자는 아래오 같이 먼저 두 개의 테스트를 작성해 보았다.
 
[TestFixture]
public class TaxCalculatorTest
{
    [Test]
    public void When2013_ShouldReturn90Percent()
    {
        //Arrange
        var taxHelper = new TaxHelper(TaxYear.Year2013);
        const int salaryExpected = 900;

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

        //Assert
        Assert.That(salaryResulted,Is.EqualTo(salaryExpected));
    }

    [Test]
    public void When2014_ShouldReturn80Percent()
    {
        //Arrange
        var taxHelper = new TaxHelper(TaxYear.Year2014);
        const int salaryExpected = 800;

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

        //Assert
        Assert.That(salaryResulted, Is.EqualTo(salaryExpected));
    }
}
 
위의 테스트에서 최대한 간단하게 작성하기 위해서 필자는 상수들을 변수로 사용하지 않고 바로 대입해서 이용했다. 하지만 좋은 습관이 아니다. 관리하기 좋은 코드는 어떤 값이든 모든 변수로 할당해서 이용해야 된다는 것을 기억해두면 좋다. 먼저 클래스의 기능을 수행한 뒤에 결과 값을 저장한다. 그 뒤에 Assert 구문을 통해서 테스트 결과를 전달하게 된다. 
 
팁: 테스트 메서드의 네이밍 규칙과 AAA패턴
 
테스트의 네이밍 규칙은 다양하게 사용되지만 필자가 사용하는 방식은 “_” 언더바를 이용해서 가정과 결과를 구분해서 나타내는 방법을 사용하고 있다. 예를 들면 이런 것이다. 
 
- 2014년도의 연봉일 경우 _ 80%의 연봉을 반환해야한다.
 
물론, 이름을 정한다라는 것은 가독성과 유지 보수에 목적을 두기 때문에 자신의 팀이 혹은 자신이 가장 잘 이해할 수 있는 구문으로 작성하는 것이 좋다. 추가로 테스트 코드를 작성하는데 있어서 가장 많이 사용패턴인 AAA 패턴을 이용했다. 각각의 역할들은 아래와 같다. 
 
Arrange(정리) – 테스트에 필요한 모든 변수들과 객체들을 셋업한다. 
Act(행동) – 실제 테스트 코드를 실행한다.
Assert(주장) – 여기서는 실제 테스트 코드가 통과 되는 규칙을 작성하면된다.
 
그럼 이제 연봉이 0일 경우에 수행하는 테스트를 작성해보도록 하자.
[TestFixture]
public class TaxCalculatorTest
{
    [TestCase(TaxYear.Year2013)]
    [TestCase(TaxYear.Year2014)]
    public void WhenSalaryIsZero_ShouldReturnZero(TaxYear taxYear)
    {
        //Arrange
        var taxHelper = new TaxHelper(taxYear);
        const int salaryExpected = 0;

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

        //Assert
        Assert.That(salaryResulted, Is.EqualTo(salaryExpected));
    }
}
 
여기서 테스트 요소가 2013년도이든 2014년도이든 상관없이 모두 0을 반환해야 한다. 그럼 두 개의 테스트를 작성할 수도 있겠지만 앞에서 살펴본 바로 TestCase를 이용해서 코드를 보다 간결하게 작성했다. 
이렇게 모든 테스트 코드의 작성이 끝나면 아래와 같이 모두 빨간색의 테스트 결과를 반환하는 것을 볼 수 있다. 
 
[그림16] 테스트 결과
이제 실제 코드를 작성하면서 위의 빨간색의 테스트를 녹색으로 만들어 보도록 하자. 
 
실제 코드 작성하기
TaxYear에 따라서 다른 세금비율을 적용하여 반환해주면 된다. 필자는 아래와 같은 로직을 작성하였다.
 
public class TaxHelper
{
    private readonly TaxYear _taxYear;

    public TaxHelper(TaxYear taxYear)
    {
        this._taxYear = taxYear;
    }

    public decimal Calculate(decimal salary)
    {
        int taxRate = 0;
        switch (_taxYear)
        {
            case TaxYear.Year2013:
                taxRate = 10;
                break;
            case TaxYear.Year2014:
                taxRate = 20;
                break;
            default:
                throw new ArgumentException();
        }
        return salary * (100- taxRate) / 100;
    }
}
 
위와 같은 로직을 작성했으면 다시 테스트를 실행해보자. 그럼 아래와 같이 네 개의 테스트가 모두 통과 되는 것을 볼 수 있을 것이다.
 
[그림17] 통과된 테스트