Test-Driven Development

린(Lean) 소프트웨어 개발론의 핵심 철학 중 하나는 “결함은 발견 즉시 해결”이다. 린 개발은 이것의 실천법으로 테스트 주도 개발(Test-Driven Development, TDD)을 제시한다. TDD 의 개념과 방법론 그리고 한계에 대해 알아보자.

TDD (Test-Driven Development) 란?

TDD는 반복 테스트을 이용한  소프트웨어 개발법이다. 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 소프트웨어를 구현한다.

TDD의 목표는 작동하는 깔끔한 코드 “Clean code that works” 이다. 이를 위해 두 가지 원칙을 제시한다 .

  • 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
  • 중복을 제거한다.

린에서는 불필요한 기능 구현을 가장 큰 낭비로 여긴다. 이는 코드베이스의 복잡성을 높이고, 유지 보수를 어렵게 하여 높은 개발 비용을 초래한다. TDD도 테스트를 통과시키는 코드만을 작성함으로써 맥락을 같이한다.

요구사항 분석 -> 설계 -> 개발 -> 테스트 -> 배포 개발법엔 소프트웨어 개발을 느리게 하는 잠재적 위험이 존재한다.

  • 소비자의 요구사항이 처음부터 명확하지 않을 수 있다.
  • 따라서 처음부터 완벽한 설계는 어렵다.
  • 설계 -> 개발을 진행하면, 실제 코드와 설계간 갈등이 생길 수 있다.
  • 한 곳의 수정이 다른 곳에 미치는 영향을 확인 및 다른 기능의 정상 동작을 보장하기 어렵다.

TDD는 테스트 케이스를 생성한다. 테스트 케이스는 자동화된 테스트 도구로 이용되어, 코드 변경시 기존 기능이 제대로 동작하는지 쉽게 확인할 수 있고 정상 동작을 보장한다. 또한 TDD는 리펙토링을 개발 프로세스에 포함시켜 ‘변경’이라는 소프트웨어의 특성을 반영한다.

애자일 개발론의 Robert C. Martin은 다음의 TDD 원칙을 제시한다.

  • 실패하는 테스트를 작성하기 전에는 절대로 제품 코드를 작성하지 않는다.
  • 실패하는 테스트 코드를 한 번에 하나 이상 작성하지 않는다.
  • 현재 실패하고 있는 테스트를 통과하기에 충분한 정도를 넘어서는 제품 코드를 작성하지 않는다.

TDD 개발법

TDD는 아래 단계의 반복으로 진행된다.

  1. 빨강 : 실패하는 작은 테스트 케이스를 작성한다. 처음에는 컴파일조차 안될 수 있다.
  2. 초록 : 테스트를 통과하는 코드를 작성한다.
  3. 리펙터링 : 테스트를 통과하기 위해 만든 코드의 모든 중복을 제거하고, 불명확한 것을 명확히 한다.

이러한 단계로 인해 TDD는  “업무 코드 작성 전에 테스트 코드를 먼저 만드는 것”으로 정의되기도 한다. 이해를 위해 간단한 sum 함수를 예제로 살펴보자.

  • 작성 메소드 이름 : sum
  • 기능 구현에 필요한 재료 (argument) : int a , int b
  • 반환 값의 타입 : int
  • 정상 동작 만족 조건 (작업 종료 조건) : a와 b 를 더한 값을 결과로 돌려줌

간단히 작성된 sum 함수의 테스트 조건이다. 꼭 위와 같은 형식이 아니라 하나의 문장이어도 된다. 단 테스트는 너무 크지 않은 규모여야 하며 테스트 통과 조건(작업 종료 조건)이 반드시 있어야 한다.

아래 예시 코드를 보자. (제품 코드 관리를 위해 테스트 클래스는 제품 클래스와 구분되어어 쌍을 이루는 것이 일반적이지만, 간단한 예시를 위해 하나의 클래스 안에 테스트 메소드와 제품 메소드를 같이 두었다.)


-(void)testMain{ // 테스트 메소드

    NSLog(@"True or False : %d",[self sumWithIntegers:10 :20] == 30);
    NSLog(@"True or False : %d",[self sumWithIntegers:1 :2] == 3);
    NSLog(@"True or False : %d",[self sumWithIntegers:-10 :20] == 10);
    NSLog(@"True or False : %d",[self sumWithIntegers:0 :0] == 0);

}
-(int)sumWithInteger:(int)a :(int)b{ // 제품 메소드
    return 0;
}

먼저 테스트 메소드를 작성한다. 메소드 안에는 테스트 성공 여부를 확인할 수 있는 코드가 있다. 위 예제는 로그에 모두 True 가 나오면 테스트 케이스가 통과되었다고 가정한다.

제품 메소드보다 테스트 메소드를 먼저 작성하므로 sumWithIntegers: 메소드를 찾을 수 없다는 컴파일 에러가 발생한다. 의아해 할 필요없다. 의도한 바다. 테스트 코드를 작성하면서 자연스럽게 제품 코드의 클래스 이름 및 메소드 이름 등의 설계 요소를 고민하게 된다.

다음 단계로 테스트 케이스의 결과를 ‘통과’로 만든다. 컴파일 오류 해결을 위해 sumWithIntegers: 메소드를 정의한다. 그리고 메소드 안에 테스트를 만족시키는 코드를 작성한다. (예시에서는 단순히 0을 리턴하였다. 제품 코드를 먼저 작성하지 않음을 보여주기 위함이다.)

