본문 바로가기

Dev.../웹서비스

[펌] Test Driven Development & Eclipse - III

3. TDD(Test Driven Development) 적용 예

 

프로그램을 개발(코딩)할 때 개발자들이 어떤 작업에서 가장 많은 시간을 소비할까? 개발 환경 구성, 프로그램의 구상, 소스코드의 타이핑, 테스트 및 디버깅 등과 같은 여러 작업이 있는데 이중에서 필자의 경우 테스트 및 디버깅에 가장 많은 시간을 소비하는 것 같다. 하지만 이 테스트 및 디버깅 과정은 필자가 가장 싫어 하는 과정이기도 하다.

하나의 프로그램을 테스트하기 위해서는 모든 예외사항에 대한 데이터를 직접 입력해야 하고 실제 데이터베이스에 정확하게 반영되는지도 확인해야 하기 때문이다. 이렇게 테스트를 수행한다고 해도 프로그램을 Release한 다음에는 여전히 마음 한 구석에는 버그에 대한 불안한 마음이 있다.

해당 기능에서 다루는 데이터가 돈과 관련이 있거나, 비즈니스 수행에 치명적인 데이터인 경우 이러한 불안감에 불면으로 고생하는 개발자들도 있을 것이다.

필자의 경우 7 ~ 8년 동안 이러한 불안 속에서 생활을 계속해왔다고 할 수 있다. 그러면 개발자들은 이런 버그에 대한 불안을 개발직에 종사하는 그 순간까지 계속 가지고 가야 하는가? 필자의 경우도 이러한 불안감으로 많은 스트레스를 받아 왔지만 해결하려고 하는 노력은 거의 없었다.

이번 장에서는 이러한 테스트와 관련되어 요즘(?, 이미 오래 전부터 있었고 실제 사용되어지던 내용들이다. 필자와 같이 노력을 기울이지 않은 개발자들에게는 요즘에서야 알게 되겠지만 말이다.) 주목 받고 있는 TDD(Test-Driven Development)에 대해 알아보고 eclipse를 이용하여 쉽게 TDD를 적용하는 방법에 대해 소개하고자 한다.

TDD 는 말 그대로 테스트 프로그램을 위주로 개발을 수행하는 기법이다. 간단하게 요약하자면 프로그램을 개발하기 전에 개발할 프로그램을 테스트하는 프로그램을 먼저 만든 다음 이 테스트 프로그램이 컴파일이 되고 테스트가 모두 통과하도록 실제 프로그램을 개발해 나가는 기법이다.

이렇게 테스트 프로그램을 먼저 만드는 장점은 테스트 수행 자체가 자동(프로그램)으로 이루어지기 때문에 한번 작성된 테스트 프로그램을 이용하여 지속적인 테스트가 가능하다는 것과 이러한 테스트를 기반으로 하여 신뢰성이 놓은 시스템을 구축할 수 있다는 것이다.

또 다른 장점은 기존의 테스트 프로그램이 없는 시스템의 경우 유지보수 시 특정 부분에 대해 변경이 가해졌을 때 시스템 전체에 대해 수작업으로 다시 테스트를 수행해야 하는 번거러움이 있고 테스트가 완료되었다 하더라도 어디 부분에서 side effect가 발생할 지 예측을 할 수 없다.

하지만 테스트 프로그램을 미리 만들어 놓고 이 테스트 프로그램을 모두 통과한 프로그램을 release하고 운영 중이라고 하면 이때 만들어진 테스트 프로그램은 해당 어플리케이션의 기능 테스트를 만족시켰기 때문에 정상적으로 운영이 될 것이다.

여기까지 설명하면 대부분의 개발자들은 아니 개발 납기 맞추기에도 일정이 촉박한데 거기다가 테스트 프로그램까지 만들라고 하다니! 너무 비현실적인 내용이야!! 라고 말하는 개발자들이 많다. 필자의 경우도 그러했다. 하지만 곰곰히 생각해 보면 앞에서 필자가 가장 많은 시간이 걸리는 부분이 테스트 및 디버깅 부분이라고 했다. 이 부분의 시간을 줄이게 되면 그만큼의 시간을 버는 것이 될 것이다. 몰론 초기에 익숙하지 않은 개발 기법에 적응하는데 있어 개발 생산성의 저하는 모든 기술에 있어 학습의 과정에서는 어쩔 수 없는 것이다.

조금의 시간을 더 투자해서 좀 더 안정적인 코드가 나와 개발자의 불안함을 조금이나마 해소 시켜 줄 수 있으면 선택해볼 만도 하다는 것이 필자의 생각이다.

 

필자 역시 이 TDD에 대해서 익숙하지 않고 실제 프로젝트에 적용해본 경험도 없다. 지금 필자의 상태는 이 TDD를 프로젝트에 어떻게 적용을 할 수 있는지 고민하고 스터디를 하고 있는 중이다. 물론 관련 세미나도 참석을 하고 여러 서적을 참고하고 있지만 구체적으로 프로젝트에 어떻게 적용했다라고 하는 사례가 없기 때문에 여전히 고전을 면치 못하고 있다.

