본문 바로가기

Engine & Module

오픈 소스 C/C++ 유닛 테스트 도구, Part 3: CppTest 알아보기

출처 : ibm.com

유닛 테스트를 위한 오픈 소스 도구에 대한 시리즈의 마지막인 이 기사에서는 Niklas Lundell의 강력한 프레임워크인 CppTest에 대해 자세하게 살펴본다. CppTest의 가장 큰 장점은 쉽게 이해할 수 있고 적용 및 사용이 용이하다는 것이다. 테스트 고정 기능 설계인 CppTest를 사용하여 유닛 테스트 및 테스트 스위트를 작성하는 방법과 여러 가지 유용한 CppTest 제공 매크로에 익숙해지는 동안 회귀 로그 형식을 사용자 정의하는 방법에 대해 살펴보자. 고급 사용자인 경우에는 이 기사에서 CppUnit 프레임워크와 CppTest 프레임워크 간 비교도 제공한다.

자주 사용하는 약어

  • HTML: Hypertext Markup Language
  • I/O: Input/output
  • XML: Extensible Markup Language

설치 및 사용법

CppTest는 GNU LGPL(Lesser General Public License)에 의거해서 Sourceforge(참고자료 참조)에서 무료로 다운로드할 수 있다. 소스 빌드는 일반적인 오픈 소스 configure-make 형식을 따른다. 결과는 libcpptest라는 정적 라이브러리이다. 클라이언트 코드는 다운로드한 소스의 일부인 헤더 파일 cppTest.h와 정적 라이브러리 libcpptest.a에 대한 링크를 포함해야 한다. 이 기사는 CppTest 버전 1.1.0을 기반으로 한다.


유닛 테스트는 소스 코드의 특정 섹션을 테스트하기 위한 것이다. 가장 단순한 형식의 경우 이 테스트에는 다른 C/C++ 코드를 테스트하는 C/C++ 함수의 콜렉션이 포함된다. CppTest는 기본 테스트 스위트 함수를 제공하는 Test 네임스페이스에서 Suite라는 클래스를 정의한다. 사용자 정의 테스트 스위트는 실제 유닛 테스트로 작동하는 함수를 정의하여 이 기능을 확장해야 한다. Listing 1에서는 두 가지 함수가 포함된 myTest라는 클래스를 정의한다(각각의 함수는 소스 코드의 부분을 테스트함). TEST_ADD는 테스트를 등록하기 위한 매크로이다.


Listing 1. 기본 Test::Suite 클래스 확장하기
	
#include “cppTest.h”
class myTest : public Test::Suite { 
  void function1_to_test_some_code( );
  void function2_to_test_some_code( );

  myTest( ) { 
      TEST_ADD (myTest::function1_to_test_some_code); 
      TEST_ADD (myTest::function2_to_test_some_code); 
  } 
}; 


테스트 스위트의 기능을 쉽게 확장하여 테스트 스위트의 계층 구조를 작성할 수 있다. 이러한 작업을 수행해야 하는 필요성은 각각의 테스트 스위트가 컴파일러의 일부 특정 코드 영역—예: 구문 분석, 코드 생성, 코드 최적화를 위한 테스트 스위트—을 테스트한다는 사실로 인해 발생하며 계층 구조는 시간 흐를수록 관리 향상에 도움이 된다. Listing 2에서는 이러한 계층 구조를 작성하는 방법을 보여 준다.


Listing 2. 유닛 테스트 계층 구조 작성하기
	
#include “cppTest.h”
class unitTestSuite1 : public Test::Suite { … } 
class unitTestSuite2 : public Test::Suite { … } 

class myTest : public Test::Suite { 
  myTest( ) { 
      add (std::auto_ptr<Test::Suite>(new unitTestSuite1( ))); 
      add (std::auto_ptr<Test::Suite>(new unitTestSuite2( ))); 
  } 
}; 

add 메소드는 Suite 클래스에 속한다. Listing 3에서는 해당 프로토타입(헤더 cpptest-suite.h에서 제공됨)을 보여 준다.


Listing 3. Suite::add 메소드 선언
	
class Suite
{
 public:	
    …
    void add(std::auto_ptr<Suite> suite);
    ...
} ;


Suite 클래스의 run 메소드는 테스트 실행을 담당한다. Listing 4에서는 테스트 실행을 보여 준다.


Listing 4. 상세 모드에서 테스트 스위트 실행하기
	
#include “cppTest.h”
class myTest : public Test::Suite { 
  void function1_to_test_some_code( ) { … };
  void function2_to_test_some_code( ) { … };

  myTest( ) { 
      TEST_ADD (myTest::function1_to_test_some_code); 
      TEST_ADD (myTest::function2_to_test_some_code); 
  } 
}; 

int main ( ) 
{ 
  myTest tests; 
  Test::TextOutput output(Test::TextOutput::Verbose);
  return tests.run(output);
} 

run 메소드는 개별 유닛 테스트가 모두 성공한 경우에만 True로 설정되는 부울 값을 리턴한다. run 메소드에 대한 인수는 TextOutput 유형의 오브젝트이다. TextOutput 클래스는 테스트 로그 인쇄를 처리한다. 기본적으로 로그는 화면에 덤프된다.

상세 모드 외에도 간단한 정보 표시 모드도 있다. 이 두 모드의 차이점은 상세 모드는 개발 테스트의 가정 실패에 대한 행 번호/파일 이름 정보를 인쇄하는 반면 간단한 정보 표시 모드는 통과했거나 실패한 테스트의 수만 제공한다는 점이다.

실패 후 계속하기

그렇다면 단일 테스트에 실패하는 경우에는 어떻게 되는가? 계속 진행할지 여부는 클라이언트 코드에 의해서만 결정된다. 기본 동작은 다른 테스트를 계속 실행하는 것이다. Listing 5에서는 run 메소드의 프로토타입을 보여 준다.


Listing 5. run 메소드의 프로토타입
	
bool Test::Suite::run(	
    Output &   output,
    bool 	        cont_after_fail = true	 
);

첫 번째 실패를 발견한 후 회귀를 종료해야 하는 상황의 경우 run 메소드에 대한 두 번째 인수는 False여야 한다. 하지만 두 번째 플래그를 언제 False로 설정해야 하는지는 명확하지 않다. 클라이언트 코드가 100% 가득 찬 디스크에 정보를 쓰려고 한다고 가정하면 코드가 실패하고 비슷한 동작을 가진 해당 스위트의 향후 테스트도 모두 실패한다. 이 상황에서는 즉시 회귀를 중지하는 것이 좋다.


출력 포맷터가 필요한 이유는 여러 가지 형식(텍스트, HTML 등)의 회귀 실행 보고서가 필요할 수 있기 때문이다. 따라서 run 메소드 자체는 결과를 덤프하지 않지만 결과를 표시하는 Output 유형의 오브벡트를 승인한다. CppTest에서는 세 가지 유형의 출력 포맷터를 사용할 수 있다.

  • Test::TextOutput. 모든 출력 핸들러 중 가장 단순한 핸들러이다. 표시 모드는 자세한 정보 표시 또는 간단한 정보 표시가 될 수 있다.
  • Test::CompilerOutput. 컴파일러 빌드 로그와 비슷한 방식으로 출력이 생성된다.
  • Test::HtmlOutput. 많이 사용되는 HTML 출력이다.

기본적으로 세 가지 포맷터 모두 출력을 std::cout에 덤프한다. 처음 두 포맷터의 생성자는 예를 들어 향후 정밀 검사를 위해 파일에 출력을 덤프해야 함을 나타내는 std::ostream 유형의 인수를 승인한다. 출력 포맷터의 사용자 정의된 버전을 작성하도록 선택할 수도 있다. 이를 위한 유일한 요구사항은 사용자 정의 포맷터가 Test::Output에서 파생되어야 한다는 것이다. 다른 출력 형식에 대해 알아보기 위해 Listing 6에 있는 코드를 생각해 본다.


Listing 6. 간단한 정보 표시 모드에서 TEST_FAIL 매크로 실행하기
	
#include “cppTest.h”
class failTest1 : public Test::Suite { 
void always_fail( ) { 
  TEST_FAIL (“This always fails!\n”); 
} 
public: 
  failTest1( ) { TEST_ADD(failTest1::always_fail); } 
}; 

int main ( ) { 
  failTest1 test1;
  Test::TextOutput output(Test::TextOutput::Terse);
  return test1.run(output) ? 1 : 0;
}

TEST_FAIL는 가정 실패의 원인이 되는 cppTest.h에 사전정의된 매크로라는 것에 유의한다. (이에 대해서는 나중에 자세히 설명한다.) Listing 7에는 출력이 표시된다.


Listing 7. 실패 수만 보여 주는 간단한 정보 표시 출력
	
failTest1: 1/1, 0% correct in 0.000000 seconds
Total: 1 tests, 0% correct in 0.000000 seconds

Listing 8에는 상세 모드에서 동일한 코드를 실행한 경우의 출력이 표시된다.


Listing 8. 파일/행 정보, 메시지, 테스트 스위트 정보 등을 보여 주는 자세한 정보 표시 출력
	
failTest1: 1/1, 0% correct in 0.000000 seconds
        Test:    always_fail
        Suite:   failTest1
        File:     /home/arpan/test/mytest.cpp
        Line:    5
        Message: "This always fails!\n"

Total: 1 tests, 0% correct in 0.000000 seconds

다음으로 컴파일러 스타일 형식을 사용하는 데 필요한 코드가 Listing 9에 표시된다.


Listing 9. 컴파일러 스타일 출력 형식을 사용하여 TEST_FAIL 매크로 실행하기
	
#include “cppTest.h”
class failTest1 : public Test::Suite { 
void always_fail( ) { 
  TEST_FAIL (“This always fails!\n”); 
} 
public: 
  failTest1( ) { TEST_ADD(failTest1::always_fail); } 
}; 

int main ( ) { 
  failTest1 test1;
  Test::CompilerOutput output;
  return test1.run(output) ? 1 : 0;
}

Listing 10에 표시된 GNU GCC(Compiler Collection) 생성 컴파일 로그와 구문상 유사하다는 것에 유의한다.


Listing 10. 실패 수만 보여 주는 간단한 정보 표시 출력
	
/home/arpan/test/mytest.cpp:5: “This always fails!\n”

기본적으로 컴파일러 형식 출력은 GCC 스타일 빌드 로그이다. 하지만 Microsoft® Visual C++® 및 Borland 컴파일러 형식으로 출력을 가져올 수 있다. Listing 11에서는 출력 파일에 덤프되는 Visual C++ 스타일 로그를 생성한다.


Listing 11. 컴파일러 스타일 출력 형식을 사용하여 TEST_FAIL 매크로 실행하기
	
#include <ostream>
int main ( ) { 
  failTest1 test1;
  std::ofstream ofile;
  ofile.open("test.log");
  Test::CompilerOutput output(
      Test::CompilerOutput::MSVC, ofile);
 return test1.run(output) ? 1 : 0;
}

Listing 12에는 Listing 11에 있는 코드의 실행 후 test.log 파일의 컨텐츠가 표시된다.


Listing 12. Virtual C++ 스타일 컴파일러 출력
	
/home/arpan/test/mytest.cpp (5) :  “This always fails!\n”

마지막으로 이 중에서 가장 많이 사용되는 HtmlOutput 사용법에 대한 코드가 있다. HTML 포맷터는 생성자의 파일 핸들을 승인하는 대신 generate 메소드에 의존한다. generate 메소드에 대한 첫 번째 인수는 기본값이 std::coutstd::ostream 유형의 오브젝트이다(자세한 내용은 소스 헤더 파일 cpptest-htmloutput.h 참조). 파일 핸들을 사용하여 로그의 경로를 다른 위치로 지정할 수 있다. Listing 13에 예제가 제공된다.


Listing 13. HTML 스타일 형식
	
#include *<ostream>

int main ( ) { 
  failTest1 test1;
  std::ofstream ofile;
  ofile.open("test.log");
  Test::HtmlOutput output( );
  test1.run(output); 
  output.generate(ofile);
  return 0;
}

Listing 14에는 test.log에 생성된 HTML 출력의 일부가 표시된다.


Listing 14. 생성된 HTML 출력의 스니펫
	
