본문 바로가기

도구의발견

CppUnit Cookbook

CppUnit Cookbook

리팩토링이라는 책을 읽다가 단위 테스트에 대한 필요성을 느끼고 예전 부터 한번 알아봐야지 하고 마음만 먹던 CppUnit에 대해서 간략한(?)하게 테스트 케이스 작성 방법에 대해서 요약해 보았습니다. 원문은 http://cppunit.sourceforge.net/doc/1.9.11/cppunit_cookbook.html#cppunit_cookbook 에서 확인 하실 수 있습니다.

Simple Test Case

코드가 정상적으로 작동하고 있는지를 알아보기 위해서는 다양한 방법이 있습니다. 디버거를 사용하여 코드를 일일이 따라가 보거나, 특정 위치에서 문자를 출력 하도록 하는 것도 간단하고 좋은 방법 중에 하나임에는 분명합니다. 하지만 다소 뒤떨어진 방법임에도 분명하지요. 자동화 되지 않은 방법이기도 하며, 텍스트 라인을 일일이 출력하기 위해서는 코드도 더러워 집니다. 게다가 여러분이 원하지 않는 쓸모 없는 데이터들 까지 출력하느라 프로그램은 정신 없어 지지요. 하지만 CppUnit은 테스트라는 귀찮은 작업을 자동화 해 줄 수 있습니다. 설치하기도 쉬울 뿐만 아니라 일단 테스트 코드를 작성하고 나면 언제 든지 당신을 위해서 테스트를 가동해 줍니다.

저 본격적으로 테스트 케이스들을 만들기 전에 간단한 테스트를 만들어 보며 몸을 풀어 보도록 하겠습니다. 모든 테스트케이스의 작성은 TestCase라는 클래스를 상속 받는 것을 시작으로 합니다. 그리고 runTest()를 오버라이드 해주어야만 합니다. 당연히 이 runTest()에는 테스트 코드를 작성해 주어야 합니다. 그리고 함수 또는 메소드의 리턴되는 값이 우리가 원하는 값인지 확인 하기 위해서는 CPPUNIT_ASSERT(bool) 를 사용 합니다.

아래의 예제는 두 Complex number 클래스가 동일한지에 대한 테스트를 하는 것입니다 :

#include "cppunit/TestCase.h"

class ComplexNumberTest : public CppUnit::TestCase {
public:
  ComplexNumberTest( std::string name ) : CppUnit::TestCase( name ) {}
 
  void runTest() {
    CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
    CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
  }
};

이상으로 매우 간단한 테스트 케이스 였습니다. 앞으로 여러분이 코드를 작성하시다 보면 하나의 객체에 이런 크고 작은 테스트케이스들을 많이 만드시게 될 것인데 이럴 때에는 fixture를 사용하시면 됩니다.

Fixture

'Fixture'라는 것은 테스트 케이스의 기반으로 사용되고 있는 객체들의 집합입니다.

그럼, fixture를 사용하는 방법을 배워 보도록 하겠습니다. 여러분이 지금 complex number 클래스를 개발 하고 있다고 가정 하도록 하겠습니다. 이름이 'Complex'인 빈 클래스를 정의하는 것으로 이야기의 시작을 장식하겠습니다.

class Complex {};

이제는 이전의 예제에서 보였던 ComplexNumberTest 클래스의 객체를 생성 해 봅시다. 코드를 컴파일하고 무슨 일이 생기는지 관찰 해 보세요. 아마도 여러분이 가장 처음에 볼 수 있는 것은 컴파일러 에러 일겁니다. 테스트 케이스에서는 '==' 오퍼레이터를 사용하는데, Complex 클래스에서는 그런것에 대한 것은 언급 되어있지 않지요? 알았다면 이제 코드를 고쳐 보도록 하겠습니다. 일단 '==' 오퍼레이터는 무조건 true를 리턴하도록 했습니다. 정상적인 코드는 아니지만 일단은 컴파일 되게하는 것에 온 힘을 쏟도록 하겠습니다.

bool operator==( const Complex &a, const Complex &b)
{
  return true;
}

다시 컴파일 해봅시다. 정상적으로 되나요? 그렇다면 이제 실행도 시켜보도록 하겠습니다. 아! 실행 방법에 대해서 말씀을 드리지 않았군요. 간단히 실행 파일을 만들면 됩니다. 더 풀어서 이야기 하자면  main() 함수 안에 CompleNumberTest 클래스의 객체를 만들고, runTest()를 호출하도록 하면 됩니다. 따로 실행 코드는 보여드리지 않아도 되겠지요? 준비 되셨나요? 그럼 이제 정말 실행을 하도록 하지요. 이번에는 컴파일은 되지만 테스트는 실패할 것 입니다. 원인은 무조건 'true'만 리턴 하는 '==' 오퍼레이터 덕분에 두 번째 CPPUNIT_ASSERT 테스트에서 실패 했기때문 입니다. 그렇다면 '일단 무조건 구현' 해 놓은 '==' 오퍼레이터를 수정해야 할 차례가 왔군요.

class Complex {
  friend bool operator ==(const Complex& a, const Complex& b);
  double real, imaginary;
public:
  Complex( double r, double i = 0 )
    : real(r), imaginary(i)
  {
  }
};

bool operator ==( const Complex &a, const Complex &b )
{
  return ( a.real == b.real )  &&  ( a.imaginary == b.imaginary );
}

자, 다시 컴파일하고 실행 시켜 보세요. 이제 여러분은 컴파일도 되고, 테스트도 통과하는 케이스를 완성하셨습니다.

간단하긴 하지만 처음으로 동작하는 테스트 케이스를 완성 했습니다. 이제 우리는 새로운 오퍼레이션과 새로운 테스트를 추가할 준비가 되었습니다. 바로 이 상황에서 'fixture'의 도움이 절실 해 집니다. 서너개의 Complex 객체를 생성하여 우리의 테스트에 사용해 보도록 하겠습니다. 그러기 위해서는 아래의 과정이 필요합니다.

  • fixture에 테스트 할 클래스를 멤버 변수로 추가 한다.
  • 그 변수들을 초기화 하기위해 setUp()를 오버라이드 한다.
  • setUp()에서 할당된 리소들을 해제하기 위해 tearDown()를 오버라이드 한다.
class ComplexNumberTest : public CppUnit::TestFixture {
private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }
};

일단 우리가 fixture를 완성하게 된다면, 이제 부터는 어떠한 복잡한 테스트 케이스도 추가 할 수 있습니다.

Test Case

Fixture를 이용해서 테스트를 작성하고 호출하기 위해서는 아래의 두 과정이 필요 합니다 :

  • Fixture 클래스의 메소드로써 테스트 케이스를 작성 합니다
  • 특정 메소드에 대해서 동작하는 TestCaller를 실행합니다 

아래의 코드는 몇 개의 테스트 케이스를 더 추가한 테스트용 클래스 입니다 :

class ComplexNumberTest : public CppUnit::TestFixture  {
private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};

TestCaller 클래스는 아래와 같이 생성되고 실행 됩니다. 모든 테스트 케이스가 아래와 같습니다 :

#include "cppunit/TestCaller.h"
#include "cppunit/TestResult.h"

CppUnit::TestCaller<ComplexNumberTest> test( "testEquality",
                                             &ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );

TestCaller클래스 생성자의 두번째 인자는 ComplexNumberTest 클래스의 메소드 주소 입니다. TestCaller 클래스가 run()을 실행 하면 지정된 메소드 역시 같이 실행 됩니다. 하지만 이것은 실질적으로 문제를 진단하는 별 도움이 되지 않습니다. 유용하게 쓸만한 것을 별로 보여주지 않거든요. 아니, 사실 아무것도 주지 않지요. 정상적으로 테스트를 끝마치게 되면 아무런 메시지도 출력하지 않고 프로그램은 종료 해 버립니다. 이 때 결과를 보다 알아 내기 위해 TestCaller 대신 유용하게 쓸 수 있는 것이 TestRunner 입니다. TestRunner에 대한 자세한 설명은 조금 뒤에 계속 하도록 하겠습니다.

Suite

지금 까지 우리는 단일 테스트 케이스를 작성하고 실행하는 방법에 대해서 알아 보았습니다. 하지만 어떻게 하면 여러분이 작성한 모든 테스트들을 한번에 실행 시킬 수 있을까요?

CppUnit은 이런 기능을 지원하기 위해 TestSuite이라는 클래스를 제공하고 있습니다. 한 가지 이상의 테스트를 수행하는 suite를 위해서 아래와 같이 하면 됩니다 :