테스트가 통과되면 테스트 수행에 사용된 테스트 클래스와 제품 클래스 모두 리펙토링한다. 필드 이름, 중복 제거, 가독성 향상 등 간결한 코드를 유지하여 변경에 유연한 코드를 만들도록 노력한다. 리펙터링은 이전의 테스트 케이스를 모두 통과시켜야만 한다. 개발 단계에서  테스트 케이스가 누적되어 유지되므로 이전 기능들에 대한 정상 동작 여부가 보장된다. (위 예시는 지극히 간단하여 리펙토링 단계 없음)

개인적으로 다음의 역량이 TDD 개발에 있어 개발자에게 중요할 것 같다.

  • 테스트 케이스 작성 능력 : 테스트 케이스 자체가 의미있지 않으면 개발된 코드를 신뢰할 수 없다.
  • API 숙련도 : 테스트 케이스를 먼저 작성하면 컴파일 조차 되지 않아 개발 도구의 자동 완성 기능의 도움을 받지 못할 수 있다.
  • 설계 및 리펙토링 능력 : 매 단계마다 상황에 맞는 설계적 결정을 해야 한다. 전체 SW 구조에 대한 직관을 가질 필요가 있다.

테스트 케이스가 의미있으려면 테스트 케이스는 정상적으로 끝까지 수행되어야 한다. 테스트의 결과 중 개발자가 의도 하지 않은 문제 – 예상치 못한 Exception 등 – 는 실패가 아닌 오류로 기록된다. 테스트 케이스가 정상적으로 끝까지 진행되어 오류가 아닌 실패로 기록되도록 하자.

테스트 클래스를 생성/사용할 때는 제품코드와 ‘소스 코드는 다르게, 패키지는 동일, 컴파일된 클래스는 다른 곳으로’ 설정하는 방법이 가장 많이 쓰인다. 폴더만으로 제품 코드와 분리하면 테스트만을 위한 라이브러리를 구별하기가 어려워진다.

일반적으로 TDD에는 Unit Test Framework이 많이 사용된다. iOS 개발 도구 Xcode에는 OCUnit Test Framework이 내장되어 있다.

TDD 장점

  • 개발자의 방향을 잃지 않게 유지: 현재 자신의 개발 내용 및 진척 상황을 항상 살펴볼 수 있다.
  • 품질 높은 소프트웨어 모듈 보유 : 간결한 코드 유지 가능
  • 자동화된 단위 테스트 케이스 소유: 개발자가 필요한 시점에 언제든 수행할 수 있으며 시스템의 이상 유무를 바로 확인할 수 있다.
  • 사용설명서 & 의사 소통의 수단:  작성된 테스트 케이스는 제품 코드 사용 설명서이자 동시에 다른 개발자와 소통하는 커뮤니케이션 통로가 된다. (제품 코드 API 사용 예시가 된다.)
  • 설계 개선 : 테스트 케이스 작성 시 개발에 포함된 다양한 설계요소들에 대해 고민하게 된다. 흔히 테스트하기 어렵다고 생각되는 코드들은 객체 설계 원리 중 기본에 해당하는 원칙들이 잘못 적용됐거나 충분히 고려되지 않았을 가능성이 높다. TDD 진행하면서 테스트가 가능하도록 설계 구조를 고민하다 보면 자연스럽게 디자인을 개선하게 된다.
  • 보다 자주 성공한다 : TDD 는 주기를 짧게 설정하도록 권한다. 개발자는 성취감을 자주 느낄 수 있다.

TDD 의 한계

TDD 선구자인 Kent Beck 은 다음 두 가지를  TDD가 현재 접근하기 어려운 분야라고 말한다.

  • 동시성(Concurrency)
  • 보안 등의 비기능적 요소

이 외에 국내 TDD 도서 저자인 채수원씨는 본인의 도서에서 아래와 같이 추가적인 어려움들을 덧붙였다. 불가능하진 않지만 쉽지 않다는 의미로 받아들이면 될 것 같다.

  • 접근 제한자 메소드 : 현재는 public 메소드만 테스트해도 무방하다는 경향이 대세로 보이지만, 필요에 따라 public 으로 테스트 후 접근 제한자를 수정하는 방법도 있다.
  • GUI : 뷰 레이어와 로직 레이어를 확실히 분리시켜 설계할 필요가 있다.
  • 의존성 모듈 테스트 (target = A but A -> B) : 타겟 A 테스트를 위해 B 도 필요하다. 그러나  B가 아직 준비가 되어 있지 않다면 테스트가 이루어질 수 없다. 보통 이런 경우 B의 Mock객체를 생성하여 B는 문제없음을 가정하고 테스트 한다.

* Updated 9/14 , 접근제한자 메소드 한계 : TDD 단계를 준수하면  Private 메소드는 빨강 – 초록 단계를 지나 리펙토링 단계에서 생성된다. Private 메소드는 새로운 테스트 케이스가 필요한 것이 아니라 이미 통과한 테스트의 리펙토링 결과다. 별도로 접근제한자를 변경해서 테스트할 필요가 없다. 그렇게 되도록 TDD 개발 단계를 준수하자.

* Reference

Advertisements
Leave a comment

3 Comments

  1. 좋은 잘 감사합니다! TDD관련 자료를 찾아봤는데, 쉽고 간결하게 정리된 자료를 찾지 못하다가 이렇게 찾으니 기쁩니다. 블로그 팔로우 했습니다.!

    Reply
  1. Xcode4 단위 테스트 프레임웍(OCUnit Test Framework) 사용하기 « Park's Park
  2. [북리뷰] Test-Driven iOS Development « Park's Park

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: