유닛 테스트를 위한 오픈 소스 도구에 대한 시리즈의 마지막인 이 기사에서는 Niklas Lundell의 강력한 프레임워크인 CppTest
에 대해 자세하게 살펴본다. CppTest
의 가장 큰 장점은 쉽게 이해할 수 있고 적용 및 사용이 용이하다는 것이다. 테스트 고정 기능 설계인 CppTest
를 사용하여 유닛 테스트 및 테스트 스위트를 작성하는 방법과 여러 가지 유용한 CppTest
제공 매크로에 익숙해지는 동안 회귀 로그 형식을 사용자 정의하는 방법에 대해 살펴보자. 고급 사용자인 경우에는 이 기사에서 CppUnit
프레임워크와 CppTest
프레임워크 간 비교도 제공한다.
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::cout
인 std::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에 내부적으로 정의되어 있다. 이러한 매크로와 해당 잠재적 유스케이스 중 일부가 다음에 설명된다. 달리 언급하지 않는 한 표시된 출력은 상세 모드를 사용하여 제공된다는 점에 유의한다.
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”); } } |
이 매크로는 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
와 비슷하다(가정이 실패하면 출력에 표현식 대신 메시지가 표시되는 점은 제외). 메시지가 포함되어 있거나 포함되어 있지 않은 가정은 다음과 같다.
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
매크로와 비슷하다(가정이 실패해도 메시지가 발행된다는 점은 제외).
이 매크로는 표현식을 검증하고 예외를 예상한다. 예외가 발생하지 않는 경우 가정이 트리거된다. 표현식의 실제 값은 테스트되지 않는다는 점에 유의한다. 테스트되는 것은 예외이다. 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이나 정수인 것이 낫다. 여기서는 무조건 userCode
가 int
유형의 예외를 발생시키기 때문에 테스트는 문제없이 통과된다.
상황에 따라 클라이언트 루틴에서 여러 유형의 예외가 발생할 수 있다. 이러한 상황을 처리하기 위해 예상 예외 유형을 지정하지 않는 TEST_THROWS_ANYTHING
매크로를 가지고 있다. 클라이언트 코드 실행 후 예외가 발생하는 한 가정은 발생하지 않는다.
이 매크로는 이 경우 표현식이 아니라 메시지가 인쇄된다는 점을 제외하고는 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
에 대해 다루었다. CppTest
는 CppUnit
보다 훨씬 단순한 프레임워크이면서도 작업을 완료한다. 이 두 가지 강력한 도구를 간단히 비교한 결과는 다음과 같다.
- 유닛 테스트 및 테스트 스위트 작성 용이성
CppUnit
과CppTest
는 둘 다 클래스 메소드의 유닛 테스트를 작성하며 해당 클래스는 도구에서 제공한 일부Test
클래스에서 파생된다.CppTest
의 구문은 조금 더 단순하지만 테스트 등록은 클래스 생성자 내부에서 발생한다.CppUnit
의 경우에는 추가 매크로CPPUNIT_TEST_SUITE
및CPPUNIT_TEST_SUITE_ENDS
가 필요하다. - 테스트 실행
CppTest
는 단순히run
메소드를 테스트 스위트에서 호출하는 반면CppUnit
은 테스트 실행을 위해run
메소드가 호출되는 별도의TestRunner
클래스를 사용한다. - 테스트 계층 구조 확장
CppTest
의 경우 이전 클래스에서 상속하는 새 클래스를 작성하여 언제나 이전 테스트 스위트를 확장할 수 있다. 새 클래스는 유닛 테스트 풀에 추가되는 일부 추가 함수를 정의한다. 사용자는 단순히 새 클래스 유형의 오브젝트에서run
메소드를 호출한다. 이와 대조적으로CppUnit
을 사용하려면CPPUNIT_TEST_SUB_SUITE
매크로를 클래스 상속과 함께 사용하여 동일한 효과를 달성해야 한다. - 형식화된 출력 생성
CppTest
와CppUnit
에는 둘 다 출력을 사용자 정의하는 기능이 포함되어 있다. 그러나CppTest
에는 사전 정의된 유용한 HTML 출력 포맷터가 있지만CppUnit
에는 없다. 하지만CppUnit
은 XML 형식을 독점적으로 지원한다. 둘 다 텍스트 및 컴파일러 스타일 형식을 지원한다. - 테스트 고정 기능 작성 테스트 고정 기능을 사용하려면
CppUnit
에서는CppUnit::TestFixture
에서 테스트 클래스가 파생되어야 한다. 설정 및 해제 루틴에 대한 정의를 제공해야 한다.CppTest
의 경우에는 설정 및 해제 루틴에 대한 정의만 제공해야 한다. 이렇게 하면 클라이언트 코드가 단순해지므로 이 방법이 가장 좋다. - 사전 정의된 유틸리티 매크로 지원
CppTest
와CppUnit
에는 둘 다 가정, float 처리 등을 위한 비교 가능한 매크로 세트가 있다. - 헤더 파일
CppTest
는 단일 헤더 파일을 포함해야 하는 반면CppUnit
클라이언트 코드는 사용된 기능에 따라 HelperMacros.h 및 TextTestRunner.h와 같은 복수의 헤더를 포함해야 한다.
유닛 테스트는 오늘날의 소프트웨어 개발의 기초가 되며 CppTest
는 C/C++
개발자가 코드 테스트에 개입하여 유지보수 문제를 예방하는 데 필요한 도구 창고에 있는 또다른 도구이다. 이 세 파트로 된 시리즈에서 다룬 세 가지 도구—Boost 테스트 도구, CppUnit
및 CppTest
—는 모두 고정 기능, 가정을 위한 매크로 및 출력 포맷터와 같은 동일한 기본 개념을 사용한다. 각각의 도구는 오픈 소스이므로 필요에 따라 코드를 쉽게 추가로 사용자 정의할 수 있다(예: CppTest를 위한 XML 포맷터). 망설이지 말고 사용해 보자.
교육
CppTest
: Sourceforge.net에서 프로젝트 페이지를 살펴보자.
- AIX와 UNIX developerWorks 영역: AIX와 UNIX 영역에서는 AIX 시스템 관리와 UNIX 스킬 확장의 모든 측면과 관련된 풍부한 정보를 제공한다.
- AIX 및 UNIX 입문(한글)? AIX와 UNIX 입문 페이지에서 자세한 정보를 볼 수 있다.
- developerWorks 기술 행사 및 웹 캐스트: 최신 기술에 대한 정보를 얻을 수 있다.
- "오픈 소스 C/C++ 유닛 테스트 도구, Part 1: Boost 유닛 테스트 프레임워크 알아보기"(developerWorks, 2009년 12월): 이 기사에서는 C/C++ 기반 제품의 Boost 유닛 테스트 프레임워크에 대해 설명한다.
- "오픈 소스 C/C++ 유닛 테스트 도구, Part 2: CppUnit 알아보기"(developerWorks, 2010년 1월): 이 기사를 읽고 JUnit 테스트 프레임워크의 C++ 포트인 CppUnit에 대해 자세히 알아보자.
제품 및 기술 얻기
CppTest
다운로드:CppTest
의 최신 버전을 다운로드하자.
- IBM 제품 평가판: DB2®, Lotus®, Rational®, Tivoli® 및 WebSphere®의 애플리케이션 개발 도구 및 미들웨어 제품을 사용해 볼 수 있다.
토론
- developerWorks 블로그: 블로그를 읽어 보고 developerWorks community에 참여하자.
- Twitter의 developerWorks 페이지를 살펴보자.
- My developerWorks 커뮤니티에 참여하자.
- AIX 및 UNIX 포럼에 참여하자.
- AIX Forum
- AIX Forum for developers
- Cluster Systems Management
- IBM Support Assistant Forum
- Performance Tools Forum
- Virtualization Forum
- 기타 AIX and UNIX Forums
'Engine & Module' 카테고리의 다른 글
srtp (1) | 2015.03.26 |
---|---|
SRTP (0) | 2014.12.17 |
윈도우 .exe 파일 PE정보 (0) | 2014.07.08 |
리소스 해커 : 리소스 파일 탐색 프로그램 (0) | 2010.08.11 |
[Codec] Adam7 Algorithm (0) | 2010.07.20 |
DirectX SDK 한글 문서 가이드 (0) | 2010.07.19 |
나의 PC의 HDD에는 뭐가 있는가? (0) | 2010.06.30 |
::CreateThread, _beginthread, _beginthreadex, ::AfxBeginThread 차이점 (0) | 2010.06.29 |