TDD vs 계약기반 테스트

단위 테스트를 어떻게 해야 잘 하나…?

단위 테스트를 어떻게 해야 잘 하는지는 사실 논란의 여지가 굉장히 많습니다. 왜냐하면

  • TDD를 해본 사람 vs 안해본 사람
  • TDD가 능숙한 사람 vs 능숙하지 않은 사람
  • 개발 툴에서 TDD를 제공하는 환경 vs 제공하지 않는 환경
  • 테스트 지식이 있는 사람 vs 없는 사람

어쨌든 목표는 동일합니다. 작성된 코드에 대한 테스트를 수행하는 것이죠. 일반적으로 테스트의 목표에 따라 테스트 방법이 달라질 수 있지만, 흔히 단위 테스트는 기능 범위에 한하여 테스트 코드를 작성하는 방법입니다. 어떻게 코드가 변경이 되든, 어떤 데이터소스 이든지 간에 성공해야 할 테스트는 반드시 성공하고, 실패해야 할 테스트는 반드시 실패해야만 기능이 올바르게 동작한다는 것을 최소한으로 보장할 수 있다는 것입니다.    

무엇이 단위 테스트를 어렵게 하나?

단위 테스트는 어떻게든 하는 것은 매우 쉽지만, 어떻게 해야 잘 할 수 있고, 시간을 단축하는 방법도 매우 중요합니다. 맹목적인 단위 테스트는 테스트의 목표를 상실하게 만들 수 있고, 의미 없는 더미(Dummy) 테스트 코드가 되기가 매우 쉽습니다. 그리고 더미(Dummy) 단위 테스트 코드를 양산하는 방법도 매우 쉽습니다.

  • 테스트에 대한 지식이 없는 경우
  • 처음부터 목적/목표가 없는 테스트 코드 작성
  • 테스트 방향이 잘못 설정된 경우
  • 단위 테스트 관리 방안이 없는 경우
  • 처음부터 잘못된 디자인이나 구현 도중 발생하는 디자인 결함

그리고 일반적으로 인간의 생각의 흐름은 매우 순차적입니다. 복잡한 생각 등을 결국은 순차적으로 나열을 하게 되지요. 그 대표적인 것이 자신의 머리 속에만 존재하던 사건을 도식화하는 것이 바로 육하원칙(5W1H) 입니다. 이런 육하원칙과 같이 테스트 대상을 명확하게 테스트 코드로 구현하는 것도 매우 중요한 부분입니다.

  • Who - 누가
  • When - 언제
  • Where - 어디서
  • What - 무엇을
  • Why - 왜
  • How - 어떻게

그리고 마지막으로 단위 테스트를 하는 것 중 "무엇을 테스트해야 하나?" 입니다. 테스트 대상을 어떻게 쪼개어 테스트를 해야 가장 생산적이고, 중복되지 않는 테스트를 작성하는가는 즉시 테스트의 관리 비용과도 영향을 미치는 중요한 부분이기도 합니다. 즉, 테스트 코드가 중복이 되면, 관리되는 테스트 코드의 양이 늘어나고, 그 테스트 결과가 반드시 성공/실패해야 함에도 그렇지 않게 될 수 도 있습니다.

흔히 열광하는 TDD(Test-Driven Development) 도 마찬가지입니다. 이론적으로 그 용이성은 필자도 인정하지만, 사실은 매우 실용적이지도 않고, 효과적이지도 않습니다. 더불어 위의 나열한 테스트 지식, 목표, 테스트 방향, 관리 방안, 디자인 측면에서도 명확하지 않는다면 쥐약과도 다름이 없습니다. 즉, 테스트라는 것에 집중되어 그 목표, 방향을 제시하기 힘들며, 객체지향적인 코드를 만들기도 어려우며, 실제로 지저분한 코드를 만들기 매우 쉬운 방법이기도 합니다.

즉, 인간의 생각은 매우 복잡한데, 순차적인 도식화 작업을 하지 않은 채로 순차화를 시키려 하니 테스트 코드의 품질과 소스 코드의 품질은 기대 이하가 대부분이었습니다.    