가장 좋은 방법은 TDD를 적용한 프로젝트에 참여하는 방법이겠지만 현실적으로 필자가 근무하는 곳에서는 아직 이러한 사항을 적용하기에는 시기상조라고 생각하고 있다.

이번 장의 목적은 필자가 스터디하면서 진행한 사항을 정리하고 제대로 방향을 잡아가고 있는지에 대해 이 글을 읽는 독자들로부터 자문을 구하기 위해서라고 생각하면 된다.

따라서, 이번 장에 나타나는 내용은 순수하게 필자의 생각만을 위주로 작성하는 글이기 때문에 글을 읽는 독자들은 이런 방법도 있구나라는 정도의 참고 수준 정도로만 인식해 주었으면 한다. 또한 잘못된 방향이나 설명이 있으면 즉시 알려주면 반영하도록 하겠다.

이 글의 대부분은 켄트벡이 쓴 Test-Driven Development By Example 라는 글을 내용을 참고로 하여 작성하였다. TDD에 대해 좀 더 깊은 내용을 알고 싶으면 이 글을 참고하기 바란다.


JUnit을 이용한 테스트 프로그램

 

그러면 eclipse를 이용하여 TDD를 적용하는 방법을 살펴보자. 먼저 JUnit를 이용하여 기본적인 테스트 프로그램을 작성해본 다음 cactus를 이용하여 Http 처리관련 테스트 프로그램을 만들어 보자.

JUnit은 테스트 프로그램에 대한 프레임워크를 제공하고 GUI 테스트 도구를 지원한다. eclipse를 이용하지 않고도 www.junit.org에서 다운로드 받아 사용할 수 있다.

먼저 간단하게 JUnit을 이용하여 간단한 테스트 프로그램을 만들어 본 다음 eclipse에서 사용하는 방법에 대해 알아보자.

 

JUnit을 이용하여 테스트 프로그램을 만들고 테스트하는 것은 JUnit 자체가 프레임워크로 구성되어 있기 때문에 이 프레임워크를 이용하여 정해진 규칙대로만 코드를 만들면 된다.

먼저 다운로드 받은 JUnit 파일을 적당한 디렉토리에 압축을 해제한다. 압축을 해제한 다음 DOS창에서 junit.jar 파일을 클래스패스에 설정하고 다음과 같은 명령을 이용하여 JUnit 테스트 GUI 도구를 실행시켜 본다.

 

 

 

다음과 같은 프로그램이 수행된다.

 

여기까지 성공했으면 JUnit의 설치는 끝났다.

이제 테스트 프로그램을 작성해보자. TDD는 다음과 같은 순서로 진행을 하면 된다. 이 순서는 켄트벡이 제시한 순서이다.

 

1. Quickly add a test(테스트 프로그램을 작성한다.)

2. Run all tests and see the new one fail

(모든 테스트 프로그램을 수행시키고 테스트에 실패한 부분을 확인한다.)

3. Make a little change(소스를 추가하거나 변경한다.)

4. Run all tests and see them all succeed

(다시 모든 테스트를 수행시키고 모두 테스트를 통과했는데 확인한다.)

5. Refactor to remove duplication(중복을 제거하기 위해 Refactoring 한다.)

 

켄트벡이 주장하는 것 중 가장 중요한 것은 먼저 테스트 프로그램을 만들라는 것이다. 그리고 가장 간단한 부분부터 먼저 테스트 프로그램을 작성하고 이 테스트가 통과하도록 프로그램을 만들라고 말하고 있다.

여기서는 예제로 두 Money를 더하기 하는 기능에 대한 프로그램을 TDD를 이용하여 만들어 보자. 지금은 TDD에 대한 흐름을 알기 위해서기 때문에 아주 간단한 예제를 선택했다.

이 예제는 켄트벡이 제시한 예제인데 켄드벡의 글을 보면 아주 자세한 예제 및 설명이 나와 있다.

먼저 테스트 프로그램을 만든다.

 

AddTest.java

import junit.framework.Test;

import junit.framework.TestCase;

import junit.framework.TestSuite;

 

public class AddTest extends TestCase

{

public AddTest(String name)

        {

             super(name);

       }

   

public void testAdd()

{

Money fiveMoney = new Money(5);

fiveMoney.add(new Money(10));

assertEquals(15, fiveMoney.getAmount());

}    

}

 

테스트 프로그램은 JUnit에서 제공하는 TestCase 클래스를 상속 받아 구현해 주면 된다.

테스트 로직을 구현할 때는 메소드 명을 testXXX()와 같은 형태로 작성하면 JUnit에서 자동으로 해당 메소드를 수행시키고 테스트 결과를 보여주게 된다. 위의 예제에서는 testAdd() 메소드가 테스트 대상이 된다. 하나의 테스트 클래스에는 여러 개의 테스트 메소드 작성이 가능하다.

