IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

Extreme Programming : Test-driven 프로그래밍
코드를 작성하기 전에 테스트 먼저 작성하기

Level: Intermediate

Roy W. Miller
소프트웨어 개발자, RoleModel Software, Inc.
2003년 4월 22일

Column iconTest-driven 프로그래밍은 프로그래머들을 당황시키는 XP의 한 측면이다. 우리들은 Test-driven 프로그래밍에 대한 의미와 이것을 다루는 방법에 대해 부정확한 추측을 하고 있다. Roy Miller가 test-driven 프로그래밍을 설명한다.

지난 50년 동안, 테스트란 것은 프로젝트가 끝날 즈음에 행해지는 것으로 인식되었다. 물론, 프로젝트가 진행되는 동안에는 통합 테스트가 있고 모든 코드 작업이 수행된 후에도 통상 테스트는 시작하지 않는다. 하지만 XP의 옹호론자들은 이 모델은 완전히 시대착오적이라고 말한다. 당신이 프로그래머라면 코드를 작성하기 전에 테스트를 만들어야하고 그런다음 테스트가 통과 할 코드를 작성해야한다. 이렇게 하는 것은 시스템을 가능한 단순하게 하는 데 도움이 된다.

테스트 먼저 작성하기
XP는 두 종류의 테스트-프로그래머 테스트(programmer tests)고객 테스트(customer tests)-에 대해서 이야기하고 있다. Test-driven 프로그래밍은 프로그래머 테스트(programmer tests) 또는 단위 테스트(unit tests)가 당신이 작성한 코드를 결정(drive) 하도록 하고있다. 이는 코드를 작성하기 전에 테스트를 갖고있어야 한다는 것을 의미한다. 이 테스트는 어떤 코드를 작성해야하는지를 지시한다. 여러분은 더도 덜도 말고 테스트가 통과시킨 코드만 작성하면 된다. XP 규칙은 간단하다. 프로그래머 테스트가 없다면 어떤 코드를 작성해야 하는지 알 수 없고 따라서 어떤 코드도 작성할 수 없다.

테스트를 작성하는 방법
이 이론은 훌륭하다. 하지만 테스트를 코드도 작성하기 전에 어떻게 작성할 것인가? 우선 Kent Beck의 Test-Driven Development: By Example (참고자료)을 읽기 바란다. 테스트를 작성하는 방법과 그것으로 코드를 결정하는 방법을 다루면서 test-driven 프로그래밍이 장점을 설명하고 있다.

Test-driven vs test-first
나는 "test-first" 보다 "test-driven"이라는 용어가 좋다. 왜냐하면 test-first는 코드를 작성하기 전에 프로그래머 테스트(programmer tests)를 작성하는 방법에 초점을 맞추기 때문이다. 그러한 메커니즘은 중요하지만 실제의 힘은 test-driven에 함축되어 있는 사고와 프로그래밍 습관의 변화에 있다. 좀더 포괄적인 뜻을 지닌 "test-driven 프로그래밍"은 이 모든 종류 모두의 테스트를 포괄하고, XP 팀이 지향하는 test-drive 방식을 말하고있다.

Person 객체를 갖고있는 시스템을 작성한다고 해보자. 나는 각 Person 객체들이 내가 나이를 물으면 대답하도록 명령하고 싶다. 아직 코드를 작성하지 않았지만 지금은 테스트를 작성해야한다. "난 내가 무엇을 테스트 해야하는지도 모르는데요? 어떻게 테스트를 작성할 수 있습니까?" 라는 질문이 들리는 듯 하다. :) 대답은 간단하다. 당신은 무엇을 테스트 해야하는지를 알고 있지만 단지 그러한 방식으로 생각해보지 않았기 때문에 모르는 것이다.