계약 기반의 테스트

최근에는 계약 기반(Contract-Based)의 아키텍처 또는 패턴의 개발이 매우 활발합니다. 코드적으로 추상화를 통해 매우 명확하며, 이런 추상화된 모형을 통해 완벽하게 모형의 인터페이스를 통해 시그너처를 구현해 나가는 방식입니다. 정확하게 계약 기반의 개발도 아니며, TDD, BDD(Behavior-Driven Development)도 아닌 그 중간적인 형태를 취하는 방법입니다.

계약 기반(Contract-Based)의 개발 방식은 이전에 자주 세미나에 언급을 하였습니다. 자동차보험의 경우 계약서와 아래와 같은 계약 명세가 존재 합니다. 이 명세가 바로 인터페이스의 모형과 시그너처에 해당한다고 봐도 무방합니다.

아래는 자동차보험의 계약 명세에 대한 상세적인 약관입니다. 이 약관은 계약 명세에 대해 자세하게 약관을 명시하고 있으며, 이것을 인터페이스의 구현이라고 보셔도 무방합니다.

즉, 인터페이스의 모형과 시그너처는 단순히 객체지향적인 인터페이스를 넘어, 계약(Contract)적인 관점의 신뢰를 이루기도 합니다. 바로 그 계약을 잘 설명했던 TechDays 2010 Spring 의 슬라이드를 몇 장 재활용해보면^^

 

계약 기반의 개발과 테스트의 장점

물론 이 계약 기반의 개발과 테스트의 장점이 없다면, 필자 또한 이렇게 침을 튀기며 얘기하지는 않을 겁니다. 이전에 얘기했듯이 테스트는 하면 할수록 어려워지고, 또한 관리하기도 무척 버거워집니다. 그 과정에 의지와 다르게 테스트의 목적과 목표가 소실되고, 테스트의 방향성을 상실하는 경우가 허다합니다. 그리고 잘못된 초기 디자인이 특히 돌이키기 힘든 어려움이 될 수 있습니다.

사실 어떤 기가 막힌 테스트 기법도 명세(인터페이스의 모형, 시그너처)가 변경이 되면 많은 노가다(?)를 감수해야 할 수 밖에 없습니다. 바로 리팩토링(Refactoring) 작업인데, 좀 더 나은 코드/디자인을 위한 것이긴 하지만, 너무 많은 리소스나 비용이 필요하다면 이 리팩토링 작업이 전혀 즐겁지 않는 작업이 되기도 합니다.

하지만 절대 변하지 않는 진리 중 하나는, 쪼개진 것을 합치는 것은 쉬워도, 합쳐 놓은 것을 쪼개기는 매우 어렵기 때문입니다.

각설하고, 계약 기반 테스트가 TDD보다 좀 더 효과적인 이유는

  • 허술하게 하는 TDD는 테스트 목적/목표가 불명확한데 테스트 코드를 먼저 짠다?
  • 보험의 약관을 가지고 명세를 만들어 간다? 무지 어렵겠는걸…?
  • 디자인 목표가 없는 추상화가 과연 올바른 추상화인가…?
  • 코드는 생각을 먼저 하고 짜는 것이 방어적/효과적/객체지향적인데, 타이핑 먼저…?

위의 나열한 몇 가지 이유만으로, 필자는 TDD를 싫어하는 이유이기도 하지만 특히 싫은 이유는 리팩토링부터 시작하는 코드는 죽어라 리팩토링으로 끝나기 마련입니다. (리팩토링을 반드시 그 자체의 의미에 두고 하는 얘기는 아닙니다) 뒤돌아서면 내심 찜찜한 기분도 들고, 좀 더 완벽함에 가깝게 디자인하려 하지 않은 것이 어찌 완벽함에 가까워지겠습니까.

물론 어떤 테스트 기법이든지 "경우에 따라 잘 맞을 때가 있고 안맞을 때"가 있습니다. 다만, 이런 TDD 기법 하나로 찬양하다시피 어떠한 올바른 방향을 제시하지 않는 것은 매우 위험한 발상이기도 합니다. 필자는 TDD 의 울타리에서 벗어나야만 올바른 TDD 를 할 수 있다고 생각을 하며 이만^^

Posted by 땡초 POWERUMC

댓글을 달아 주세요

목차

[Testing] TDD (Test-Driven Development)-테스트 주도 개발

[Testing] BDD (Behavior-Driven Development)–행위 주도 개발

[Testing] Moq.NET (T/B Driven Development)

 



Moq.NET

Moq 는 "Mock-you" 또는 "Mock" 로 부른다고 합니다. Moq.NET 3.0 은 C# 3.0 과 .NET Framework 3.5 를 통해 Linq Expression Tree 와 Lambda Expression(람대 표현식) 으로 직관적이고 생산적이라고 합니다.

이전에 봤던 웹 사이트 로그인 사용자 스토리를 다시 봅시다. 단, 이 예제에서는 복잡성을 만족하는 항목을 삭제합니다.

웹 사이트의 로그인 사용자 스토리

  • 사용자 아이디는 영문만 입력 가능하고 한글은 입력할 수 없다
  • 사용자 아이디는 최소 3자리, 최대 10자리까지 입력가능하고 초과시 경고 메시지를 보여준다
  • 사용자 비밀번호는 최소 5자리, 최대 20자리까지 입력 가능하고 초과시 경고 메시지를 보여준다

   

위의 사용자 스토리를 Mocking Framework 인 Moq.NET 을 이용하여 TDD와 BDD 형태로 테스트를 작성해 나갈 수 있습니다.

아래의 소스 코드는 Login 인터페이스를 정의한 코드입니다.

public interface ILogin
{

LoginResult Login(string id, string password);

LoginInputResult Valide(string id, string password);

}

   

public enum LoginResult

{

Authentication,

NoAuthentication

}

   

public enum LoginInputResult

{

Success,

MinLengthId,

MaxLengthId,

MinLengthPassword,

MaxLengthPassword

}

   

이제 Login 의 Behavior(행위) 측면에서 주도적인 개발을 하고자 합니다. BDD 이용하여 [그림1] 의 Clean up code 를 얻기 위한 목적이 아닙니다. 좀 더 TDD 에 가깝고, 디자인과 설계에 초점을 맞추고자 합니다.

class Program
{

static void Main(string[] args)

{

var id = "powerumc";

var pwd = "aaaa";

var mock = new Mock<ILogin>();

}

   

private static string minString(int length)

{

return Match<string>.Create( o => o.Length <= length );

}

   

private static string maxString(int length)

{

return Match<string>.Create( o => o.Length >= length );

}

}

   

위의 코드를 통해 Mocking Object 를 생성하도록 Mock<T> 생성자를 볼 수 있습니다. 바로 Mock<T> 를 통해 Mocking Object 를 생성합니다.

아래의 코드는 위의 로그인 사용자 스토리에서 입력되는 아이디/비밀번호의 유효성을 검사하도록 Mocking Object 를 설정합니다.

// Validate

mock.Setup( o => o.Valide(minString(3), It.IsAny<string>()))

.Returns(LoginInputResult.MinLengthId)

.Callback(()=>Console.WriteLine("ERROR> MinLengthId"));

mock.Setup( o => o.Valide(maxString(10), It.IsAny<string>()))

.Returns(LoginInputResult.MaxLengthId)

.Callback(()=>Console.WriteLine("ERROR> MaxLengthId"));

mock.Setup( o => o.Valide(It.IsAny<string>(), minString(3)))

.Returns(LoginInputResult.MinLengthPassword)

.Callback(()=>Console.WriteLine("ERROR> MinLengthPassword"));

mock.Setup( o => o.Valide(It.IsAny<string>(), maxString(20)))

.Returns(LoginInputResult.MaxLengthPassword)

.Callback(()=>Console.WriteLine("ERROR> MaxLengthPassword"));

   

아래의 코드는 유효성을 통과한 아이디/비밀번호로 로그인을 시도하고 결과값을 리턴하는 Mocking Object 입니다.

// Login

mock.Setup( o => o.Login(It.IsAny<string>(), It.IsAny<string>()))

.Returns(LoginResult.NoAuthentication)

.Callback(()=>Console.WriteLine("No Authentication"));

mock.Setup( o => o.Login(id, pwd))

.Returns(LoginResult.Authentication)

.Callback(()=>Console.WriteLine("Success Login"));

   

자, 그럼 가상의 Mocking Object 를 통해 인터페이스만으로 로그인 시도를 해보도록 하겠습니다.

var obj = mock.Object;

var loginInputResult = obj.Valide(id,pwd);

   

if( loginInputResult != LoginInputResult.Success ) return;

   

obj.Login(id,pwd);

   

입력 변수 값에 따라 비록 Mocking Object 지만, 테스트 시나리오를 충분히 검증할 수 있습니다.

Id="aaa", pwd="aaa" 일 경우는 아래와 같은 결과가 나타나겠죠~?

 

Id="powerumc", pwd="aaaa" 일 경우는 아래와 같이 모든 테스트 시나리오를 통과한 결과입니다.

 

그럼, Microsoft Research 프로젝트의 일환인 Pex 를 이용하여 Mocking Object 를 Testing 해보겠습니다. Pex 테스트 프로젝트를 생성하고 아래의 PexMethod 를 만들었습니다.

아래는 테스트 결과입니다. 총 31개의 테스트 케이스 중에 통과되는 1개의 테스트를 확인할 수 있습니다.

자 어떻습니다~? 전혀 구현 코드를 구현하지 않았음에도 인터페이스를 통해 TDD-테스트 주도 개발이 가능합니다. BDD-행위 주도 개발을 통해 번거로운 TDD 의 Red, Green, Refector 의 유한반복 사이클 없이도, 실제 인터페이스의 디자인과 설계에 초점을 맞추어 테스트를 진행하였습니다.

단순히 BDD 가 최고야~ 라는 말은 아닙니다. 우리가 BDD 를 통해 TDD 를 좀 더 쉽고, 근접하게 접근할 수 있습니다. 그리고 코드의 구현이 전혀 없이 오직 디자인과 설계에 초점이 맞추어져 있다는 사실을 알면 됩니다. 바로 Moq.NET 은 Mocking Object 를 통해 TDD 와 BDD 개발을 통해 좀 더 품질 좋은 WhiteBox Testing 의 기대효과를 누릴 수 있습니다.

데이터베이스의 데이터를 조회하여 테스트한다고 할 때에도, 반드시 데이터베이스가 존재하지 않아도 됩니다. 가상의 데이터베이스 커넥션 객체를 만들어서 쓰면 됩니다. 쇼핑몰 사이트에서 결재 프로세스를 테스트해야 하나요? 그렇다면 가상의 객체를 통해 설계된 결재 프로세스가 합당한지 Mocking Object 를 통해 테스트를 하면 됩니다. 그 이후에는 잘 설계된 인터페이스를 구현하기만 하면 되겠죠?

   

이 외에도 WikiPedia 에 Mock Library 의 종류가 있네요. 아무튼 테스트와 Moq.NET 에 대한 내용은 여기까지 하는 것으로 마치도록 하렵니다.

 

참고 문헌

http://en.wikipedia.org/wiki/Mock-object
http://behaviour-driven.org/

Posted by 땡초 POWERUMC

댓글을 달아 주세요

 

목차

[Testing] TDD (Test-Driven Development)-테스트 주도 개발

[Testing] BDD (Behavior-Driven Development)–행위 주도 개발

[Testing] Moq.NET (T/B Driven Development)

이번에 Moq.NET 3.0 버전이 릴리즈 되었습니다. Moq.NET 는 Mocking Object 를 통해 특정 테스트를 진행하고 훨씬 TDD 기반에 근접한 테스팅을 가능하게 합니다. 즉, Mocking Object 는 실제 클래스나 개발이 완료되지 않는 시점에서부터 테스트를 가능하도록 합니다.