testXXX() 메소드 내에서는 테스트 대상이 되는 기능을 수행시키고, 그 수행 결과가 개발자가 원하는 결과가 나타나는지 검증을 하는 로직을 작성해주면 된다.

위의 예제에서는 테스트 하고자 하는 기능은 두 Money의 합을 구하는 기능이기 때문에 fiveMoney.add() 가 수행되었다.

검증을 위한 부분은 두 Money의 합의 결과가 제대로 수행되었는지 확인하기 위해 assertEquals() API를 이용하여 값을 비교하여 결과가 15인 경우 테스트를 통과한 것으로 인정하고 15가 아닌 경우 테스트를 실패 처리한다.

JUnit을 이용하여 테스트 결과를 검증하는 방법은 assertEquals, assertNotNull, assertNull, assertNotEquals, assertTrue 등과 같은 검증용 메소드를 이용하여 검증한다.

 

 

DOS 창에서 위의 화면과 같이 클래스 패스를 설정해 주고 컴파일을 수행하게 되면 당연히 에러가 발생할 것이다. 에러의 원인은 아직까지 실제 우리가 구현하고자 하는 Money 클래스를 만들지 않았기 때문이다. TDD는 이렇게 Money 클래스를 먼저 만드는 것이 아니라 Money클래스를 테스트하는 AddTest 클래스부터 먼저 만들고 이 AddTest클래스가 정상적으로 동작하도록 하는 순서로 개발을 진행하게 된다.

 

Money.java

public class Money

{

    int amount;

   

    public Money(int amount)

    {

        this.amount = amount;

    }

}

 

다시 컴파일을 해본다. 처음에는 Money 클래스가 없다는 에러가 나타났는데 지금은 다른 에러가 나타났다.

 

AddTest.java:15: cannot resolve symbol

symbol  : method add  (Money)

location: class Money

        fiveMoney.add(new Money(10));

                 ^

AddTest.java:16: cannot resolve symbol

symbol  : method getAmount  ()

location: class Money

        assertEquals(15, fiveMoney.getAmount());

                                  ^

2 errors

 

이 두개의 에러를 잡기 위해 Money 클래스에 add() 메소드와 getAmount() 메소드를 추가한다. 아직까지 구체적인 기능에 대한 구현은 생각하지 않았기 때문에 메소드 정의만 한다.

 

Money.java

public class Money

{

    int amount;

   

    public Money(int amount)

    {

        this.amount = amount;

    }

   

    public void add(Money money)

    {

    }

   

    public int getAmount()

    {

    }

}

 

이제 컴파일 하면 다음과 같이 에러가 하나만 나타난다.

 

.\Money.java:15: missing return statement

    {

    ^

1 error

 

getAmount() 메소드에서는 int type을 반환해야 하는데 반환하는 값이 없기 때문에 발생한 에러이다. 여기에 return 0; 이라고 수정하고 다시 컴파일 해보자.

 

Money.java

public class Money

{

    int amount;

   

    public Money(int amount)

    {

        this.amount = amount;

    }

   

    public void add(Money money)

    {

    }

   

    public int getAmount()

    {

        return 0;

    }

}

 

예상대로 컴파일은 통과하였다. 그러면 테스트가 성공하였는지를 JUnit을 이용하여 수행시켜 보자.

위의 테스트 프로그램의 경우 main() 메소드가 없기 때문에 앞에서 본 JUnit에서 제공하는 GUI 도구를 이용하여 테스트를 수행할 수 있다. JUnit 설치 시 수행시킨 JUnit GUI 도구를 실행시킨 다음 Test Class name 오른쪽에 있는 “…” 버튼을 클릭하여 테스트 할 클래스를 선택한다. 이 “…” 버튼을 클릭하게 되면 TestCase를 상속받은 모든 테스트 클래스가 목록에 나타나게 된다. 여기서는 AddTest를 선택한다.

 

 

Run 버튼을 클릭하여 테스트를 수행시킨다.

 

지금까지 진행된 상태에서 테스트를 수행하게 되면 테스트가 실패했다는 표시로 빨간색 바가 나타나고 화면의 가운데 부분에 어느 부분에서 실패했는지가 나타난다. 여기서는 15라는 값이 나타나야 결과는 0 값이 나왔기 때문에 테스트가 실패하였다.

 

           Junit.framework.AssertionFailedError:expected:<15>but was:<0>

 

그러면 이제 다시 소스로 돌아와서 소스를 적절하게 수정하여 테스트를 통과하도록 만든다. 가장 간단하게 테스트를 통과하도록 하는 방법은 getAmount() 메소드에서 15를 반환하도록 수정 하면 된다.

 

Money.java

public class Money

{

    int amount;

   

    public Money(int amount)

    {

        this.amount = amount;

    }

   

    public void add(Money money)

    {

    }

   

    public int getAmount()

    {

        return 15;

    }