당신에게는 코드가 아직 없다. 하지만 Person 객체가 어떠해야 하는지에 대해서는 개념이 있다. 여기에는 나이를 정수로 리턴하는 메소드가 있어야한다. 나는 주로 자바를 다루기 때문에 프로그래머 테스트(programmer tests)를 작성하기 위해 JUnit을 사용한다. Listing 1은 Person 객체를 위해 작성하고자 하는 JUnit 테스트이다:

Listing 1. Person 객체용 JUnit 테스트

package com.roywmiller.testexample;

import junit.framework.TestCase;

public class TC_Person extends TestCase {

  protected Person person;

  public TC_Person(String name) {
    super(name);
  }

  protected void setUp() throws Exception {
    person = new Person();
  }

  public void testGetAge() {
    int actual = person.getAge();
    assertEquals(0, actual);
  }

  protected void tearDown() throws Exception {
  }

}

우선 JUnit에 익숙하지 않은 사람들을 위해 방법과 구조를 살펴보도록 하자. TestCase 클래스는 가장 많이 사용하게 될 클래스이다. 여러분은 TestCase를 하위분류하는 테스트 클래스를 작성하기만 하면 된다. 일단 테스트 클래스를 갖고있으면 테스트 메소드에 실제 액션이 발생한다. 이들은 test 접두사로 시작한다. 테스트를 실행할 때, JUnit은:

  • 테스트 클래스를 검사하고 "test"로 시작하는 각각의 메소드를 실행한다.
  • 각 테스트 메소드 전에 setUp() 메소드를 실행한다.
  • 각 테스트 메소드 후에 tearDown() 메소드를 실행한다.

이 예제에서 setUp() 메소드는 Person을 인스턴스화 한다. 다시말하면 내가 20개의 테스트 메소드를 가지고 있다면 각각은 새로운 Person 인스턴스로 시작한다는 의미이다. tearDown()에서는 할 일이 없다. 따라서 지금은 비워둔다. setUp()이나 setUp()이 필요하지 않다는 것은 강조할 가치가 있다. 나는 두 번째 또는 세 번째 테스트 메소드를 작성하고 모두가 공유하는 셋업이나 tear down 액티비티가 있다고 결정하기 전까지는 그들을 만들지 않는다.

나는 테스트 메소드에 몇 개의 디자인 결정을 내렸다. 나는 내가 person을 수행할 수 있고 "디폴트" Person 이 나에게 0이란 나이를 되돌려 줄것이라고 생각했다. 또한 Person 객체가 getAge() 메소드를 갖게 될 것이라고 생각했다. 그러한 상상은 지금까지는 나쁘지 않다. 비록 그들이 영원히 지속될 것은 아니지만 말이다. 그러한 생각을 기반으로 Person을 인스턴스화하여 테스트 메소드에서 테스트할 메소드를 호출하고 "assert" 메소드 다발 중에서 하나를 호출했다. 선언 메소드는 어떤 것이 true 인지 테스트한다. 다시말해서 그들은 JUnit이 true 값을 갖고 있다는 것을 선언한다. 표 1은 선언 카테고리 리스트이다:

표1. 선언 카테고리
선언 메소드 설명
assertEquals 두 개의 동일성을 비교한다(프리머티브 또는 객체).
assertTrue boolean을 true로 선언한다.
assertFalse boolean을 false로 선언한다.
assertNull 객체가 null이라는 선언한다.
assertNotNull 객체가 null이 아니라는 것을 선언한다.
assertSame 두 객체가 같은 인스턴스임을 선언한다.
assertNotSame 두 객체가 같은 인스턴스가 아니라는 것을 선언한다.

이 경우 Person 인스턴스가 제공한 나이는 0 임을 확인했다. 0은 새로운 Person 객체 디폴트이다.

물론, 이 테스트는 컴파일 하지 않을 것이다. 그림 1은 이것을 Eclipse에서 실행할 때 JUnit Fast View를 나타내는 모습이다.

그림1. JUnit 컴파일 오류
JUnit compile failure

분명히 Person 클래스를 갖고있지 않아 나의 테스트는 문제의 실행을 하게 되었다. 빨간줄이 생겼다. 만약 모든것이 실행되고 테스트가 통과되었다면 이 바(bar)는 녹색이 될 것이다.

문제될 것 없다. 나는 Person 클래스를 만들것이다:

Listing 2. Person 클래스

package com.roywmiller.testexample;

public class Person {
  
  public int getAge() {
    return 0;
  }

}

테스트를 실행할 때 이것은 통과되어 녹색줄이 될 것이다. getAge()에서 어떤 것을 리턴해야한다. 그렇지 않으면 이것은 컴파일 하지 않을 것이다. 새로운 Person 인스턴스용 디폴트인 0이 흔히 사용되고 이것은 잘 실행된다. 다시 테스트를 통과할 코드를 작성했다.

Person에 디폴트 나이를 부과하는 것은 좋다. 하지만 그것이 시스템을 많이 돕는 것은 아니다. Person은 그 보다 더 현명해야 한다. 나에게 정말로 필요한 것은 생년 월일을 보유하고 있고 오늘을 기점으로 나이를 응답하는 Person이다. 나의 Person 객체가 성장해야할 필요가 있다는 것을 의미한다. 코드 작업에 들어가기에 앞서 testGetAgetestGetDefaultAge로 이름을 바꾸고 나의 테스트 케이스를 위한 또 다른 테스트를 작성했다:

Listing 3. 새로운 테스트 메소드

public void testGetAge() {
  GregorianCalendar calendar = new GregorianCalendar(1971, 3, 23);
  person.setBirthDate(calendar.getTime());
  int actual = person.getAge();
  assertEquals(31, actual);
}

Person에는 setBirthDate() 메소드가 없기 때문에 이 테스트는 컴파일 하지 않는다. 내가 한 개를 만든 후에, Person은 Listing 4와 같다:

Listing 4. 업데이트 된 Person 클래스

package com.roywmiller.testexample;

import java.util.Date;

public class Person {
  
  protected Date birthdate;
  
  public int getAge() {
    return 0;
  }
  
  public void setBirthDate(Date aBirthDate) {
    this.birthdate = aBirthDate;
  }

}

PersongetAge()에 대해 다른 어떤 것도 수행하지 않는다. 따라서 테스트는 실패했다. 그림 2는 JUnit Fast View 이다:

그림2. JUnit 선언 오류
JUnit assertion failure

AssertionFailedError는 결과 값이 31 대신 0을 나타내고 있다. 다른 어떤 것을 수행하도록 메소드를 변경하지 않았기 때문에 이 오류는 예상된 결과이다. 이제 나는 테스트를 통과할 코드를 만들 수 있다. 디폴트 나이인 0을 따라야 하지만 1971년 3월 23일에 태어난 누군가의 나이를 계산해야한다. 어떤 프로그래머들은 (Kent Beck 포함) birthdate가 null 인지를 검사하고 그런다음 계산을 좀더 잘 하는 또 다른 테스트를 작성하는 것 같이 간단한 것을 수행할 것을 권한다. 하지만 이 경우 예제를 간단하게 만들것이기 때문에 내가 원하는 방식으로 나이를 계산함으로서 테스트 통과를 시도할 것이다. Calendar를 사용할 것이다:

Listing 5. getAge() 구현

package com.roywmiller.testexample;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

public class Person {
  
  protected Date birthdate;
  
  public int getAge() {
    if (birthdate == null)
      return 0;
    else {
      int yearToday = 
        Calendar.getInstance().get(Calendar.YEAR);
      
      Calendar calendar = new GregorianCalendar();
      calendar.setTime(birthdate);
      int birthYear =calendar.get(Calendar.YEAR);
      
      return yearToday - birthYear; 
    }
  }
  
  public void setBirthDate(Date aBirthDate) {
    this.birthdate = aBirthDate;
  }

}

테스트를 실행하면서 31이 나올것을 기대하였으나 결과는 32가 되었다. 오류이다. 무엇이 잘못되었는가? 문제는 내가 작성한 코드에 있다는 것을 안다. else 절을 보면 연도만을 기준으로한 나이를 계산했다. 이것은 틀렸다. 나는 현재 31살 이지만 다음달엔 32살이 된다. 알고리즘이 틀린 결과를 냈다. 이 문제를 해결하기 위해서 Listing 6를 사용했다:

Listing 6. 수정된 getAge()

else {
  int yearToday = Calendar.getInstance().get(Calendar.YEAR);
  
  Calendar calendar = new GregorianCalendar();
  calendar.setTime(birthdate);
  int birthYear = calendar.get(Calendar.YEAR);
  
  if (yearToday == birthYear)
    return yearToday - birthYear;
  else
    return yearToday - birthYear - 1;
}

녹색줄이다. Person 클래스에는 코드 중복이 약간 있다. 하지만 나중에 리팩토링을 위해 남겨두겠다.

이 예제는 test-driven 프로그래밍의 정수를 보여준다. 나는 매 단계 마다 테스트를 통과할 코드를 작성했다. 여러분도 코드를 작성하기 전에 테스트를 작성해야 한다는 사고에 익숙해져야 한다 .

테스트를 먼저 작성해야 하는 이유
아마도 테스트 먼저 작성하는 것은 대단한 생각이 아니라고 생각할지도 모르겠다. 너무 낯설거나 불필요하게 보일 수도 있다. 나는 두 번째 이유를 자주 들었다. 대부분 숙련된 프로그래머에게서 그런 소리를 들었다. 이들은 영리하고 많은 경험이 있고 무엇을 하고있는지 알기 때문에 테스트를 작성할 필요가 없다고 말한다. 테스트를 먼저 작성해야하는 세 가지 이유가 있다:

  • 학습(Learning)
  • 변수 발생(Emergence)
  • 확신(Confidence)

테스트 먼저 작성하는 것은 학습에 있어 좋은 방법이다. 이것은 작성하고 있는 것에 대한 인터페이스에 초점을 맞추도록 도와준다. 테스트를 작성할 때 사용하고 있는 클래스가 이미 존재하고 있는 것처럼 가장할 수 있다. 그런다음 나머지 시스템에서 원하는 방식으로 그 클래스를 사용하는 것이다. 나중에 이 클래스를 사용하는 방법을 잊으면 테스트를 보고 구체적인 예제를 볼 수 있다.

테스트 먼저 작성하기의 가장 흥미로운 점은 변수 발생이 가능하도록 한다는데 있다. 만들고 있는 시스템은 성장해야 한다. XP는 전체 작업을 설계하지 않는다. 테스트 먼저 작성하고 그들을 통과시킬 때 여러분은 코드가 원하는 것을 수행하도록 할 수 있다. 단지 코딩만을 시작한다면 단순히 생각을 구현하게 될 것이다. 그러한 결정을 미루면 미룰수록 시스템을 보다 좋게 만들 수있는 새로운 방향을 발견할 기회는 많아진다.

테스트를 먼저 작성할 때 환상적인 로직 범위를 갖게된다. 나는 코드를 통해서는 모든 경로를 다룰 수 없다. 하지만 테스트를 통해 많은 것을 다루게 될 것이다. 대부분의 사전 XP 프로젝트보다 나은 테스트 슈트를 갖고 있다. 나는 그러한 테스트를 실행할 수 있다. 단지 하나의 버튼을 누름으로서. 몇 초 후에 내가 명령한 대로 코드가 실행되는 지를 알수있다. 팀의 모든 사람들은 언제나 코드를 변경할 수 있다. 심지어 배포일 전까지도. 이는 나에게 프로그래머로서 확신을 준다.

Extreme Programming 칼럼 피드백!
이 칼럼에 대한 여러분의 피드백을 기다립니다. XP가 무엇인가 라는 큰 문제 부터 어리석거나, 불가능한 질문까지도 환영합니다.

