.Net Technology/.NET TDD2014.10.19 18:11
지금까지 살펴본 내용을 실제 예제를 통해서 실전 감각을 익혀보도록 하겠다. 먼저 아래와 같은 프로그램을 설계하고 개발해야 된다고 가정해보자. 
 
편의점 카운터에서 물건을 스캔한 뒤에 고객에게 최종 가격을 알려주는 프로그램이 필요합니다. 물건들의 가격은 데이터 베이스에 있으며, 한가지 특별한 것은 현재 모든 물건을 2개 사면 하나 더 주는 이벤트를 하고 있기 때문에 이 로직을 반영해주세요.
 
위의 요청 사항을 보다 간단히 생각하고 구현해 보도록 하겠다. 우리는 하나의 메서드를 제공해 줄 것이다. 즉, 실행 프로그램은 List<string>배열로 이루어진 각 물건 코드들의 리스트를 전달하면 각 물건의 가격들을 합산한 가격을 알 수 있으면 되는 것이다. 단, 같은 코드가 3개가 되면 하나의 물건 가격은 제외되어야 한다.
 
먼저 필자의 코드를 바로 확인하기 전에 자신이 직접 위의 문제를 생각해보고 직접 코드를 작성해보고 코드를 비교하는 것을 추천하고 싶다. 그럼 먼저 TDD를 고려하지 않은 전혀 객체지향적이지 않은 코드를 먼저 보여주고 한 스텝씩 그 코드를 고쳐 나가 보도록 하겠다.
 
먼저 실제 물건의 가격을 가져오는 역할을 담당하는 PriceRepository를 아래와 구현할 것이다. 
 
public class PriceRepository
{
    public decimal GetPrice(string code)
    {
        //From Database
        return 40.0m;
    }
}
 
이제 이 리파지터리를 이용해서 실제 로직을 구현하면 되는데 CheckOutManager라는 클래스를 만들어서 ScanAll 이라는 기능을 통해서 가격을 합산할 것이고 Total 이라는 프로퍼티를 만들어 합산된 가격을 반환할 수 있게 만들어 볼 것이다.
 
public class CheckoutManager
{
    private decimal _total;
    public decimal Total {
        get { return _total; }
        set { _total = value; }
    }

    public void ScanAll(List<string> arrItems)
    {
    }
}
 
구조가 이렇게 만들어졌으면 이제 ScanAll 이라는 메서드를 아래와 같이 한번 구현해 보도록 하겠다. 
 
public void ScanAll(List<string> arrItems)
{
    var priceRepo =new PriceRepository();
    var dicItemCount =new Dictionary<string, int>();
    foreach (var code in arrItems)
    {
        //가격 계산
        decimal price = priceRepo.GetPrice(code);
        _total += price;
        //2개 이상 살 경우 1개 무료 확인
        if (!dicItemCount.ContainsKey(code))
        {
            dicItemCount.Add(code,1);
        }
        //3번째는 무료 아이템
        if (dicItemCount[code] == 3)
        {
            dicItemCount[code] = 0;
            _total -= price;
        }
    }
}
 
여기서 먼저 PriceRepository를 생성하였다. 그리고 dicItemCount 라는 컬렉션을 생성하여 실제 같은 아이템을 몇개 구입하는지 저장하여 만약 3개가 되면 그 가격을 할인 해주고 다시 카운트를 0으로 리셋시키는 코드를 아래에 구현하였다. 
 
만약 필자가 객체지향에 대한 개념과 필요성에 대해서 큰 인지가 없었다면 이런 코드가 충분히 생산되었을 것이다. 이 코드는 앞에서 살펴본 다섯가지 규칙들에서 세 가지를 벗어나고 있다. 그럼 하나씩 살펴보면서 수정해 보도록 하자. 
 
 
하나의 기능으로 구성하기
 
먼저 얼핏 보기에는 PriceRepository를 별도로 생성하여 지원했기 때문에 각 기능별로 나눈 것처럼 보일 수 있다. 하지만 ScanAll 메서드에서 우리는 두 개의 기능을 동시에 사용하고 있다. TDD로 예를 들자면 우리는 두 가지 기능을 한 메서드에서 테스트 해야한다는 것이다.
 
하나는 값이 잘 저장되는지를 테스트 해야 하는 것이고 다른 하나의 요소는 실제 가격을 할인해주는 로직을 테스트 해야 하는 것이다. 뿐만 아니라 향후에 다른 할인 로직이 추가될 가능성이 다분히 높다. 만약 3개를 사면 1개를 공짜로 주는 로직으로 변경된다고 할 경우에 이 로직이 분리되지 않는다면 다른 기능에 영향을 줄 수도 있을 뿐더러 향후 문제가 있을 경우에 어디서 문제가 발생했는지 정확하게 말해주는 것이 어렵다. 
 
위의 문제를 해결하기 위해서 DiscountManager를 만들어서 위의 기능을 분리해야 한다. 필자는 아래와 같이 클래스와 메서드를 구현했다. 
 
public class DiscountManager
{
    public Dictionary<string, int> dicItemCount = new Dictionary<string, int>();
    public decimal GetDiscount(string code, decimal price)
    {
        if (!dicItemCount.ContainsKey(code))
        {
            dicItemCount.Add(code, 1);
        }
        //3번째는 무료 아이템
        if (dicItemCount[code] == 3)
        {
            dicItemCount[code] = 0;
            return price;
        }
        return 0;
    }
}
 
GetDiscount 로직은 이미 ScanAll 메서드에서 구현한 내용을 그대로 옮겼을 뿐이다. 다만 dicItemCount라는 전역 변수로 구분한 부분이 다를 수 있다. 이 메서드에서는 할인되는 가격을 반환하게 된다. 때문에 아무 할인이 없는 경우에는 0을 반환하게 된다. 그럼 ScanAll 함수를 변경해 보도록 하자.
 
public void ScanAll(List<string> arrItems)
{
    var priceRepo =new PriceRepository();
    var disManager = new DiscountManager();
    foreach (var code in arrItems)
    {
        //구매
        decimal price = priceRepo.GetPrice(code);
        _total += price;
        //할인 적용
        _total += disManager.GetDiscount(code, price);

    }
}
 
코드가 보다 간결해졌다. 이유는 기능을 두가지로 구분해서 사용하고 있기 때문이다. 하나는 PriceRepository에서 값을 가져오고 할인은 모두 DiscountManager에서 진행하고 있기 때문이다. 하지만 여전히 개선할 사항들이 많이 있다. 그럼 다음 원칙으로 넘어 가도록 하자. 
 
<팁: 시작 – double과 decimal의 차이 >
기본 프로그래밍의 경험이 있는 사람은 돈을 관리할때 당연히 decimal 타입을 이용해야 한다는 것 정도는 알 것이다. 그렇다면 왜 double이 아니라 decimal일까? 그 이유는 double은 최대 길이를 가지고 있기 때문이다. 예를 들면 은행에서 굉장히 큰 이율을 계산할때 정말 작은 소수점자리의 값을 저장해야 되는 경우가 있는데 double은 그런 부분에서 약간의 오차가 생기기 마련이다. 하지만 decimal은 문자열 기반으로 작성되기 때문에 자리수와 상관없이 값을 보다 정교하게 저장하고 불러오는 것이 가능하다.
<팁: 끝>
 
 
인터페이스의 분리와 의존성 제거
 
먼저 각각의 클래스들을 인터페이스를 이용해서 기능을 분리하는 작업을 진행할 필요가 있다. 이유는 기능별로 필요한 기능만 할당해 주는 것일 수도 있지만 여기서는 의존성을 제거해주기 위한 목적이 더 크다. 필자는 아래와 같은 인터페이스들을 추가하였다. 
 
public interface IPriceRepository
{
    decimal GetPrice(string code);
}
public interface IDiscountManager
{
    decimal GetDiscount(string code, decimal price);
}
 
그리고 각각의 클래스들은 위의 인터페이스를 상속 받았다. 
 
public class DiscountManager : IDiscountManager
{
}
public class PriceRepository:IPriceRepository
{
}
 
이제 마지막으로 ScanAll 메서드에서 참조하는 로직을 수정해야 한다. 먼저 기존의 코드에서는 다음과 같이 메서드 안에서 객체를 초기화 하였다.
 
public void ScanAll(List<string> arrItems)
{
    var priceRepo = new PriceRepository();
    var disManager = new DiscountManager();
}
 
우리가 인터페이스를 사용한다고 하더라도 실제로 객체초기화를 내부 로직에서 하게 된다면 여전히 객체들의 의존성은 존재하게 된다. 때문에 우리는 이 부분을 전역변수로 선언함과 동시에 생성자를 이용해서 해결해 보도록 하겠다. 필자는 아래와 같이 다음 함수들을 전역 변수로 선언하였고 이 변수들을 생성자를 통해서 초기화 하였다.
 
public IPriceRepository priceRepo;
public IDiscountManager disManager;

public CheckoutManager(IPriceRepository priceRepo,IDiscountManager disManager )
{
    this.priceRepo = priceRepo;
    this.disManager = disManager;
}
 
이렇게 하면 우선적으로 솔리드 원칙들에 대해서는 어느정도 도입이 끝나게 된다. 최종 코드는 아래와 같다. 
 
public interface IPriceRepository
{
    decimal GetPrice(string code);
}

public interface IDiscountManager
{
    decimal GetDiscount(string code, decimal price);
}

public class DiscountManager : IDiscountManager
{
    public Dictionary<string, int> dicItemCount = new Dictionary<string, int>();

    public decimal GetDiscount(string code, decimal price)
    {
        if (!dicItemCount.ContainsKey(code))
        {
            dicItemCount.Add(code, 1);
        }
        //3번째는 무료 아이템
        if (dicItemCount[code] == 3)
        {
            dicItemCount[code] = 0;
            return price;
        }
        return 0;
    }
}

public class PriceRepository : IPriceRepository
{
    public decimal GetPrice(string code)
    {
        //From Database
        return 40.0m;
    }
}

public class CheckoutManager
{
    public IPriceRepository priceRepo;
    public IDiscountManager disManager;

    public CheckoutManager(IPriceRepository priceRepo, IDiscountManager disManager)
    {
        this.priceRepo = priceRepo;
        this.disManager = disManager;
    }

    private decimal _total;

    public decimal Total
    {
        get { return _total; }
        set { _total = value; }
    }

    public void ScanAll(List<string> arrItems)
    {
        foreach (var code in arrItems)
        {
            //구매
            decimal price = priceRepo.GetPrice(code);
            _total += price;
            //할인 적용
            _total += disManager.GetDiscount(code, price);

        }
    }
}
 
위의 코드를 살펴보면 아직도 부족한 부분이 많이 있다. 즉, 솔리드 컨셉에서 다듬어 줄 수 없는 것들을 보안해 보도록 하자.
 
접근 제한자의 설정
 
public private와 같은 접근제한자들은 거의 프로그래밍 처음 시작 단계에서 배우는 개념이다. 하지만 실제 프로그래밍에서 그 중요성이 많이 느끼지 못해서 그런지 허술하게 적용하게 되는 경우가 많다. 실제로 모든 변수들을 public으로 설정해도 큰 문제가 없이 프로그램을 개발할 수 있기 때문이다. 하지만 프로그램에서 접근제한자는 복잡한 컴포넌트를 만들거나 로직을 만들 때 그 복잡성을 줄여주는데 큰 역할을 한다. 즉, 큰 규모에서의 소프트웨어를 만든다고 가정할 때 각 팀별로 만든 로직을 이용하는데 있어서 객체 내부적으로만 수정되어야 하는 값을 노출시켰고 그 변수를 접근하게 된다면 전혀 의도하지 않은 의존성이 만들어 지게 되는 것이다. 
 
접근 제한자는 모든 객체지향 프로그램의 기본이라고 강조해도 지나치지 않는다. 그럼 먼저 ChckoutManager에서 잘못 정의된 접근제한자를 다시 설정해 보도록 하자. 
public class CheckoutManager
{
    private IPriceRepository priceRepo;
    private IDiscountManager disManager;
}
 
DiscountManager 클래스에서 역시 수정해 보도록 하자.
public class DiscountManager : IDiscountManager
{
    private Dictionary<string, int> dicItemCount = new Dictionary<string, int>();
}
 
 
readonly의 정의
 
우리는 객체가 단일 성격의 역할을 하게 될 경우를 위해서 readonly 설정을 도입할 필요가 있다. 혹시나 다른 로직에 의해서 변경될 수 있는 가능성을 없애는 것이다. 먼저 CheckoutManager에서 생성자에 의해서 초기화 되는 IPriceRepository와 IDiscountManager는 한번 대입된 뒤에 다시 변경되어지면 안된다. 만약 변경되어 사용하려면 다른 객체를 생성하는 것이 객체지향 성격을 유지할 수 있다. 필자는 아래와 같이 정의를 다시 변경하였다. 
 
public class CheckoutManager
{
    private readonly IPriceRepository priceRepo;
    private readonly IDiscountManager disManager;
}
 
추가적으로 우리는 Total 이라는 프로퍼티도 외부에서 변경이 되지 않게 막아줘야 할 필요가 있다. 때문에 설정했던 setter 기능을 제거해 보겠다. 
 
public decimal Total
{
    get { return _total; }
}
 
이렇게 하면 코드들이 어느정도 자리가 잡히게 된다.
 
네이밍 규칙의 도입
 
네이밍 규칙에 대해서는 개인마다 또한 회사마다 원하는 표준을 정의하여 진행하게 되는 경우가 많다. 필자도 그 규칙들에 따라서 스타일을 많이 변경했지만 필자가 가장 선호하는 것은 닷넷 프레임워크가 사용하는 네이밍 규칙을 따라가는 것이다. 이유는 닷넷 개발자들이 가장 많이 사용하는 프레임워크이기 때문에 그만큼 익숙한 패턴이기 때문이다. 네이밍의 이유는 가독성과 생산성을 목표로 한다. 누구나 쉽게 이해해야 하는 것이다.
 
먼저 아래 [표-1]은 실제 MS의 개발팀에서 활동하고 있는 페페 브라운(Pepe Brown)이 밝히고 있는 내부 네이밍 규칙을 정리한 것이다. 
 
 
타입코딩 규칙적용예
클래스,
구조체,
컬렉션
언더바로 시작하는 것이 아닌 항상 대문자로 시작하며 두 개 이상의 단어가 결합될 경우에 각 단어들은 항상 대문자로 시작하는 파스칼 케이스를 이용한다. 단, I로 시작할 경우에 인터페이스 규칙과 혼란을 주기 때문에 이 경우에만 소문자 i 로 시작한다. 또한 주의할 것은 절대 약어를 이용하지 않는다.InstanceManager
XmlDocument
MainForm
델리게이트 클래스클래스와 같은 규칙을 이용하지만 단 뒤에 Delegate라는 단어를 추가한다.WidgetCallbackDelegate
예외 클래스클래스와 같은 규칙을 이용하지만 단, 뒤에 Exception이라는 단어를 추가한다.InvalidTransactionException
애트리뷰트 클래스클래스와 같은 규칙을 이용하지만 단, 뒤에 Attribute라는 단어를 추가한다.WebServiceAttribute
인터페이스클래스와 같은 규칙을 이용하지만 항상 I라는 알파벳을 단어 앞에 추가한다.IWidget
열거형(enum)클래스와 같은 규칙을 이용한다.
.
SearchOptions
AcceptRejectRule
메서드클래스와 같은 규칙을 이용한다. 단, 주의할 것은 많은 개발자들이 메서드를 약어를 이용해서 줄이는 경우가 많은데 메서드 또한 약어를 이용해서는 안된다.public void DoSomething()
프로퍼티,
public 변수
역시 클래스와 같은 파스칼 케이스를 이용한다.public int ItemCode
메서드의 파라메터첫 글자만 소문자로 쓰는 카말 케이스를 이용한다. 역시 이 타입도 약어로 줄여서는 안된다.string itemCode
메서드 안에 선언된 지역 변수메서드처럼 카말 케이스를 이용하지만 줄여 표현하는 것이 가능하다.string strName;
클래스 안에서 정의된 private 나 protected 전역변수이 경우에 실제 마이크로소프트 팀에서도 사용 표준이 조금 다르다. 카말 클래스를 이용하지만 이름 앞에 언더바(_)를 붙이는 경우도 있으며 m_ 을 붙이는 경우도 있다. 둘다 썩 명쾌한 방법은 아니지만 필자는 _ 언더바를 이용하는 것을 더 선호한다.private int _itemCode;
 
[표1] 마이크로소프트의 네이밍 기준
 
 
이제 위와 같은 표준을 적용하여 다시 코드를 적용해 보도록 하겠다. 아래 코드는 네이밍 규칙까지 적용한 최종적인 코드를 보여주고 있다. 
 
public interface IPriceRepository
{
    decimal GetPrice(string code);
}

public interface IDiscountManager
{
    decimal GetDiscount(string code, decimal price);
}

public class DiscountManager : IDiscountManager
{
    private Dictionary<string, int> _itemCountList = new Dictionary<string, int>();

    public decimal GetDiscount(string code, decimal price)
    {
        if (!_itemCountList.ContainsKey(code))
        {
            _itemCountList.Add(code, 1);
        }
        //3번째는 무료 아이템
        if (_itemCountList[code] == 3)
        {
            _itemCountList[code] = 0;
            return price;
        }
        return 0;
    }
}

public class PriceRepository : IPriceRepository
{
    public decimal GetPrice(string code)
    {
        //From Database
        return 40.0m;
    }
}

public class CheckoutManager
{
    private readonly IPriceRepository _priceRepo;
    private readonly IDiscountManager _disManager;

    public CheckoutManager(IPriceRepository priceRepo, IDiscountManager disManager)
    {
        this._priceRepo = priceRepo;
        this._disManager = disManager;
    }

    private decimal _total;

    public decimal Total
    {
        get { return _total; }
    }

    public void ScanAll(List<string> items)
    {
        foreach (var code in items)
        {
            //구매
            decimal price = _priceRepo.GetPrice(code);
            _total += price;
            //할인 적용
            _total += _disManager.GetDiscount(code, price);

        }
    }
}
 
이렇게 하여 리펙토링이 모두 끝났다. TDD를 이용할 경우에 설계만 진행하고 테스트 코드를 먼저 작성한 뒤에 실제 코드를 구현하게 되지만 이번 예제에서는 객체지향적인 코들르 개발하는 것이 목적이었기 때문에 테스트 코드는 작성하지 않았다. 지금은 이 코드가 잘 익숙하지 않다고 하더라도 테스트 코드를 적용하다 보면 자연스럽게 이러한 객체지향 개념을 도입하게 될 것이다. 
 
 
정리
 
이번 장에서는 솔리드 디자인 원칙들에 대해서 살펴보았다. 다섯가지의 규칙들을 다시 정리하자면 먼저 객체는 단 하나의 기능을 가지고 있어야 한다. 그리고 그 객체의 기능에 있어서는 항상 확장이 가능하게 오픈되어야 하지만 수정에 의해서는 폐쇄시켜야 한다. 그리고 객체들은 항상 최소한의 기능을 가지고 있어서 파생된 자식들에 대해서 대입이 가능해야 한다. 즉, 자신이 필요없는 기능임에도 불구하고 자식 클래스들을 위해서 가지고 있어서는 안된다는 것이다. 이러한 문제는 인터페이스의 분리를 통해서 제거할 수 있다. 마지막으로 우리는 객체들끼리 가지고 있는 강한 참조를 인터페이스를 통해서 느슨한 결합으로 변경하는 것을 살펴보았다. 
 
실제 편의점의 체크아웃 예제를 통해서 솔리드 디자인 컨셉을 어느정도 적용하는 예제를 살펴보았다. 단계별로 구조를 변경해 보았으며 추가적으로 솔리드 디자인 컨셉 이외의 규칙들도 적용해 보았다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:11
마지막으로 원칙이 무엇보다도 TDD에서 가장 중요한 부분이다. 다른 원칙들이 지켜지지 않는다고 하더라도 TDD를 진행하는데 불편함이 있을 뿐이지 그렇게 어렵지는 않다. 하지만 만약 의존성 반전이라는 원칙이 지켜지지 않는다면 TDD 자체가 어려워진다. 
 
의존성 반전이라는 것은 쉽게 이야기 해서 객체간의 강한 참조를 없애는 것이다. 즉, 느슨한 결합을 인터페이스를 통해서 실현하는 것을 의미한다. 그럼 첫번째 원칙에서 살펴본 코드의 문제를 살펴보도록 하자.
 
class FileLogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}

class Customer
{
    private FileLogger obj = new FileLogger();
    public virtual void Add()
    {
        try
        {
            //Some codes
        }
        catch (Exception ex)
        {
            obj.Handle(ex.ToString());
        }
    }
}
 
 
위의 코드의 문제를 보다 쉽게 진단하기 위해서 하나의 추가사항을 설정하겠다. 만약 프로젝트에 따라서 로그를 파일에 저장하는 것이 아니라 DB에 저장한다고 가정해보자. 그럼 당연히 인터페이스를 도입하여 아래와 같이 설계를 바꿀 수 있을 것이다.
interface ILogger
{
    void Handle(string error);
}

class FileLogger :ILogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}

class DBLogger : ILogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}
 
이렇게 두개의 객체를 생성한 뒤에 해야할 일은 바로 Customer 클래스에서 이 로그 클래스들을 상황에 따라서 자유롭게 사용이 가능하도록 코드를 작성하는 것이다. 만약 Customer 클래스가 웹 환경일때에는 DB에 저장하고 윈도우 환경일 때에는 DB로 로그를 남겨야 한다면 어떻게 해야할까? 두개의 객체를 모두 가지고 있어야 할까? 그렇지 않다. 우리는 객체가 생성될 때 객체를 넘겨주는 형태로 클래스를 변경할 수 있다. 다음 코드를 살펴보자.
 
class Customer
{
    private readonly ILogger _obj;
    public Customer(ILogger obj)
    {
        _obj = obj;
    }
    public virtual void Add()
    {
        try
        {
            //Some codes
        }
        catch (Exception ex)
        {
            _obj.Handle(ex.ToString());
        }
    }
}
 
모두 인터페이스를 참조함으로써 인터페이스를 구현한 어떤 타입이 넘어 온다고 하더라도 우리는 쉽게 코드를 작성하고 이용하는 것이 가능하다. 예를 들어 아래와 같이 원하는 대로 객체를 생성할 수 있는 것이다. 
 
var customerDB = new Customer(new DBLogger());
var customerFile = new Customer(new FileLogger()));
 
<팁: 시작 – 팩토리 패턴>
 
팩토리 패턴은 의존성 제거를 위해서 뿐만 아니라 보다 적은 코드로 원하는 환경에 따른 객체를 초기화 시켜주기 위해서 이용되는 패턴이다. 앞의 예제에서 또한 팩토리 패턴을 도입할 수 있다. 먼저 다음과 같은 팩토리 클래스를 정의해보자.
 
public class LoggerFactory
{
    public static ILogger GetLogger(string environment)
    {
        if (environment == "Web")
        {
            return new DBLogger();
        }
        else
        {
            return new FileLogger();
        }
    }
}
 
실제 environment 라는 변수를 설정하였고 이 변수를 통해서 원하는 객체를 만들어 넘겨주는 개념이다. 그럼 Customer 클래스에 팩토리 패턴을 이용하는 새로운 생성자를 하나 추가해보자.
 
public class Customer
{
    private readonly ILogger _obj;

    public Customer(string environment)
    {
        _obj = LoggerFactory.GetLogger(environment);
    }
}
 
이렇게 개발할 경우에 우리는 일일히 로그 객체를 생성해서 넘겨 줘야 하는 수고를 덜 수 있다.
 
<팁: 끝>
 
이렇게 하여 다섯가지 원칙들을 모두 살펴보았다. 물론, 객체 지향에 있어서 이 다섯가지로 만족할 수 있는 것은 아니지만 적어도 이 다섯가지 원칙을 지키는 것을 목표로 한다면 충분히 높은 퀄리티의 코드를 만들어 내는데 큰 보탬이 될 것이다.
 


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:10
인터페이스를 이용해서 객체를 기능별로 잘 분리해야 한다는 원칙이다. 솔리드의 첫번째 규칙이었던 객체는 하나의 기능을 가지고 있어야 한다는 것과 의미적으로 보면 같은 맥락이 될 수 있다. 하지만 여기서의 의미는 인터페이스를 통해서 기능을 세부저긍로 분리하는 것이다. 바로 앞의 예제가 완벽하지 않은 이유는 일반 고객은 할인이 불가능함에도 불구하고 GetDiscount라는 메서드를 가지고 있기 때문이다. 필자는 이 기능을 분리하기 위해서 아래와 같이 인터페이스를 추가해보도록 하겠다. 
 
public interface IDiscount
{
    double GetDiscount(double totalSales);
}
 
이제 위의 소스를 아래와 같이 변경해보도록 하자. 
public interface IDiscount
{
    double GetDiscount(double totalSales);
}

public interface ISpecialService
{
    void SetSpecialService(string order);
}

public class Customer
{
}

public class SilverCustomer : Customer, IDiscount, ISpecialService
{
    public double GetDiscount(double totalSales)
    {
        return totalSales - 50;
    }
    public void SetSpecialService(string order)
    {
        //Save to Database
    }
}

public class GoldCustomer : Customer,IDiscount, ISpecialService
{
    public double GetDiscount(double totalSales)
    {
        return totalSales - 100;
    }
    public void SetSpecialService(string order)
    {
        //Save to Database
    }
}
 
이렇게 각자의 기능들을 인터페이스를 통하여 분리하였다. 향후에 원하는 기능을 수행하기 위해서 객체들을 손쉽게 분리할 수 있으며 또한 쓸모없는 기능을 가지고 있는 것을 방지할 수 있다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:10
리스코브 대입의 규칙은 이러하다. “프로그램에 존재하는 한 객체는 어떤 추가적인 수정없이 파생된 서브 클래스들로 대입이 가능해야 한다.” 다시 풀어 설명하자면 자식 클래스들이 부모의 기능을 수행하는데 있어서 어떤 문제가 있어서는 안된다는 것이다. 쉽게 이해하기 위해서 바로 앞에서 사용한 예제를 통해서 그 문제와 해결방안에 대해서 자세히 살펴보도록 하자. 
 
먼저 새로운 기능을 추가해보도록 하겠다. 바로 실버와 골드 고객에게만 스페셜 서비스를 주고 싶은 것이다. 즉, 스페셜 서비스를 요청하면 이 요청을 받아서 DB에 저장하는 메서드를 추가하는 것이다. 먼저 실버는 고객 클래스를 골드는 실버 클래스를 상속받고 있는 구조이기 때문에 우리는 부모인 Customer에 SetSpecialService라는 가상 메서드를 만들어 보도록 하겠다.
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
    public virtual void SetSpecialService(string order)
    {
        throw new Exception("It allows only Silver or Gold Customer.");
    }
}
 
 
그리고 Customer 클래스를 상속받은 실버와 골드 고객들은 아래와 같이 메서드를 구현하였다. 
 
public class SilverCustomer : Customer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
    public  override void SetSpecialService(string order)
    {
        //Save to Database
    }
}

public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
    public override void SetSpecialService(string order)
    {
        //Save to Database
    }
}
 
 
실제 DB로 저장한다고 가정만 할 뿐 실제로 코드는 작성하지 않았다. 자, 그럼 이 코드의 문제를 살펴보자. 바로 아래와 같은 코드를 작성할 때 문제가 발생하게 된다. 
 
List<Customer> customers = new List<Customer>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());

foreach (Customer c in customers)
{
    c.SetSpecialService("Order");
}
 
 
이렇게 코드를 실행할 경우에 우리는 세번째로 추가된 Customer 객체 즉, SetSpecialService를 호출할 권한이 없기 때문에 실제 런타임에서 에러를 발생시키고 만다. 이 것을 해결하기 위해서 어떻게 해야 할까? 아래와 같이 try catch를 넣으면 어떨까? 
 
foreach (Customer c in customers)
{
    try
    {
        c.SetSpecialService("Order");
    }
    catch(Exception ex)
    {
        //...Message ..?
    }
)
 
 
예외는 예상치 못한 상황의 에러가 발생될 때 사용하는 것이지 특정 로직을 이미 알고 있으면서 사용하는 것이 아니다. 지금까지의 예제는 설계부터가 잘못되었기 때문에 완전히 잘못된 방향으로 코드가 흘러가고 있었다. 먼저 필요없는 기능을 부모 클래스나 자식 클래스가 가지게 된 것 부터가 이 규칙을 위반하게 된다. 즉, 이 규칙은 자식들은 부모의 기능을 완전히 대체할 수 있어야 한다는 것인데 부모 클래스에 필요없는 기능을 넣어 준것이 문제가 된다. 그렇다면 이 잘못된 설계를 바로 잡아 보도록 하자. 필자는 위의 구조를 고치기 위해서 ISpecialService라는 인터페이스를 추가했다.
 
public interface ISpecialService
{
    void SetSpecialService(string order);
}
 
 
이제 이 인터페이스를 이용해서 위의 클래스들의 구조를 아래와 같이 변경해 보도록 하자. 
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
}
public class SilverCustomer : Customer, ISpecialService
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
    public virtual void SetSpecialService(string order)
    {
        //Save to Database
    }
}
public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
    public override void SetSpecialService(string order)
    {
        //Save to Database
    }
}
 
 
위와 같이 구조를 인터페이스를 이용해서 수정하였다. 이제 다음과 같은 코드를 다시 작성해보도록 하자. 
 
List<ISpecialService> customers = new List<ISpecialService>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());
 
그럼 Customer 클래스를 추가하는 부분에서 컴파일 에러를 가지게 된다. 즉, 우리는 적어도 구조를 바꿈으로써 런타임 에러를 피할 수 있다. 여기까지가 리스코브 대입에 해당하는 원칙이다. 즉, Customer를 상속받는 자식 클래스들은 충분히 Customer클래스를 대체하여 대입할 수 있다는 부분이다. 그리고 ISpecialService를 상속받은 객체들은 서로 대입이 가능하다.
 
 
팁: 객체의 인터페이스 확인하기
 
하지만 만약 Customer 리스트를 만들고 ISpecialService를 상속받은 객체를 찾고 싶다면 어떻게 해야 할까? 이 경우에 우리는 Type에서 지원하는IsAssignableFrom메서드를 통해서 실제로 해당 인터페이스가 구현되어 있는지의 여부를 가져올 수 있기 때문에 다음과 같은 코드를 작성하는 것이 가능하다. 
 
List<Customer> customers = new List<Customer>();
customers.Add(new SilverCustomer());
customers.Add(new GoldCustomer());
customers.Add(new Customer());

foreach (Customer c in customers)
{
    if (typeof(ISpecialService).IsAssignableFrom(c.GetType()))
    {
        (c as ISpecialService).SetSpecialService("");
    }
}
 
팁: 끝
 
위의 코드가 완벽히 솔리드 원칙들을 지원하고 있는 것이 아니다. 아직 더 다듬어야 하는 작업들이 남아 있다. 그럼 다음 규칙을 살펴 보도록 하자.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:10
개방과 폐쇄라고 해서 단순히 private나 public을 적절히 이용해야 된다고 생각하면 큰 오산이다. 이 개방과 폐쇄의 원칙은 확장에 있어서는 오픈하고 수정에 있어서는 폐쇄하라는 의미이다. 각각의 유닛들이 만들어 질때virtual 메서드를 적절히 이용하거나 상속을 이용할 수 있으면 우리는 보다 객체지향적인 코드를 생성할 수 있다. 
 