#include "cppunit/TestCaller.h"
#include "cppunit/TestSuite.h"
#include "cppunit/TestResult.h"

CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                       "testEquality",
                       &ComplexNumberTest::testEquality ) );
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                       "testAddition",
                       &ComplexNumberTest::testAddition ) );
suite.run( &result );

TestSuites의 addTest 메소드는 Test 인터페이스를 구현한 어떠한 객체든지 인자로 받을 수 있습니다. 예를 들자면 여러분은 여러분의 코드에 TestSuite를 생성 할 수 있고, 저는 제 코드에 생성 할 수 있습니다. 그리고 우리는 여러분의 것과 제 것을 인자로 받는 TestSuite를 생성하여 모든 TestSuite를 포함하는 새로운 것을 만들 수 있습니다.

CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( ComplexNumberTest::suite() );
suite.addTest( SurrealNumberTest::suite() );
suite.run( &result );

TestRunner

여러개의 테스트 케이스를 동시에 실행 하는 방법에 대해서 알아 보았습니다. 그럼 이제는 무엇에 대해서 알아봐야 할 까요? 실행을 했으니 결과가 어떻게 나왔는지 궁금하지 않으십니까? 이번 장에서는 테스트의 결과를 어떻게 알아 낼 수 있는지에 대해서 알아 보도록 하겠습니다.

CppUnit에서는 실행 할 suite를 정의하고 결과를 보여 줄 수 있는 툴(tool)을 제공합니다. 그전에 여러분은 TestRunner에서 접근 할 수 있는 static 메소드 suite를 만들어야 합니다. 이 함수는 TestSuite 클래스의 포인터를 리턴 합니다.

이전의  ComplexNumberTest suite를 TestRunner가 이용 할 수 있게 하기 위해서는 아래와 같은 코드를 ComplexNumberTest 클래스에 추가 합니다 :

public:
  static CppUnit::Test *suite()
  {
    CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testEquality",
                                   &ComplexNumberTest::testEquality ) );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testAddition",
                                   &ComplexNumberTest::testAddition ) );
    return suiteOfTests;
  }

텍스트 버젼을 사용하기 위해서는, Main.cpp에 아래와 같은 헤더파일을 추가합니다 :

#include <cppunit/ui/text/TestRunner.h>
#include "ExampleTestCase.h"
#include "ComplexNumberTest.h"

그리고 main 함수 안에 addTest(CppUnit::Test *)를 호출하는 코드를 넣어 줍니다 :

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  runner.addTest( ExampleTestCase::suite() );
  runner.addTest( ComplexNumberTest::suite() );
  runner.run();
  return 0;
}

위의 테스트에서 모든 테스트가 통과 한다면 테스트 결과에 대한 유용한 정보들을 얻을 수 있습니다. 하지만 실패 한다면 아래의 메시지들이 출력 됩니다 :

  • 실패한 테스트 케이스의 이름
  • 테스트를 포함하고 있는 소스 파일의 이름
  • 실패가 발생한 라인 넘버
  • CPPUNIT_ASSERT()에 기록 되어 있던 문자열

CppUnit은 failures 와 errors를 구분합니다. Failure 는 예상가능하고 assertion에 의헤 체크가 가능한 반면 에러는 0으로 나누어서 발생하는 예외 처럼 예상이 불가능 합니다.

Helper Macros

fixture의 static suite() 메소드를 구현한다는 것은 지겹도록 반복적이고 에러를 발생 시킬 가능성이 농후한 작업입니다. Writing test fixture 매크로는 static suite() 메소드를 자동적으로 작성할 수 있게 하기 위해 만들어 졌습니다.

아래의 코드는 매크로를 사용하기 우해 다시 작성된 ComplexNumberTest 입니다 :

#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  {

첫 째, suite를 선언언 하고 클래스의 이름을 매크로에게 전달 합니다 :

CPPUNIT_TEST_SUITE( ComplexNumberTest );

static suite()에 의해 생성된 suite 메소드는 클래스 이름 뒤에 만들어 집니다. 이제 fixture의 각 테스트 케이스를 선언해 봅시다 :

CPPUNIT_TEST( testEquality );
CPPUNIT_TEST( testAddition );

마지막으로 suite의 마지막을 선언 합니다.  suite() 메소드는 CPPUNIT_TEST_SUITE_END 매크로에 정의 되어 있기 때문에 따로 선언 해줄 필요가 없습니다.:

마지막으로 아래의 코드 부분들은 변경되지 않았습니다 :

private:
  Complex *m_10_1, *m_1_1, *m_11_2;
protected:
  void setUp()
  {
    m_10_1 = new Complex( 10, 1 );
    m_1_1 = new Complex( 1, 1 );
    m_11_2 = new Complex( 11, 2 ); 
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};

Suite에 추가된 TestCaller의 이름들은 fixture의 이름과 메소드 이름을 합성하여 만들어 집니다. 현재 케이스에서 만들어지는 이름은 아마도 "ComplexNumberTest.testEquality" 와 "ComplexNumberTest.testAddition" 가 될 것입니다.

Helper macros는 assertion 작성하는데 많은 도움이 됩니다. 예를 들어 ComplexNumber객체가 0으로 나누기를 시도 했을 때 MathException을 던지는지에 대한 테스트 같은 것 말이죠 :

  • CPPUNIT_TEST_EXCEPTION(예외 발생을 기다리는 매크로)을 이용하여 테스트를 suite에 추가 합니다.
  • 테스트 케이스 메소드를 작성합니다
CPPUNIT_TEST_SUITE( ComplexNumberTest );
// [...]
CPPUNIT_TEST_EXCEPTION( testDivideByZeroThrows, MathException );
CPPUNIT_TEST_SUITE_END();

// [...]

  void testDivideByZeroThrows()
  {
    // The following line should throw a MathException.
    *m_10_1 / ComplexNumber(0);
  }

만일 기대하던 예외가 발생하지 않는다면, assertion실패가 보고 됩니다.

TestFactoryRegistry

TestFactoryRegistry는 아래의 두 가지 문제를 해결하기 위해서 만들어 졌습니다.was created to solve two pitfalls:

  • Fixture suite를 TestRunner에 추가 하는 것을 잊어버렸을 경우(특히 다른 파일에 있는 경우 잊기 쉽지요)
  • Test case의 헤더 파일들을 인클루드 하는 것이 컴파일의 병목이 될 경우(see previous example)

이럴경우 suite들을 초기화 타이밍에 TestFactoryRegistry에 등록 하는 것으로 해결 할 수 있습니다.

CompleNumber sute를 등록하기 위해서는 .cpp 파일에 아래의 코드를 추가합니다 :

위의 매크로는 static AutoRegisterSuite 변수를 선언하고, 그 변수는 생성 타이밍에 TestSuiteFactory를 TestFactoryRegistry에 등록합니다. TestSuiteFactory는 ComplexNumber::suite()가 리턴하는 TestSuite를 돌려 줍니다.

Text test runner를 이용하여 테스트를 실행 시키기 위해서, 더 이상의 fixture는 필요 없습니다 :

#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;

TestFactoryRegistry의 인스턴스를 얻어 옵니다 :

  CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();

그리고 TestFactoryRegistry(CPPUNIT_TEST_SUITE_REGISTRATION()를 이용해서 모든 test suite가 등록 되어져 있음)에 의해 만들어진 새로운 TestSuite 얻어 온 후 TestRunner에 추가 합니다 :

  runner.addTest( registry.makeTest() );
  runner.run();
  return 0;
}

Post-build check

지금 까지 테스트 케이스의 작성과 매크로의 사용법 까지 알아 보았습니다. 하지만 이렇게 작성된 unit test들을 어떻게 하면 우리의 빌드 프로세스에 통합 할 수 있을 까요? 사실 어플리케이션 빌드를 하고, 다시 테스트 코드를 빌드하는 것 보다는 어플리케이션을 빌드 할 때 모든 것이 같이 한꺼번에 되는 것이 편하잖아요? 이것을 하기 위해서 어플리케이션은 반드시 0이 아닌 어떠한 값을 리턴해야 합니다.

TestRunner::run() 는성공을 나타내기 위해서 boolean 값을 리턴 합니다. 우리의 메인 프로그램을 업데이트 시켜 볼까요? :

#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest( registry.makeTest() );
  bool wasSucessful = runner.run( "", false );
  return wasSucessful;
}

자 이제는 프로그램을 컴파일하고 실행 시켜 보도록 합시다.

Original version by Michael Feathers. Doxygen conversion and update by Baptiste Lepilleur.

Reference


유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!