…
<table summary="Test Failure" class="table_result">
  <tr>
    <td style="width:15%" class="tablecell_title">Test</td>
    <td class="tablecell_success">failTest1::always_fail</td>
  </tr>
  <tr>
    <td style="width:15%" class="tablecell_title">File</td>
    <td class="tablecell_success">/home/arpan/test/mytest.cpp:18</td>
  </tr>
  <tr>
    <td style="width:15%" class="tablecell_title">Message</td>
    <td class="tablecell_success">"This always fails!\n"</td>
  </tr>
</table>
…



동일한 테스트 스위트의 일부를 형성하는 유닛 테스트에는 동일한 초기화 요구사항 세트가 있는 경우가 있다. 이 경우에는 특정 매개변수, 오픈 파일 핸들/운영 체제 포트 등이 포함된 오브젝트를 작성해야 한다. 각 클래스 메소드에서 동일한 코드를 반복하는 대신 각 테스트에 대해 호출되는 일부 공통 초기화 및 종료 루틴을 사용하는 것이 더 좋다. 테스트 스위트의 일부로서 설정 및 해제 메소드를 정의해야 한다. Listing 15에서는 고정 기능을 사용하는 테스트 스위트 myTestWithFixtures를 정의한다.


Listing 15. 고정 기능을 사용하여 테스트 스위트 작성하기
	
#include “cppTest.h”
class myTestWithFixtures : public Test::Suite { 
  void function1_to_test_some_code( );
  void function2_to_test_some_code( );

  public: 
  myTest( ) { 
      TEST_ADD (function1_to_test_some_code); 
      TEST_ADD (function2_to_test_some_code); 
  } 

  protected: 
    virtual void setup( ) { … };
    virtual void tear_down( ) { … };
}; 

설정 및 해제 메소드에 대한 명시적 호출을 만들 필요는 없다는 점에 유의한다. 나중에 테스트 스위트를 확장할 계획이 있는 경우가 아니면 이러한 루틴을 가상으로 선언하지 않아도 된다. 두 루틴은 리턴 유형이 void여야 하며 인수를 승인하지 않는다.


CppTest는 클라이언트 소스 코드를 테스트하는 실제 메소드에서 사용하는 몇 가지 유용한 매크로를 제공한다. 이러한 매크로는 cpptest.h에 포함된 cpptest-assert.h에 내부적으로 정의되어 있다. 이러한 매크로와 해당 잠재적 유스케이스 중 일부가 다음에 설명된다. 달리 언급하지 않는 한 표시된 출력은 상세 모드를 사용하여 제공된다는 점에 유의한다.

TEST_FAIL(메시지)

Listing 16에 표시된 이 매크로는 무조건적인 실패를 나타내기 위한 것이다. 이 매크로를 사용할 수 있는 일반적인 상황은 클라이언트 함수의 결과를 처리하는 경우이다. 결과가 예상 결과와 일치하지 않는 경우 메시지와 함께 예외가 발생한다. TEST_FAIL 매크로가 히트되면 해당 특정 유닛 테스트에서는 더 이상 코드가 실행되지 않는다.


Listing 16. TEST_FAIL 매크로를 사용하는 클라이언트 코드
	
void myTestSuite::unitTest1 ( ) { 
    int result = usercode( );
    switch (result) { 
       case 0: // Do Something 
       case 1: // Do Something 
       …
       default: TEST_FAIL (“Invalid result\n”); 
   }
}

TEST_ASSERT(표현식)

이 매크로는 C 가정 라이브러리 루틴과 비슷하다(TEST_ASSERT가 디버그와 릴리스 빌드 둘 다에 사용되는 점은 제외). 표현식이 False로 확인되면 오류가 플래그된다. Listing 17에는 이 매크로에 대한 내부 구현이 표시된다.


Listing 17. TEST_ASSERT 매크로 구현
	