예를 들어 보도록 하겠다. 만약 호텔의 고객을 관리한다고 가정할때 고객의 등급이 일반, 실버, 골드로 나누어 진다고 가정해보자. 그리고 각각의 고객별로 다르게 할인율이 적용된다고 가정하겠다. 이때 이 할인율을 계산하는 GetDiscount라는 메서드를 만들어야 한다면 어떻게 클래스를 설계할지 생각해보자. 일반적으로 다형성의 개념을 도입하지 않는다면 아래와 같은 코드가 작성될 것이다. 
 
public enum CustomerType { Ordinary, Silver, Gold }

public class Customer_Bad_Example
{
    private readonly CustomerType _custType;
    public Customer_Bad_Example(CustomerType cType)
    {
        this._custType = cType;
    }

    public decimal GetDiscount(decimal totalSales)
    {
        if (_custType == CustomerType.Gold)
        {
            return totalSales - 100;
        }
        else if (_custType == CustomerType.Silver )
        {
            return totalSales - 50;
        }
        else
        {
            return totalSales;
        }
    }
}
 
어떠한가? 필자는 이 코드가 왠지 고향냄새가 나는 친근한 코드로 느껴진다. 이 코드를 보고 객체지향의 다형성이라는 개념을 적용해야 겠다라는 생각을 품을 수 있으면 어느정도 객체지향에 이해가 있는 독자일 것이다. 하지만 왜 다형성을 도입해야 하는지에 대한 이유를 아는 것이 더 중요하다.
 
다음 코드를 보면서 살펴보도록 하겠다. 먼저 필자는 위의 코드를 아래와 같이 다시 수정하였다. 
 
public class Customer
{
    public virtual double GetDiscount(double totalSales)
    {
        return totalSales;
    }
}

public class SilverCustomer : Customer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 50;
    }
}

public class GoldCustomer : SilverCustomer
{
    public override double GetDiscount(double totalSales)
    {
        return base.GetDiscount(totalSales) - 100;
    }
}
 
먼저 우리가 다형성을 도입하는 것은 향후 수정사항이 있을 경우에 보다 편하고 안전하게 그 수정사항을 대처할 수 있게 하기 위함이다. 만약 하나의 메서드에 모든 고객들의 로직을 포함시킨다면 향후 수정이 생길 경우에 모든 고객들의 코드에 영향을 줄 수 있는 위험이 있다. 즉, 모든 참조가 그 하나의 메서드에 걸려있게 되면서 만약 실버 고객을 위한 코드를 입력한다고 하더라도 그것이 잘못되면 다른 참조한 메서드들에게도 그 영향이 전달되는 것이다.
 
TDD에 있어서도 하나의 유닛만 테스트 해야 된다는 TDD의 원칙을 벗어나게 된다. 하나의 유닛 즉, 하나의 기능만 테스트 하고 정확하게 어떤 로직에서 에러가 있는지를 알려주는 것이 TDD의 목표이기 때문에 우리는 이 메서드의 기능을 분리 할 필요가 있다.
 
다형성을 통해서 우리는 기존의 기능들이 수정되는 것을 어느 정도 폐쇄시킬 수 있는 것을 살펴보았고 virtual 메서드를 제공함으로써 확장에 대한 가능성은 얼마든지 열어 두었다. 하지만 위의 코드가 완벽한 것은 아니다. 바로 다음에 살펴볼 리스코브의 대입을 완벽히 지원하고 있는 것이 아니기 때문이다. 


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:09
첫번째 디자인 규칙은 객체에서 수많은 기능이 아닌 단, 하나의 기능만 포함시키는 것이다. 쉽게 그림으로 이해해보도록 하자. 만약 클래스가 [그림1]처럼 만들어졌다면 어떨까? 
 
[그림1] 맥가이버 칼
 
이 도구를 사용하는데 있어서 큰 문제는 먼저 어디에 어떤 도구(기능)가 있는지 찾기 어렵다는 것이다. 필자 또한 필자의 클래스를 돌아보면 수많은 기능들이 포함되어지는 경우가 많다. 예를 들어 게시판의 글을 가져옴과 동시에 사용자의 정보들도 가져온다고 생각해보자. 이때 만약 게시글의 리파지토리와 사용자 리파지토리에서 각각의 정보를 가지고 올 수 있다면 위의 역할을 충족시키지만 하나의 클래스에서 작성되어진다면 이 원칙을 벗어나게 된다. TDD에서 유닛의 개념과 이 원칙의 개념은 상당히 비슷하다.
 
아주 심플한 예제로 아래 코드에서 잘못된 예를 찾아보자. 
 
class Customer
{
    public void Add()
    {
        try
        {
            //DB 접근코드
        }
        catch (Exception ex)
        {
            System.IO.File.WriteAllText(@"c:\Error.txt", ex.ToString());
        }
    }
}
 
 
위의 클래스에서 잘못된 부분은 명확하다. 이 클래스의 이름은 Customer이므로 고객과 관련된 정보들에 대한 기능만 가지고 있어야 한다. 하지만 만약 에러가 있을 경우에 catch 구문에서 로그를 작성하는 기능을 포함하게 된다. 아래 코드는 제대로 구분한 예를 보여준다. 
 
class FileLogger
{
    public void Handle(string error)
    {
        System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
}

class Customer
{
    private FileLogger obj = new FileLogger();
    public void Add()
    {
        try
        {
            // DB 접근코드
        }
        catch (Exception ex)
        {
            obj.Handle(ex.ToString());
        }
    }
}
 
위의 코드에서 예제를 위해서 try catch를 사용하였지만 좀 더 엄격하게 말하면 이 기능 또한 Customer 클래스에 있어야 하는 기능이 아니다. 뿐만 아니라 FileLogger를 직접 참조함으로써 의존성에 있어서도 여전히 강한 커플링을 유지하고 있다. 이런 내용들은 아래에서 다른 원칙을 소개하면서 다시 다루어 보겠다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:09

이번 장에서는 TDD를 하기 위해서 필요한 객체지향의 개념에 대해서 살펴본다. 객체지향이라고 해서 클래스가 무엇인지 인터페이스가 무엇인지를 다룬다기 보다는 이러한 개념을 가지고는 있지만 이것을 제대로 활용하기 위한 내용이 될 것이다. TDD를 도입하면서 가지게 되는 이점 중 하나가 바로 객체지향적인 코드를 보장해 주는 것이라고 언급했었다. 물론, TDD를 하면서 설계에 많은 고민을 가지게 됨으로써 보다 객체지향적인 코드를 설계하고 배워 나가는 것도 있다. 하지만이번 장에서는 그러한 시행착오를 없애기 위한내용으로 TDD를 위한 필수적인 객체지향 패턴들과 규칙들을 살펴보도록 하겠다. 

솔리드 디자인 원칙
 
솔리드(SOLID)는 다섯개의 객체지향 패턴으로 구성된 디자인 컨셉이다. 솔리드의 의미는 실제 그 각각의 디자인 규칙들의 첫번째 이니셜들을 묶어서 붙여진 것이다. 그럼 각각의 규칙들의 이름을 알아 보도록 하자. 
 
하나의 기능 - Single responsibility
 
개방과 폐쇄 - Open-closed
 
리스코브 대입 - Liskov substitution
 
인터페이스의 분리 - Interface segregation 
 
의존성의 반전 - Dependency inversion
 
위의 다섯가지 규칙은 필자가 영국에서 개발자로 활동하면서 가장 많이 공부한 디자인 패턴들이다. 한국과 다르게 영국은 디자인 패턴과 컨셉들을 굉장히 중요하게 여겼다. 이유는 솔리드 디자인 원칙에 맞추어 코드를 작성하면 보다 구조적이고 높은 퀄리티의 소스코드를 지향하기 때문이다.
 
TDD에서도 이 디자인 컨셉을 적용할 경우에 실제 작성된 코드를 리팩토링하는 수고를 덜 수 있다. 이번 장에서는 대부분의 내용을 이 디자인 패턴을 소개하고 적용해보는 것으로 채워갈 것이다.
 
지금까지 수많은 객체지향적인 디자인 패턴들이 소개되어 왔고 또한 많은 디자인 패턴 도서들에서도 수많은 디자인 패턴들을 소개하고 있다. 필자 또한 패턴 이름을 전혀 모르고 코드를 작성했지만 세월을 돌아서 패턴을 공부하다 보면 이미 이용해왔을 뿐 이름을 몰랐을 뿐이었던 적도 많이 있다.
 
이런 수많은 디자인 패턴들 속에서의 문제는 어떤 환경에서 어떻게 도입해야 될지에 대해서는 항상 개발자들이 풀어야 하는 숙제일 뿐만 아니라 상황에 따라서 그 디자인 패턴들이 단점이 더 크게 작용할 수 있기도 하다. 예를 들어 솔리드 디자인 원칙 중 하나의 규칙에서는 컴포지트 패턴을 쓰는 것보다는 스트레티지 패턴을 쓰는 것을 권고하고 있다.
 
솔리드 디자인 컨셉에서 소개하고 있는 다섯가지 원칙들은 명확하게 객체들을 어떻게 정의하고 또한 구분해서 개발해야 되는지에 대한 규칙을 보다 자세히 설명하고 있다. 그럼 첫번째 규칙부터 살펴보도록 하자.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:09
다른 객체와의 결합
 
지금까지의 예제는 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) 객체가 왜 필요한지에 대해서 살펴봤다. 유닛 테스트의 원칙을 지키기 위해서 우리는 의존성을 없애야만 하고 실제 코드들은 의존성들을 고려하여 보다 객체지향적으로 설계되어야 한다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:08
지금 살펴본 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] 통과된 테스트
 


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:08
대부분의 테스트 메서드에서는 예상 된 결과를 비교 하면서 테스트 결과를 테스트 프레임워크에 전달하게 된다. 이 때 이 역할을 수행해주는 것이 NUnit의 Assert 클래스이다. 지금까지의 테스트 예제에서는 Assert의 AreEqual이라는 메서드를 이용해서 두 값을 비교하면서 테스트 결과를 살펴봤었다.
 
하지만 AreEqual이나 AreSame과 같은 메서드는 실제 지금도 지원을 하고 있지만 오래된 문법이다. 실제 NUnit이 2.4로 버전업을 하면서 새로운 문법을 소개했었는데 그것은 바로 That이라는 문법이다. 그럼 상황에 따라서 어떻게 값을 비교할 수 있을지 살펴보도록 하자. 
 
 
팁! 값 타입과 참조타입의 == 키워드의 이용
 