그런데 필자는 Moq.NET 를 이해하는 과정에서 내가 알고 있던 것보다 더 깊은 배경이 있었다는 것을 알게 되었습니다. 예를 들어, TDD 외에 BDD 또한 애자일 개발 방법론에 포함된다는 사실과, 조금은 낯설은 BDD-행위 주도 개발을 직접 체험해 보는 과정에서 말입니다. ^^ 

   

왜 TDD (Test-Driven Development) ?

TDD 가 좋으면서 쓰지 않는 이유는 뭘까요? 일반적으로 완벽한 TDD 를 수행하는 과정은 매우 힘듭니다. TDD 에 대한 이론을 들었을때는 확 가슴에 와닿지만, 이것을 몸소 체험하는 과정에서 개발자의 인내력의 한계를 올려놨다 내려놨다 하는 극한을 체험하게 합니다^^; 그러므로 일반적인 TDD 사이클인 Red, Green, Refactor 는 사람을 금방 지치게 만들죠^^; 한 사이클을 마치기 위해서는 많은 시간이 투자되어야 한다는 겁니다.

[그림1] TDD Process

하지만 우리가 TDD 를 수행하는 목적은 이러한 과정에서 코드에 대한 신뢰도를 향상시키고 품질을 향상시키고자 하는 것입니다. 그렇기 때문에 하고자 하는 목적이 보다 나은 품질을 보장하기 위해서라면 TDD 가 필요하다는 것이 머릿속으로만 느낌이 팍팍 옵니다^^;

이제 슬슬 스스로에게 딜레마다 옵니다. 좋은 줄은 알지만 누군가 나에게 TDD 를 강요한다면 아마도 전 "Oh~ No!" 라고 할 것 같네요 -_-;

 

WhiteBox Testing & BlackBox Testing

Moq 를 이야기 하기도 전에 정말 딴소리를 많이 하네요. 일반적으로 테스팅은 크게 WhiteBox Testing 과 BlackBox Testing 으로 구분할 수 있습니다. 이 두 가지 테스팅의 차이는 좁은 범위에서 내부적인 프로세스를 아느냐 모르느냐의 차이고요, 넓은 범위에서는 테스트 레벨의 차이라고 보시면 됩니다.

다음의 그림을 보면 좀 더 이해가 쉬울 겁니다.

[그림2] BlackBox Test 와 WhiteBox Test

즉 BlackBox Test 와 WhiteBox 테스트는 그 목표의 설정이 다르게 됩니다.

BlackBox Test 는 로그인 기능에 대해 요구사항이 있고, 그 기능이 반드시 가져야 할 스팩이 있을 것입니다. 요구사항을 통과하기 위해서는 로그인 기능의 스펙을 만족하면 되고, 실제 단위 테스트 등으로 그런 케이스를 통과를 하면 됩니다. 즉, 내부적으로 어떻게 동작하는지는 알아야 할 필요가 없으므로 수십/수백가지의 테스트 케이스를 통과하여 기능이 정상적으로 동작하는 것을 보장하기 위한 목표입니다.

WhiteBox Test 는 해당 기능에 대해 내부적인 구조를 기반으로 테스트를 진행하게 됩니다. 테스터는 이미 위의 로그인 기능이 내부적으로 어떤 프로세스로 진행되는지 알고 있으며, 예측 가능한 테스트 시나리오를 작성하여 테스트를 진행하게 됩니다. 이 테스트의 목적은 테스트 케이스를 통과하는 BlackBox Test 를 한 단계 뛰어넘어 잠재적인 오류까지 테스트를 통해 잡아내는 것이 목표입니다.
즉, 코드상의 Memory Leak 이나 SQL Injection, Stack Overflow 등 잠재적인 오류를 미리 찾는 경우도 있습니다. 이미 이런 기능은 Visual Studio 2005 이상부터 지원되는 정적 코드 분석(Static Code Analysis) 가 제공이 됩니다. 또, WhiteBox Test 는 코드 커버리지(Code Coverage) 의 수치를 높임으로써 테스트가 안된 코드의 양을 최소화 시키고, 궁극적으로 소프트웨어의 품질을 향상시키는데 있습니다.(Software QA)

Posted by 땡초 POWERUMC

댓글을 달아 주세요