#define TEST_ASSERT(expr)  \
{  	\
  if (!(expr))  \
  { \
  assertment(::Test::Source(__FILE__, __LINE__, #expr));	\
     if (!continue_after_failure()) return;  \
  } \
}

TEST_ASSERT_MSG(표현식, 메시지)

이 매크로는 TEST_ASSERT와 비슷하다(가정이 실패하면 출력에 표현식 대신 메시지가 표시되는 점은 제외). 메시지가 포함되어 있거나 포함되어 있지 않은 가정은 다음과 같다.

TEST_ASSERT (1 + 1 == 0);
TEST_ASSERT (1 + 1 == 0, “Invalid expression”);

Listing 18에는 이 가정이 히트되는 경우의 출력이 표시된다.


Listing 18. TEST_ASSERT 및 TEST_ASSERT_MSG 매크로의 출력
	
Test:    compare
Suite:   CompareTestSuite
File:     /home/arpan/test/mytest.cpp
Line:    91
Message: 1 + 1 == 0

Test:    compare
Suite:   CompareTestSuite
File:     /home/arpan/test/mytest.cpp
Line:    92
Message: Invalid Expression 

TEST_ASSERT_DELTA(expression1, expression2, 델타)

expression1과 expression2 사이의 차이가 델타를 초과하는 경우 예외가 발생한다. 이 매크로는 expression1과 expression2가 부동 소수점 숫자인 경우에 특히 유용하다. 예를 들어, 반올림이 실제로 어떻게 수행되는지에 따라 4.3은 4.299999나 4.300001로 저장될 수 있으므로 비교가 제대로 수행되려면 델타가 필요하다. 또다른 예제는 운영 체제 I/O에 대한 코드를 테스트하는 것이다. 파일을 여는 데 걸리는 시간은 항상 같을 수는 없지만 특정 범위 안에 있어야 한다.

TEST_ASSERT_DELTA_MSG(표현식, 메시지)

이 매크로는 TEST_ASSERT_DELTA 매크로와 비슷하다(가정이 실패해도 메시지가 발행된다는 점은 제외).

TEST_THROWS(표현식, 예외)

이 매크로는 표현식을 검증하고 예외를 예상한다. 예외가 발생하지 않는 경우 가정이 트리거된다. 표현식의 실제 값은 테스트되지 않는다는 점에 유의한다. 테스트되는 것은 예외이다. Listing 19에 있는 코드를 생각해 본다.


Listing 19. 정수 예외 처리하기
	
class myTest1 : public Test::Suite { 
  …
  void func1( ) { 
     TEST_THROWS (userCode( ), int);
  }
  public: 
     myTest1( ) { TEST_ADD(myTest1::func1); } 
}; 

void userCode( ) throws(int) { 
   …
   throw int;
}

userCode 루틴의 리턴 유형은 중요하지 않지만 double이나 정수인 것이 낫다. 여기서는 무조건 userCodeint 유형의 예외를 발생시키기 때문에 테스트는 문제없이 통과된다.

TEST_THROWS_ANYTHING(표현식)

상황에 따라 클라이언트 루틴에서 여러 유형의 예외가 발생할 수 있다. 이러한 상황을 처리하기 위해 예상 예외 유형을 지정하지 않는 TEST_THROWS_ANYTHING 매크로를 가지고 있다. 클라이언트 코드 실행 후 예외가 발생하는 한 가정은 발생하지 않는다.

TEST_THROWS_MSG(표현식, 예외, 메시지)

이 매크로는 이 경우 표현식이 아니라 메시지가 인쇄된다는 점을 제외하고는 TEST_THROWS와 비슷하다. 다음 코드를 생각해 보자.

TEST_THROWS(userCode( ), int);
TEST_THROWS(userCode( ), int, “No expected exception of type int”);

Listing 20에는 이러한 가정이 실패하는 경우의 출력이 표시된다.


Listing 20. TEST_THROWS 및 TEST_THROWS_MSG 매크로의 출력
	
Test:    func1
Suite:   myTest1
File:    /home/arpan/test/mytest.cpp
Line:    24
Message: userCode()

Test:    func2
Suite:   myTest1
File:    /home/arpan/test/mytest.cpp
Line:    32
Message: No expected exception of type int


이 시리즈의 Part 2에서는 또 하나의 자주 사용되는 오픈 소스 유닛 테스트 프레임워크인 CppUnit에 대해 다루었다. CppTestCppUnit보다 훨씬 단순한 프레임워크이면서도 작업을 완료한다. 이 두 가지 강력한 도구를 간단히 비교한 결과는 다음과 같다.

  • 유닛 테스트 및 테스트 스위트 작성 용이성 CppUnitCppTest는 둘 다 클래스 메소드의 유닛 테스트를 작성하며 해당 클래스는 도구에서 제공한 일부 Test 클래스에서 파생된다. CppTest의 구문은 조금 더 단순하지만 테스트 등록은 클래스 생성자 내부에서 발생한다. CppUnit의 경우에는 추가 매크로 CPPUNIT_TEST_SUITECPPUNIT_TEST_SUITE_ENDS가 필요하다.
  • 테스트 실행 CppTest는 단순히 run 메소드를 테스트 스위트에서 호출하는 반면 CppUnit은 테스트 실행을 위해 run 메소드가 호출되는 별도의 TestRunner 클래스를 사용한다.
  • 테스트 계층 구조 확장 CppTest의 경우 이전 클래스에서 상속하는 새 클래스를 작성하여 언제나 이전 테스트 스위트를 확장할 수 있다. 새 클래스는 유닛 테스트 풀에 추가되는 일부 추가 함수를 정의한다. 사용자는 단순히 새 클래스 유형의 오브젝트에서 run 메소드를 호출한다. 이와 대조적으로 CppUnit을 사용하려면 CPPUNIT_TEST_SUB_SUITE 매크로를 클래스 상속과 함께 사용하여 동일한 효과를 달성해야 한다.
  • 형식화된 출력 생성 CppTestCppUnit에는 둘 다 출력을 사용자 정의하는 기능이 포함되어 있다. 그러나 CppTest에는 사전 정의된 유용한 HTML 출력 포맷터가 있지만 CppUnit에는 없다. 하지만 CppUnit은 XML 형식을 독점적으로 지원한다. 둘 다 텍스트 및 컴파일러 스타일 형식을 지원한다.
  • 테스트 고정 기능 작성 테스트 고정 기능을 사용하려면 CppUnit에서는 CppUnit::TestFixture에서 테스트 클래스가 파생되어야 한다. 설정 및 해제 루틴에 대한 정의를 제공해야 한다. CppTest의 경우에는 설정 및 해제 루틴에 대한 정의만 제공해야 한다. 이렇게 하면 클라이언트 코드가 단순해지므로 이 방법이 가장 좋다.
  • 사전 정의된 유틸리티 매크로 지원 CppTestCppUnit에는 둘 다 가정, float 처리 등을 위한 비교 가능한 매크로 세트가 있다.
  • 헤더 파일 CppTest는 단일 헤더 파일을 포함해야 하는 반면 CppUnit 클라이언트 코드는 사용된 기능에 따라 HelperMacros.h 및 TextTestRunner.h와 같은 복수의 헤더를 포함해야 한다.

유닛 테스트는 오늘날의 소프트웨어 개발의 기초가 되며 CppTestC/C++ 개발자가 코드 테스트에 개입하여 유지보수 문제를 예방하는 데 필요한 도구 창고에 있는 또다른 도구이다. 이 세 파트로 된 시리즈에서 다룬 세 가지 도구—Boost 테스트 도구, CppUnitCppTest—는 모두 고정 기능, 가정을 위한 매크로 및 출력 포맷터와 같은 동일한 기본 개념을 사용한다. 각각의 도구는 오픈 소스이므로 필요에 따라 코드를 쉽게 추가로 사용자 정의할 수 있다(예: CppTest를 위한 XML 포맷터). 망설이지 말고 사용해 보자.


참고자료

교육

제품 및 기술 얻기

  • CppTest 다운로드: CppTest의 최신 버전을 다운로드하자.

  • IBM 제품 평가판: DB2®, Lotus®, Rational®, Tivoli® 및 WebSphere®의 애플리케이션 개발 도구 및 미들웨어 제품을 사용해 볼 수 있다.

토론

필자소개

Arpan Sen은 전자 설계 자동화 업계에서 소프트웨어 개발을 이끄는 리드 엔지니어다. Sen은 솔라리스, SunOS, HP-UX, IRIX와 같은 다양한 유닉스 운영체제는 물론이고 리눅스와 마이크로소프트 윈도우 환경에서 여러 해 동안 개발했다. Sen은 소프트웨어 성능 최적화 기법, 그래프 이론, 병렬 컴퓨팅에 관심이 많다. Sen은 소프트웨어 시스템 분야에서 대학원 학위를 받았다.