알다시피 닷넷에서는 참조 타입과 값 타입 이렇게 두 개의 타입이 존재한다. 값 타입은 Stack 영역에 저장됨과 동시 그 값을 가지고 있다. 하지만 참조 타입은 Stack 영역이 아닌 Heap 영역에 저장되며 그 변수는 단순히 Heap 영역의 주소만 가지고 있게 된다. 때문에 값이 같다라는 것은 값 타입만 해당하는 것이다. 단, 예외는 있다. String의 경우 분명 참조 타입이지만 ==을 사용하여 값을 비교할 수 있다. 그 이유는 String 클래스의 경우 자신이 내부적으로 직접 == 키워드를 Equals 라는 메서드와 연결하기 때문이다.
 
다시 정리해서 String을 제외한 참조타입에서 == 를 실행할 경우에 그것은 같은 값이냐는 뜻이 아니라 같은 객체인지를 비교하게 된다. 조금 더 정확하게 말해서 같은 주소를 가지고 있냐라는 뜻이다.
 
두 값을 비교하고 싶은 경우
 
기존에 사용하던 AreEquals라는 단순한 문법을 최대한 활용하기 위해서 NUnit은 that 구문을 도입했고 두 개의 값을 비교하는데 있어서 Is.EqualTo 라는 문법을 사용하는 것이 가능해졌다. 먼저 숫자를 비교하는 예제는 아래와 같이 간단하게 작성할 수 있다.
 
Assert.That(2 + 2, Is.EqualTo(4.0)); Assert.That(2 + 2 == 4); Assert.That(2 + 2 != 5);
하지만 여기서 만약 같지 않다라는 구문을 넣고 싶다면 아래와 같이 작성할 수 있다. 
 
Assert.That(2 + 2, Is.Not.EqualTo(5));
 
여기서 만약 오차 범위를 허용하고 싶다고 가정해보자. 예를 들어서 5라는 값에서 플러스 마이너스 0.5까지는 허용하고 싶은 것이다. 이때 우리는 Within이라는 추가 메서드를 이용해서 구문 수식하는 것이 가능하다.
 
int result = 5;
Assert.That(5.5, Is.EqualTo(result).Within(0.5));
 
즉, 5.5라는 예상된 값을 비교하는데 있어서 0.5까지의 오차 범위를 허용하기 때문에 실제 값이 5일 경우에도 위의 테스트는 성공적으로 돌아가게 된다. 뿐만 아니라 Percent라는 수식어를 통해서 우리는 오차 범위가 실제 숫자가 아닌 예상한 값의 몇 프로까지 허용할지를 지정해줄 수 있다. 
 
int result = 5;
Assert.That(5.5, Is.EqualTo(result).Within(20.5).Percent);
 
위의 예제에서는 실제 결과값에서 20.5% 까지의 오차는 허용하겠다는 것이다.
 
 
문자열의 비교
 
문자열 또한 위와 같은 방법으로 비교가 가능하다. 특별히 대소문자를 구별할 것인지에 대한 옵션을 정의하는 것이 가능하다. 아래 코드를 살펴보자. 
 
Assert.That("Hello!", Is.Not.EqualTo("HELLO!"));
Assert.That("Hello!", Is.EqualTo("HELLO!").IgnoreCase);
 
두 테스트 모두 통과 된다. 두 번째 구문에서 우리는 IgnoreCase를 붙였기 때문에 가능하다. 그 외에도 특별히 추가적인 문자열을 가지고 있는지 여부를 StringContaining 메서드를 통해서 확인할 수 있다. 
 
string phrase = "Make your tests fail before passing!";
Assert.That(phrase, Is.StringContaining("tests fail"));
Assert.That(phrase, Is.Not.StringContaining("tests pass"));
 
물론 반드시 테스트에서 제공하는 기능을 써서 비교해야 한다는 것은 아니다. 우리가 원래 알고 있던 문법을 응용할 수 있으면 아래와 같이 작성해도 무방하다.
 
Assert.That(phrase.Contains("tests fail"));
 
기본적으로 String에서 제공하는 Contains가 true/false를 반환하기 때문에 위의 테스트는 당연히 true를 반환하게 되고 테스트는 통과될 수 있다.
 
 
날짜의 비교
 
실제 캐싱을 이용할 경우에 시간을 비교해서 캐시가 되고 있는지를 확인하는 경우가 종종 있다. 이때 우리는 날짜를 비교하게 되는데 Within을 지원하기 때문에 손쉽게 이 코드를 작성할 수 있다. 
 
DateTime now = DateTime.Now;
Thread.Sleep(1000);
Assert.That(now, Is.EqualTo(DateTime.Now).Within(2).Seconds);
 
즉, 여기서 now라는 시간이 현재 시간과 2초 안의 차이가 난다면 이 테스트는 통과 하게 된다.
 
 
배열의 값들을 비교
 
NUnit은 실제 배열 안의 값들이 같은지를 비교하는 것이 가능하다. 하지만 배열의 요소들은 같아도 각 배열 요소들의 순서가 다르면 다른 값으로 인식한다. 
 
int[] i3 = new int[] { 1, 2, 3 };
double[] d3 = new double[] { 1.0, 2.0, 3.0 };
int[] iunequal = new int[] { 1, 3, 2 };
Assert.That(i3, Is.EqualTo(d3));
Assert.That(i3, Is.Not.EqualTo(iunequal));
 
 
객체의 비교
 
객체가 같은지를 비교한다는 것은 가지고 있는 주소가 같은지를 비교한다는 것이다. 즉, 같은 객체를 바라보고 있는지 아닌지에 대한 여부이다. 이전 문법이라면 AreSame이라는 메서드를 이용하면 될 것이다. 하지만 여기서는 Is.EqualTo 대신 Is.SameAs 라는 구문을 이용하면 된다. 아래 예제를 살펴보자. 
 
Exception ex1 = new Exception();
Exception ex2 = ex1;
Assert.That(ex2, Is.SameAs(ex1));

Exception ex3 = new Exception();
Assert.That(ex3, Is.Not.SameAs(ex1));
 
 
이렇게 해서 NUnit을 다루는데 있어서 필요한 기본적인 지식을 어느 정도 전달했다. 이제 위의 지식들을 바탕으로 실전 프로그램을 한번 작성해보도록 하자.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:08
NUnit에서 지원하는 애트리뷰트들에 대해서 먼저 살펴보도록 하자. 
 
TestFixture
 
바로 앞의 예제에서는 클래스 위에 TestFixture를 이용해서 테스트를 정의 했다. 이 애트리뷰트는 주로 테스트 메서드들을 가지고 있음을 명시해주기 위해서 사용한다. 다시 말해서, 만약 이 애트리뷰트를 클래스에 정의하게 되면 내부적인 테스트 애플리케이션이 내부적인 테스트 메서드들이 존재하는지 스캔하게 된다. 아래 코드는 이 테스트 메서드를 사용하는 방법을 보여준다.
 
using System;
using NUnit.Framework;

namespace UnitTestingExamples
{
    [TestFixture]
    public class SomeTests
    {
    }
}
 
 
Test
 
Test 애트리뷰트는 TestFixture 안에 존재하는 메서드들에서 사용한다. 바로 테스트를 진행해야 되는 메서드라는 것을 알려주는 역할을 하게 된다. 단 여기서 중요한 부분이 이 테스트 메서드는 반드시 public 으로 작성되어야 하고 어떤 파라메터를 받아서는 안 된다. 또한 반환 타입은 항상 void로 작성되어야 한다. 다음 소스코드는 Test 메서드의 예제를 보여주고 있다.
 
namespace UnitTestingExamples
{
    using System;
    using NUnit.Framework;

    [TestFixture]
    public class SomeTests
    {
        [Test]
        public void TestOne()
        {
        }
    }
}
 
 
TestFixtureSetUp 과 TestFixtureTearDown
 
이 애트리뷰트들은 TestFixture로 선언된 클래스 안의 메서드들에 추가할 수 있는 기능인데, TestFixtureSetUp은 테스트를 진행하기 전에 먼저 호출되어 테스트 하는데 있어서 초기화 하거나 정의해야 값을 지정하기 위해서 사용된다. 그리고 TestFixtureTearDown의 경우 모든 테스트들이 마친 뒤에 마지막으로 호출된다. 예를 들어 만약 테스트 뒤에 어떤 일을 수행해야 하거나 테스트 전에 반드시 준비해야 하는 공통 작업이 있다면 이 애트리뷰트들이 유용하게 사용될 것이다.
 
namespace NUnit.Tests
{
    using System;
    using NUnit.Framework;

    [TestFixture]
    public class SuccessTests
    {
        [TestFixtureSetUp]
        public void Init()
        {  }

        [TestFixtureTearDown]
        public void Cleanup()
        {  }

        [Test]
        public void Add1()
        {  }

        [Test]
        public void Add2()
        {  }
    }
}
 
위의 테스트를 실행하게 될 경우에 메서드가 호출되는 순서는 Init()->Add1->Add2->CleanUp 이 될 것이고 실제 테스트는 Add1 메서드와 Add2메서드만 진행하고 결과를 보여주게 된다.
 
 
ExpectedException
 
이 메서드는 테스트를 실행했을 때 이 애트리뷰트에서 정의한 예외가 만들어졌는가를 테스트 하게 된다. 물론 try catch를 이용해서 예외를 잡은 뒤에 실제로 예상한 예외가 만들어졌는지 직접 비교할 수도 있지만 그럴 경우에 테스트 코드가 try catch로 묶이면서 조금 복잡해 질 수 있다. 예제 코드는 아래와 같다. 
 
namespace NUnit.Tests
{
    using System;
    using NUnit.Framework;

    [TestFixture]
    public class SuccessTests
    {
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void ExpectAnExceptionByType()
        { 
            throw new InvalidOperationException(); 
        }

        [Test]
        [ExpectedException("System.InvalidOperationException")]
        public void ExpectAnExceptionByName()
        { 
        }
    }
}
 
먼저 코드에서 원하는 예외 타입을 typeof 키워드를 이용해서 지정하는 방법이 있으며 아니면 직접 어셈블리 이름을 지정하는 것도 가능하다. 위의 테스트 코드를 살펴보면 첫번째 테스트에서는 throw를 통하여 강제적으로 예외상황을 만들어 발생시켰다. 하지만 아래 메서드에서는 예외를 발생시키지 않았다. 테스트 결과는 예상했던 대로 예외를 발생시킨 하나의 테스트만 통과되는 것을 볼 수 있다.
 
[그림12] ExpectException의 테스트 결과
 
 
Ignore
 