테스트를 먼저 작성하지 않는 이유

테스트를 먼저 작성하려면 훈련이 필요하다. 테스트를 작성하는 것이 쉬운일이 아니다. 그렇다고 해서 테스트를 작성하지 않는다면 결국 테스트를 거치지 않은 코드를 갖게 된다. 다음 시스템 기능을 코딩할 때 어떤 부분이 올바르게 작동하지 않는다면 어디서 문제를 찾을 것인가? 테스트 없이 그 질문에 대한 답을 확실하게 말해 줄 수 없다. 모든 것이 잘 돌아가는 것처럼 보일지라도 지금 당장은 나타나지 않지만 시스템 상의 어떤 것이 잘못되 있을 것이 분명하다. 테스트 없는 디버깅도 마찬가지이다.

테스팅 툴과 기술
거의 모든 언어에는 xUnit 라이브러리가 있다. JUnit은 자바 플랫폼용 작업을 잘 수행한다. 나는 개인적으로 Eclipse IDE (참고자료)를 사용하고 있다. Eclipse는 오픈 소스이고 실행이 가능한 테스트 슈트가 있다. 이를 토대로 많은 테스트를 만들 수 있다. 사용할 수 있는 기술로는 ObjectMother 패턴, Mock 객체, Sham 객체 등이 있다.

ObjectMother 패턴
ObjectMother 패턴은 Gang of Four Abstract Factory 패턴의 실제 구현이다. (참고자료). 이것은 테스팅에 필요한 인스턴스를 주기위해 팩토리 객체를 만들도록 명령한다:

Listing 7. ObjectMother 예제

Seminar seminar = TF_Seminar.newFullyLoaded;
seminar.doSomething();
assertEquals("expectedValue", seminar.getValue());

Mock 객체
Mock 객체는 테스팅 용 객체를 배치할 수 있도록 한다 (참고자료). Mock 객체에게 실제 컴포넌트가 갖기 원하는 인터페이스를 준다. 그런다음 실제 컴포넌트가 형성될 때 까지 mock을 사용한다. mock은 존재하지 않는 컴포넌트에게는 stub 그 이상이다. 코드가 mock와 어떻게 인터랙팅하는지를 평가할 수 있다. Mock 객체는 많이 알려져있지만 과용되고 있는 것 같다.

Sham 객체
가끔씩은 같은 인터페이스를 구현하고 테스트에서 이것과 어떻게 인터랙팅하는 지에 대한 특정 문제에 대해 대답할 수 있는 fake 객체가 필요할 때가 있다. sham 객체가 그렇다. sham 객체는 당신이 필요로하는 것이 무엇이든 될 수 있다:

Listing 8. Person용 sham

protected class ShamPerson extends Person {
  protected boolean getAgeWasCalled;
  
  public int getAge() {
    getAgeWasCalled = true;
    return 25;
  }
}

프로그래밍 혁명
코드를 작성하기 전에 테스트를 작성하는 것은 프로그래머로서의 내 삶을 혁신시켰다. 당신에게도 마찬가지로 적용될 것이다. 내 코드는 언제나 단순하고 깨끗하고 강력하다. 코드를 작성하기 전에 코드를 테스트하는 방법을 생각하는 훈련만으로도 상황이 더 나아진다. 모든 소프트웨어 개발팀이 XP의 많은 관습을 거절하고 테스트 우선 작성한다면 소프트웨어 개발 세계는 엄청 나아질 것이다.

참고자료

목 차:
테스트 먼저 작성하기
테스트를 작성하는 방법
테스트를 먼저 작성해야 하는 이유
테스트를 먼저 작성하지 않는 이유
테스팅 툴과 기술
프로그래밍 혁명
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Demystifying Extreme Programming series
Incremental development with Ant and JUnit
AspectJ와 mock 객체를 이용한 유연한 테스팅
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Roy W. Miller는 소프트웨어 개발자이자 기술 컨설턴트이다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의