이 애트리뷰트는 실제 테스트로 정의 되어 있어도 테스트를 진행하지 않고 무시하겠다라는 의미로 사용한다. 물론, 실제 [Test] 애트리뷰트를 지운 뒤에 주석으로 달아도 되지만 그럴 경우 테스트 목록에서 없어져 버리고 만다. 하지만 Ignore를 이용할 경우에 테스트 리스트에서 빨간색도 초록색도 아닌 노란색 메시지를 표시하면서 왜 포함이 되지 않았는지에 대한 이유를 작성할 수 있다. 예제 코드를 살펴보자. 
 
namespace NUnit.Tests
{
    using System;
    using NUnit.Framework;

    [TestFixture]
    public class SuccessTests
    {
        [Test]
        public void ExpectAnExceptionByType()
        {
        }

        [Ignore("이번 버전에서는 제외된 기능")]
        [Test]
        public void ExpectAnExceptionByName()
        { 
        }
    }
}
 
이렇게 메시지를 정의하게 될 경우 아래와 같은 테스트 결과와 위에서 정의한 메시지 또한 확인할 수 있다.
 
[그림13] Ignore 메세지
 
 
TestCase
 
이 애트리뷰트는 가장 자주 사용하는 기능이다. 여러 가지 테스트 케이스를 만들어 내고 싶다면 이 기능을 사용해도 된다. 다시 말해서 같은 테스트를 다른 값들을 가지고 반복해서 진행하고 싶다면 두세 번 테스트 메서드를 적용하지 않고도 단순히 값만 할당하는 것으로 테스트를 진행할 수 있다. 예를 들어 1장에서 아래와 같이 계속 반복되는 테스트를 작성했었다.
 
[TestClass]
public class MultipleCalculatorTest
{
    [TestMethod]
    public void WhenValueDivisibleOnlyBy3()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "Joel";
        string result = mCal.Calculate(3);
        Assert.AreEqual(valueExpected, result);
    }

    … 생략 …

    [TestMethod]
    public void WhenValueDivisionBy3and5()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "Joel Sarah";
        string result = mCal.Calculate(15);
        Assert.AreEqual(valueExpected, result);
    }
    [TestMethod]
    public void WhenValueIsNotDivisibleBy3Or5()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "4";
        string result = mCal.Calculate(4);
        Assert.AreEqual(valueExpected, result);
    }
}
 
위의 코드에서는 valueExpected 라는 우리가 테스트에서 예상하고 있는 문자열 값과 mCal.Calculate에 할당하게 되는 숫자 값만 다를 뿐 안에서 진행하는 내용은 모두 같다. 위의 내용을 TestCase를 이용해서 아주 간단하게 작성할 수 있다.
 
namespace NUnit.Tests
{
    using System;
    using NUnit.Framework;
    using HOONS.TDD.Chapter1.SampleExample;

    [TestFixture]
    public class TestCase 
    {
        [TestCase("Joel", 3)]
        [TestCase("Sarah", 5)]
        [TestCase("Noah", 7)]
        [TestCase("4", 4)]
        public void Chapter1_TestAgain(string valueExpected, int number)
        {
            var mCal = new MultipleCalculator();
            string result = mCal.Calculate(number);
            Assert.AreEqual(valueExpected, result);
        }
    }
}
 
이렇게 TestCase를 작성해줌으로써 우리는 아래와 같이 테스트 케이스별 결과를 확인할 수 있다. 
 
[그림14] TestCase 예제
 
 
Timeout
 
이 애트리뷰트는 사용이 간단하다. 만약 테스트가 지정한 밀리세컨드의 시간 안에 수행되지 않을 경우에 에러메세지를 반환하는 것이다. 즉, 성능에 민감한 문제가 있을 경우에 간단하게 정의하여 사용할 수 있다. 
 
namespace NUnit.Tests
{
    using System;
    using NUnit.Framework;

    [TestFixture]
    public class Timeout 
    {
        [Test]
        [Timeout(1)]
        public void ShouldFinishInOneMS()
        {    }
    }
}
 
위의 테스트에서는 1밀리 세컨드를 셋팅 했다. 아무리 메서드 안의 실행 값이 없다고 한들 이 시간 안에 끝나는 것이 무리인지 실패 메시지를 반환하게 된다. 
 
[그림15] Timeout 애트리뷰트
 
위의 값을 10으로 바꿀 경우 해당 PC에 따라 다르겠지만 필자의 PC에서는 7 밀리초를 반환하면서 테스트가 통과하는 것을 볼 수 있었다.
 
이렇게 해서 대부분의 중요한 NUnit의 애트리뷰트들을 살펴보았다. 여기서 소개된 정도만 사용할 수 있으면 충분히 효율적인 테스트 코드를 작성하는 것이 가능할 것이다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:07
대부분의 테스트 프레임워크들은 다음과 같은 순서들을 제공한다. 
 
1. 테스트 코드 작성
2. 테스트 실행
3. 결과 확인
4. (옵션) 테스트 코드 수정
 
먼저 테스트 코드를 작성하기 전에 개발환경을 셋팅해야 한다. MSTest는 마이크로소프트에서 제공하는 기본 테스트 툴인데 이 툴을 이용할 경우에는 별다른 셋업이 필요 없지만 NUnit을 비주얼 스튜디오 테스트 창과 연동하기 위해서는 아래의 주소에서 NUnit 테스트 아답터라고 불리는 통합 UI 도구를 다운 받아서 설치해도 되고 Nuget 페키지에서 직접 설치해도 된다. 다시 정리하자면 NUnit이라는 라이브러리와 TestAdapter라는 두개의 프레임워크가 필요하다.
 
- NUnit 테스트 아답터(NUnit Test Adapter)
http://visualstudiogallery.msdn.microsoft.com/6ab922d0-21c0-4f06-ab5f-4ecd1fe7175d
 
필자는 Nuget 페키지에서 모두 셋업 할 예정이기 때문에 별다른 셋팅없이 바로 테스트 프로젝트를 만들어 보도록 하겠다. 이번 도서에서 필자는 비주얼 스튜디오 2013을 이용하지만 비주얼 스튜디오 2012와도 큰 차이 없이 진행할 수 있다. 그럼 먼저 테스트 프로젝트를 하나 생성해 보도록 하겠다. 비주얼 스튜디오에서 [파일]-[새프로젝트] 를 실행한다.
 
[그림3] 테스트 프로젝트 생성
 
새로운 프로젝트가 생성하면 아래와 같은 기본 클래스와 메서드가 작성되어 있을 것이다. 
 
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {

        }
    }
}
[기본 소스코드]
 
위의 TestClass와 TestMethod가 바로 비주얼 스튜디오에서 기본적으로 제공하는 MSTest라는 프레임워크에서 지원하고 있는 기능이다. 여기서 우리는 NUnit이라는 프레임워크를 이용할 것이기 때문에 using Microsoft.VisualStudio.TestTools.UnitTesting 라는 참조 구문은 지워도 무방하다. 
 
자, 그럼 NUnit 도구들을 추가해 보도록 하자. 먼저 NUnit 패키지를 추가하기 위한 두 가지 방법이 존재하는데 Nuget 패키지 관리자를 직접 열어서 찾아서 추가하는 방법이다. 솔루션 탐색기를 열어서 [참조] 에서 [NugetGet 패키지 관리] 메뉴를 선택하자.
 
[그림4] NuGet 페키지 관리 창의 선택
 
Nuget 패키지 관리 창을 열었으면 NUnit으로 검색을 실행해야 하는데 왼쪽에서 [온라인] 메뉴를 선택한 뒤에 오른 쪽 위의 검색창에 NUnit을 검색해 보도록 하자. 그럼 아래 [그림5]와 같은 검색 결과들이 나오게 되는데 여기서 우리는 두 개의 라이브러리를 추가해야 해주어야 한다. 먼저 NUnit 라이브러리를 선택한 뒤에 설치 버튼을 눌러 설치 하도록 하자.
 
[그림5] NUnit 라이브러리 추가하기
 
설치가 완료되면 그 뒤에 NUnit 아답터를 추가하도록 하자. 
 
[그림6] NUnit 테스트 아답터의 설치
 
다시 설명하자면 이 아답터는 실제 비주얼 스튜디오 테스트 창에 NUnit 라이브러리로 테스트된 결과를 연동하여 보여주는 역할을 한다. 이렇게 두 개의 라이브러리를 추가하게 되면 다음과 같은 참조들이 생성된다.
 
[그림7] 참조된 NUnit 라이브러리
 
여기서 가장 위에 있는 Microsoft.VisualStudio.QualityTools.UnitTestFramework는 사용하지 않을 것이기 때문에 없애도 무방하다. 
 
지금까지 Nuget 매니저를 통하여 라이브러리를 참조한 방법 외에도 콘솔 창을 이용해서 단순한 명령어 하나로 쉽게 추가하는 방법이 있는데 한번 살펴보도록 하자. 먼저 콘솔창을 열기 위해서 [도구]-[라이브러리 패키지 관리자]-[패키지 관리자 콘솔]을 실행해보자. 
 
[그림8] 패키지 관리자 콘솔
 
그럼 [그림9]와 같은 콘솔 창이 나오게 되는데 여기서 우리는 아래의 명령어를 통해서 손쉽게 NUnit을 추가하는 것이 가능하다. 
 
install-Package NUnit
NUnit의 설치 명령어
 
Install-Package NUnitTestAdapter
NUnit 아답터의 설치 명령어
 
 
[그림9] 콘솔을 이용하여 라이브러리의 추가
 
 
이렇게 함으로써 아주 간단하게 테스트 환경 셋팅이 마무리 된다. 그럼 이제 기본으로 생성된 코드에서 기본 애트리뷰트 이름을 변경하고 새로운 네임스페이스를 참조하여 보도록 하겠다.
 
using System;
using NUnit.Framework;

namespace UnitTestProject1
{
    [TestFixture]
    public class UnitTest1
    {
        [Test]
        public void TestMethod1()
        {

        }
    }
}
 
위에서 정의된 메서드가 하나의 테스트 요소가 된다. 테스트가 아닌 실제 프로그램에서 작성된 메서드는 하나라고 하더라도 테스트할 요소들은 여러 개가 될 수 있기 때문에 여러 개의 테스트 메서드가 생성될 수 있다. 
그럼 이제 기본 테스트 탐색기를 실행해 보도록 하겠다. 실제 프로그램을 빌드하는 것과 테스트 하는 것은 별개의 작업이다. 테스트를 실행하려면 비주얼 스튜디오 메뉴에서 [테스트]를 선택한 뒤에 [실행] – [모든 테스트]를 선택하면 된다.
 
[그림10] 테스트 실행하기
 
위의 명령을 실행할 경우 테스트 탐색기가 열리면서 아래 [그림9]와 같이 테스트 메서드 이름과 수행시간 그리고 테스트 이름(메서드이름)들이 출력되는 것을 볼 수 있다.
 
[그림11] 테스트 결과 레포트
 
우리는 어떤 코드도 작성하지 않았기 때문에 바로 통과를 진행할 수 있었다. 그럼 이제 본격적으로 NUnit에서 지원하는 기능들을 살펴보도록 하겠다.
 


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:07
앞에서 이미 통합 테스트(Integrate Test)와 유닛 테스트(Unit Test)의 차이가 무엇인지 그리고 왜 유닛 테스트가 중요한지에 대해서 설명했다. 이번 장에서는 보다 자세히 유닛 테스팅이 무엇인지 살펴보고 또한 NUnit을 이용해서 테스트 코드를 작성하는 방법을 살펴보도록 하겠다. 
 
유닛 테스트의 정의
 
먼저 유닛 테스트를 간단하게 정의하자면 하나의 프로그램 유닛을 테스트 할 수 있는 단위로 정의 할 수 있다. 여기서 하나의 프로그램 유닛이라는 것은 하나의 기능을 가지고 있는 메서드 정도가 될 수 있다. 앞에서 1부터 20까지의 수 중에서 3의 배수와 5의 배수일 때 특정 문자열을 반환하는 예제를 살펴봤었다. 여기서 우리는 for문 안에서 몇 번을 반복하든지 크게 상관하지 않았다. 중요한 것은 해당 수에 알맞은 문자열을 반환하는 것이 우리의 목표였고 하나의 유닛이었던 것이다. 만약 우리가 For문이 제대로 1부터 20까지 반환하는지도 한 메서드 안에서 같이 정의 했다면 이 프로그램은 이미 유닛의 범위를 넘어서게 된다. 
 
단, 여기서 하나의 유닛이 존재한다고 하더라도 테스트 요소는 여러 개일 수 있다. 우리도 이미 앞의 예제에서 하나의 메서드를 가지고 4개 이상의 테스트를 작성했다. 물론, 위의 예제가 굉장히 간단해서 그럴 수도 큰 고민 없이 작성했을 수도 있다. 만약 그 메서드에서 다른 클래스를 참조 해서 사용하고 있다면 어떨까? 우리는 이렇게 서로 다른 객체들이 서로 참조관계를 가지고 있는 것을 가지고 의존성(Dependency)이라고 부른다. 이 경우에 우리는 의존성을 제거하나 하나의 유닛만 테스트 해야 하는데 이 부분에 대해서는 뒷부분에서 보다 자세히 살펴 볼 것이다.
 
하나의 유닛을 정의하는데 있어서 무엇보다도 중요한 것은 유닛 테스트의 핵심이 바로 향후에 어떤 에러가 있을 때 어느 유닛에서 문제가 있는지 정확하게 잡아내는 것에 있다는 것이다. 
 
 
모호한 테스트 범위들
 
먼저 처음 유닛테스팅을 시작할 때 가장 모호한 부분이 바로 지금 테스트 하고 또 작성해야 될 코드가 다른 시스템과 연결되어 있을 경우이다. 예를 들면 다른 DLL을 참조하여 사용할 수도 있고 DB나 웹서비스와 같이 외부 시스템들과 연결되어 있는 경우가 다분하다. 만약 테스트를 진행했는데 이러한 요소들과의 연결부분에서 에러를 발생할 가능성이 있기 때문에 우리는 실제로 내부적인 문제를 진단하는데 큰 어려움이 겪게 된다. 앞에서 유닛 테스트의 핵심은 모든 테스트를 유닛 별로 자동화시켜 놓은 뒤에 어떤 문제가 발생했을 때 그 유닛을 정확하게 잡아내는 것에 있다고 했다. 즉, 우리는 여러 레이어들을 일일이 진단하면서 다시 그 버그를 잡는 작업이 진행된다면 그것은 이미 유닛 테스트의 목적을 상실하게 되는 것이다.
 
다른 또 하나의 이슈는 DB와 같이 데이터를 공유하여 사용하다 보면 다른 개발자들에 의해서 그 라이브러리가 수정되거나 업데이트 되거나 할 경우에 에러가 발생할 수 있다. 즉, 테스트 결과는 외부의 리소스에 따라서 달라질 수 있다는 것이다. 다시 유닛 테스트의 정의로 돌아와서 만약 하나의 값을 데이터 베이스에 저장한 뒤에 읽어오고 또 지우는 테스트가 포함된다면 이것은 유닛 테스트의 범위를 벗어 나게 되는 것이다. 효율 면으로 살펴봤을 때도 외부 라이브러리와의 연동이 유닛 테스트가 될 수 없는 이유도 분명하다. 만약 나 혼자서만 그 DB를 사용하는 것이 아니라 다른 개발자들도 그 DB의 같은 테이블을 사용하고 있다고 가정해보자. 이 경우에 여러 개발자가 같은 테스트를 작성하게 될 확률이 높다. A모듈에서도 그 테이블에 값을 불러 올 수도 있고 B 모듈에서도 그 테이블에 값을 불러 올 수 있기 때문인 것이다. 
 
그렇다면 이런 경우의 테스트는 어떻게 진행해야 될 것인가? 이 경우에 우리는 통합 테스트 코드를 작성해야 한다. 통합 테스트 역시 개발 중에 작성되는 테스트로서 유닛테스트와 크게 다르지 않다. 통합 테스트에 있어서 좋은 모토는 “일찍 통합하여 자주 통합하자” 이다. 이 통합 테스트는 뒷부분의 챕터에서 보다 자세히 살펴보도록 하겠다. 
 
 
유닛 테스트 프레임워크
 
유닛 테스트 프레임워크가 존재하기 전에 개발자들은 자동적으로 점검이 가능한 테스트를 작성하는데 있어서 큰 어려움이 있었다. 초기에는 실제로 테스트 팀에서 각 테스트 별로 직접 윈도우를 만들고 또 버튼을 추가해서 테스트를 진행하고 했었다. 이 테스트 결과로 메시지 박스를 보여주거나 텍스트를 뿌려서 보여주고는 했다. 하지만 이렇게 진행해오던 테스트들이 지금은 비주얼 스튜디오나 별도의 테스트 도구를 통해서 어떤 추가적인 개발 없이 각각의 테스트를 자동화 시키는 것이 가능해졌다. 애자일의 개발방법론에서 매일 빌드하여 배포하고 테스트를 자동화하는 CI(Continuous Integration)을 도입이 가능해진 이유도 바로 이렇게 발전된 유닛 테스트 프레임워크 툴이 존재하기 때문이기도 하다. 
 
NUnit이나 MSTest와 같은 테스트 프레임워크들이 바로 그 역할들을 수행하게 된다. 유닛 테스트 프레임워크들은 일관된 코딩 스타일을 제공하고 있는데 단순히 테스트 메서드와 테스트 클래스들 위에 애트리뷰트를 적용하면서 간단하게 코드를 작성할 수 있다. 즉, 과거처럼 테스트 UI를 직접 작성하거나 에러메세지를 작성하여 보여주지 않아도 되는 것이다. 비주얼 스튜디오에서는 테스트에 실패하게 될 경우에 왜 테스트가 잘못 되었는지 메시지를 보여주게 된다. 
 
[그림1] 비주얼 스튜디오 2013에서의 테스팅 메시지
 
 
NUnit 프레임워크는 현존하는 프레임워크들 중에 가장 많이 사용되고 또 이용되고 있다. NUnit은 닷넷 테스트 툴로 제공되고 있으며 자바를 위한 프레임워크로 JUnit이라는 프레임워크 또한 제공하고 있다. 기존에는 비주얼 스튜디오가 별도로 테스트 툴을 제공하지 않았기 때문에 아래 [그림2]처럼 별도의 툴을 실행시켜야만 했다.
 
[그림2] 이전 NUnit의 UI


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:07
TDD 개발 프로세스
 
먼저 아래와 같은 프로그램을 작성한다고 가정하겠다. 
 
 
1부터 20까지의 수를 출력하는 프로그램을 작성하시오. 단, 3의 배수에서는 숫자대신 “JOEL”를 5의 배수에서는 “SARAH” 그리고 3과 5의 배수에서는 “JOEL SARAH” 를 출력하시오. 
 
 
위의 프로그램을 Console에 작성한다고 가정할 때 먼저 지금까지 개발해온 개발 방법론으로 접근해보도록 하겠다. 아마도 80% 이상의 개발자들은 아마 아래와 같은 코드를 작성할 것이다.
 
static void Main(string[] args)
{
    for (int i =1 ; i<=20;i++)
    {
        if (i % 3 == 0 && i % 5 == 0)
            Console.WriteLine("Joel Sarah");
        else if (i % 3 == 0)
            Console.WriteLine("Joel");
        else if (i % 5 == 0)
            Console.WriteLine("Sarah");
        else
            Console.WriteLine(i.ToString());
    }
}
 
그리고 코드를 실행해서 코드가 잘 실행되었는지 확인할 것이다. 

[화면] 결과
 
하지만 여기서 TDD 프로세스로 이 프로그램을 작성해보도록 하자.
 
 
Test-Driven Development 프로세스
 
먼저 구조를 디자인 해야 하는 것이 먼저 일 것이고, 그 다음 테스트 코드를 작성한 뒤에 마지막으로 실제 코드를 작성하게 될 것이다. 아래 샘플 코드를 읽기 전에 만약 여러분이면 어떻게 테스트 코드를 작성할지 한번 먼저 생각해 보길 권한다.
 
앞선 프로그램에서는 코드 디자인에 대해서는 별다른 생각 없이 바로 코드 작성을 시작했지만 TDD에서는 테스트 코드를 작성하기 위해서 디자인에 대해서 더 신중하게 고민하고 또 구조를 잡고 시작해야 한다. 또한, 우리는 무엇을 테스트 해야 할지를 명확하게 이해하고 있어야 한다. 즉, 테스트 요소들을 분석해내야 하는 것이다. 앞의 예제에서 우리는 프로그램을 실행함과 동시에 3과 5의 배수와 5 그리고 3과5의 배수인 15 번째 수를 찾아서 정확하게 의도한 문자가 출력되었는지 살펴 봤을 것이고 추가로 3과 5의 배수가 아닐 경우에는 숫자가 출력된 것을 살펴봤을 것이다. 즉, 이것이 우리가 테스팅 해야 하는 요소 들인 것이다. 다시 테스팅 요소들을 아래와 같이 보다 정리해 볼 수 있다. 

 
- 숫자가 3의 배수일 때 Joel을 반환

- 숫자가 5의 배수일 때 Sarah를 반환

- 숫자가 3과 5의 배수일 때 Joel Sarah를 반환

- 숫자가 3의 배수도 5의 배수도 아닐 때 숫자를 반환
 
 
테스트 요소의 정리가 끝났기 때문에 프로그램을 디자인 해보도록 하겠다. 먼저 위의 기능을 수행하는 클래스를 아래와 같이 정의 하도록 하겠다. 
 
public class MultipleCalculator
{
    public string Calculate(int i)
    {
        //To Do
        return null;
    }
}
 
먼저 클래스를 생성했고, 아직 코드는 작성하지 않고 남겨 놨다. 이 Calculate 메서드는 실제 숫자를 전달하면 그에 따른 적합한 수를 반환해줄 것이다. 그럼 이제 테스트 코드를 작성해 보도록 하자. 
 
여기서 필자는 새로운 프로젝트를 하나 추가했다. 비주얼 스튜디오 2012에서 기본적으로 지원하는 테스팅 프로젝트를 추가할 경우에 아래와 같은 기본 코드가 작성되어 있을 것이다. 
 
[TestClass]
public class UnitTest1
{
    public UnitTest1()
    {
        //
        // TODO: Add constructor logic here
        //
    }

    [TestMethod]
    public void TestMethod1()
    {
        //
        // TODO: Add test logic here
        //
    }
}
 
필자는 NUnit을 주로 선호하지만 이번 강좌에서는 MSTest를 이용할 것이다. 자세한 내용은 다음 회차에서 보다 자세히 설명할 것이다. 여기에서는 눈으로만 살펴 보도록 하자. 필자는 4개의 테스팅을 다음과 같이 작성하였다. 
 
 
[TestClass]
public class MultipleCalculatorTest
{
    [TestMethod]
    public void WhenValueDivisibleOnlyBy3()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "Joel";
        string result = mCal.Calculate(3);
        Assert.AreEqual(valueExpected, result);
    }
    [TestMethod]
    public void WhenValueDivisibleOnlyBy5()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "Sarah";
        string result = mCal.Calculate(5);
        Assert.AreEqual(valueExpected, result);
    }

    [TestMethod]
    public void WhenValueDivisionBy3and5()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "Joel Sarah";
        string result = mCal.Calculate(15);
        Assert.AreEqual(valueExpected, result);
    }
    [TestMethod]
    public void WhenValueIsNotDivisibleBy3Or5()
    {
        var mCal = new MultipleCalculator();
        string valueExpected = "4";
        string result = mCal.Calculate(4);
        Assert.AreEqual(valueExpected, result);
    }
}
 
Assert.AreEqual() 이라는 메서드에서는 두 개의 값을 받아서 서로 같은지를 비교해서 테스트를 진행해준다. 지금 Calculate라는 메서드는 null 을 반환하기 때문에 당연히 테스트를 통과할리가 없다. 테스트를 실행하면 아래와 같이 빨간 신호들이 켜지게 된다. 
 
 
이제 우리가 할 일은 Calculate 메서드를 만들고 프로그램을 완성시켜서 저 빨간 불을 초록색 불로 만드는 작업이 필요하다. 앞에서 테스트 작성이 모두 끝났기 때문에 실제 구현 단계로 넘어 가서 아래와 같이 코드를 작성하도록 하겠다.
 
 
public class MultipleCalculator
{
    public string Calculate(int i)
    {
        if (i % 3 == 0 && i % 5 == 0)
            return "Joel Sarah";
        else if (i % 3 == 0)
            return "Joel";
        else if (i % 5 == 0)
            return "Sarah";
        else
            return i.ToString();
    }
}
 
이렇게 프로그램을 업데이트 하고 다시 테스트를 재실행하면 아래와 같은 결과를 확인할 수 있다.
 
 
이제 마지막으로 Main 메서드를 다음과 같이 수정하면 모든 작업을 끝이 난다. 
 
static void Main(string[] args)
{
    var mCal = new MultipleCalculator();
    for (int i = 1; i <= 20; i++)
    {
        Console.WriteLine(mCal.Calculate(i));
    }
}
 
물론, 실제 프로그램은 이렇게 간단할 리는 없을 것이다. 하지만 어떠한가? 의도가 무엇이었든 간에 코드가 조금 구조를 잡아가는 느낌이 들지 않는가? 여기서 수정 사항이 아래와 같이 있다고 가정해보자. 
4의 배수일 때는 Noah를 출력해주시오.
 
이때 우리는 당연히 테스트 코드를 먼저 작성해야 할 것이다. 즉, 4의 배수를 확인하는 메서드를 하나 작성하게 될 것이고, 빨간 에러 메세지를 본 뒤에 실제 Calculate메서드를 수정해서 테스트를 초록색 불로 만드는 작업을 하게 될 것이다. 이때 우리는 별다른 프로그램의 구동 없이 테스트 결과 만으로 테스트를 완료할 수 있음과 동시에 기존의 유닛 테스트들도 테스트가 통과됨을 알게 됨으로써 시간적으로도 충분히 절약할 수 있을 것이다. 

 
정리
 
이번 강좌에서는 통해서 먼저 TDD의 방법론에 대해서 살펴보았다. 실제 코드를 작성하기 전에 코드를 먼저 구조화 한 뒤에 테스트 코드를 작성한다. 그리고 나서 실제 동작 코드를 작성함으로써 나중에 테스트 뒤에 디자인이나 코드 구조를 바꿔야 하는 시간적인 낭비를 막을 수 있다고 설명했다.

그리고 이어서 통합 테스팅과 유닛 테스팅의 차이를 살펴 보았다. 유닛 테스팅은 테스트 요소들을 유닛테스트 프레임워크를 이용해서 실제 코드로 구현함으로써 테스트를 자동화 시킬 수 있을 뿐만 아니라 모듈 간의 의존성과 상관없이 쉽게 문제를 진단할 수 있는 이유 또한 설명했다.

마지막으로 간단한 예제를 통해서 TDD 개발 프로세스를 살펴 보았다. 이 프로젝트에서는 NUnit이 아닌 비주얼 스튜디오에서 기본적으로 제공하는 MSTest를 이용해서 간단하게 살펴봤다.

다음 장에서는 유닛 테스트 프레임워크인 NUnit을 사용하는 방법을 살펴볼 것이고 필요한 테스팅 메서드와 클래스들을 설명할 것이다.


저작자 표시
신고
Posted by 박경훈
.Net Technology/.NET TDD2014.10.19 18:06
먼저 TDD에서 도입하는 유닛 테스트를 이해하기 위해서 기존의 테스트 프로세스를 먼저 살펴볼 필요가 있다. 대부분의 프로젝트에서는 통합 테스트(Integration Test)를 통하여 소프트웨어를 테스트하고 진단하게 된다. 먼저, 우리가 운전하고 있는 자동차가 어떤 문제가 생기면 어떻게 될까? 혼자서 문제를 진단하고 알아 내는 것이 과연 가능할까? 자동차의 엔진들은 보통 많은 부품들과 함께 작동되기 때문에 어떤 것들이 어떻게 연결되어 있는지 알아야만 진단이 가능하다. 
 
아쉽게도 소프트웨어도 자동차의 엔진과 굉장히 비슷하다. 많은 엔진들과 기능들이 서로 모듈 별로 연결되어 있기 때문에 어떤 한 모듈이라도 제대로 동작되지 않으면 전체 기능이 동작되지 않게 되는 경우가 많다. 아래 [그림3]의 여러 층으로 설계 된 웹 애플리케이션의 경우를 살펴보자. 
 
 


[그림3] N-레이어 기반 애플리케이션

 
 
[그림3]에서와 같이 여러 개의 버그 포인트를 가지고 있고 여러 개의 의존성을 가지고 있는 환경의 테스트를 통합 테스트(Integration Test)로 불리고 있는데 빌 해트젤(Bill Hetzel)에 의해서 이 용어가 처음 정의 되었다. 
 
댓글을 버튼을 눌렀을 때 데이터 베이스까지의 작업을 하나로 묶어서 통합 테스팅을 진행할 경우에 문제는 무엇일까? 먼저 가장 큰 문제는 우리가 테스트를 자동화 시킨다 한들 실제 버그가 일어날 경우에 어떤 모듈에서 진행되는지 알 수 없다는 것이다. 문제를 조금 더 심화시켜 보자면 여러 모듈을 사용하고 있는데 이 모듈들은 다른 팀들에서 각자 개발되었다고 가정해보자. 이런 환경에서 소프트웨어에 문제가 생길 경우 과연 어떤 팀의 문제인지 쉽게 진단해 낼 수 있을까? 그럼 어느 팀에서 이 문제를 분석해서 알려주어야 할 것인가? 프레젠테이션 팀에서 무조건 이 책임을 짊어져야 하는 것인가? 
 
두 번째는 특정 모듈이 변경되었다면 어떨까? 연관성이 있는 모든 코드들을 다시 테스팅 해야 한다는 것이다. 그런데 더 큰 문제는 테스트를 바로 바로 진행하지 않았기 때문에 지금 변경한 모듈 때문에 실패한 것인지 아니면 이전에 변경한 코드 때문에 실패한 것인지 알 수 없다면 어떨까? 이때 우리는 심증으로 이야기 할 수 밖에 없다. “이 모듈이 동작이 안 되는 것 같은데 한번 봐주실 수 있으신가요?” 라고 물어 볼 수도 있고 “확인해 봤는데요 제 모듈 쪽에서는 문제가 없는 것 가은데요” 라며 대답할 수도 있을 것이다.
 
이러한 통합 테스트의 문제를 우리는 유닛 테스트와 NUnit, MSTest 과 같은 유닛 테스트 프레임워크를 도입함으로 해결 할 수 있다. 유닛 테스트은 말 그대로 하나의 작은 유닛 단위로 테스트 코드를 작성하여 테스트를 자동화 시키는 것을 말한다. 즉, 유닛 테스트는 각 코드 단위 별로 어떤 변경과 문제가 있을 때마다 자동으로 알려주는 것이 가능하기 때문에 더 이상 문제를 찾기 위해서 그렇게 곤욕을 치루지 않아도 된다.
 
한가지 오해하면 안되는 것이 있다면 TDD를 한다고 해서 통합 테스트가 필요 없다는 것이 아니다. 뒷부분에서 다룰 내용이지만 유닛테스트 원칙을 유지하는 부분 때문에 실제 DB나 외부 리소스와 연동한 테스트를 진행하는 부분에서 유닛 테스트를 진행하기 힘든 부분이 있다. 때문에 이런 부분은 반드시 통합 테스트 코드를 작성해야 한다.


저작자 표시
신고
Posted by 박경훈