프로그래밍 일반 2014. 12. 23. 16:25

C++ 유닛테스트

게임처럼 요구사항이 수시로 변하는 프로그램에서 유닛테스트를 적용하기란 쉽지가 않다.


개인적으로는 데이터 의존성 테스트나 누락여부를 체크하는 부분에 사용하는게 적당하다고 생각한다.


예로 아이템을 추가할 때 연관 설정 파일들 (사운드, 연출, 스크립트 등..)에 누락이 없는지 확인하는지 테스트


최대한 일반화 할 수 있는 것들을 케이스로 만들어 적용하면 데이터 스크립팅과 관련된 이슈들은 QA에 올린 다음 문제를 발견하고 원인을 파악하는 수고들을 덜 수 있다.


유닛 테스트 익스플로러가 지원되는 컴파일러를 사용 중이라면 문제가 없지만 그렇지 않은 경우 가볍게 사용할 수 있는 테스트 코드를 추가한다.






테스트 결과를 처리하고 수집하는 클래스만 따로 확인하면 아래의 코드를 보면 되고


/**

@brief 테스트의 결과를 처리하고 수집

테스트 객체 트리거

*/

class CTestResult

{

private :

void addSuccess(const CContext& ctxt) { m_success.push_back(ctxt) ; }

void addFailure(const CContext& ctxt) { m_failure.push_back(ctxt) ; }

void addError  (const CContext& ctxt) { m_error.push_back(ctxt)   ; }

public  :

// Helper Method

int totalRunCount() const { return (successCount() + errorCount() + failureCount()) ; }

int successCount()  const { return static_cast<int>(m_success.size()); }

int failureCount()  const { return static_cast<int>(m_failure.size()); }

int errorCount()    const { return static_cast<int>(m_error.size())  ; }


/// 현재 테스트가 모두 성공적이었는지를 체크한다.

bool wasSuccessful() const

{

if ( 0 == errorCount() && 0 == failureCount() )

return true ;

return false ;

}


/// 테스트 결과 초기화

void clear()

{

m_timer.clear()   ;

m_success.clear() ;

m_failure.clear() ;

m_error.clear()   ;

}


/// 테스트 구동 함수. 하나의 메서드에 대해서 구동을 하게 된다.

bool run(const CMethod& method)

{

// 테스트 함수 내부 for구문 처리용

bool bResult = false;

int iIterator = 0;

int iCancelIterator = -1;


m_timer.start() ;


do {


try

{

// 넘겨받은 테스트 함수를 호출한다.

bResult = method.execute(iIterator, iCancelIterator) ; 


CContext c("OK","",method.name(),"",0) ;

addSuccess(c) ;

}

catch(CFailureException& e)

{

addFailure(e.context()) ;

}

catch(std::exception& e)

{

CContext c(e.what(),"",method.name(),"",0) ;

addError(c) ;

}

catch(...)

{

CContext c("unexpected exception is occured","",method.name(),"",0) ;

addError(c) ;

}


iCancelIterator = iIterator;


} while (false == bResult);


m_timer.stop() ;

return true ;

}


/// 파일 포인터에 테스트 결과를 출력해주는 함수

friend inline void WriteResult(const CTestResult& test, FILE* fp);


/// 테스트 결과를 trace하는 함수

friend inline void TraceResult(const CTestResult& test);



private :

CTimer m_timer ; // 시간 측정용 객체


std::vector<CContext> m_success ; // 성공한 테스트 정보 객체

std::vector<CContext> m_failure ; // 실패한 테스트 정보 객체

std::vector<CContext> m_error   ; // 에러 테스트 정보 객체

} ;


유닛 테스트는 보통 테스트 assert를 사용해서 실패하면 결과를 기록하는데 테스트 케이스 구문에서 for 루프를 도는 경우 첫번째 assert 실패 시 해당 케이스는 테스트를 종료하게 된다.

그런 문제를 해결하기 위해 실패한 곳부터 다시 처리하도록 만든 부분이 do {} while 문으로 처리한 구문이다.


테스트 케이스는 CTestCase를 상속 받으면 된다.

아래는 사운드 테스트 케이스 예제..


#pragma once


#include "CppUnitTest.h"


namespace cppunit

{

namespace testcase

{

class CTestCaseDiceSound

: public cppunit::CTestCase

{

public:

CTestCaseDiceSound() {}

CTestCaseDiceSound(const CTestCaseDiceSound& rhs)

: CTestCase(rhs)

{

_append(&CTestCaseDiceSound::TestFuncCheckDiceThrowSound);

_append(&CTestCaseDiceSound::TestFuncCheckDiceSwingSound);

}


virtual CTest* clone() const override

{

return new CTestCaseDiceSound(*this);

}


/// 사운드 매핑 테이블 체크

bool TestFuncCheckDiceThrowSound(int& output_iterator, const int iCancelIterator);


/// 

bool TestFuncCheckDiceSwingSound(int& output_iterator, const int iCancelIterator);

};

}

}


/**

@brief 사운드 매핑 테이블 체크

*/

bool cppunit::testcase::CTestCaseDiceSound::TestFuncCheckDiceSwingSound( int& output_iterator, const int iCancelIterator )

{

output_iterator = 0;


for ( ..;

               ..;

++iter, ++output_iterator)

{

if (output_iterator <= iCancelIterator) continue;

    ... 

    ...

{

const bool bIsFind = strSwing != CString();


char buff[256] = {0,};

sprintf_s(buff, sizeof(buff), "[%d] find error - swing dice sound", pTempItemInfo->m_nItemCode);


// 여기가 중요하다.

// 테스트 assert 사용!!

assertTrue(buff, bIsFind);

}

}


return true;

}



케이스는 테스트 그룹에 포함된다.


/********************************************************************

purpose: 관련 테스트 그룹

*********************************************************************/

#pragma once


#include "CppUnitTest.h"

#include "TestCaseDiceSound.h"


namespace cppunit

{

namespace testgroup

{

class CTestGroupDice

: public CTestSuite

{

public :

CTestGroup_Dice()

{

addTest(std::auto_ptr<CTest>(new testcase::CTestCaseDiceSound));

  addTest(std::auto_ptr<CTest>(new 케이스));

  ..

  .. 

}

};

}

}


테스트를 실행하는 클래스 사용 예

// trace 출력

cppunit::CTestRunner::run(std::auto_ptr<cppunit::CTest>(new cppunit::testgroup::CTestGroupDice));


// 텍스트 출력

COleDateTime currentDate(COleDateTime::GetCurrentTime());

SYSTEMTIME tempTime;

currentDate.GetAsSystemTime(tempTime);


CStringA strFileName;

strFileName.Format("UnitTestResult_%4d%2d%2d_%2d%2d%2d.txt"

, tempTime.wYear

, tempTime.wMonth

, tempTime.wDay

, tempTime.wHour

, tempTime.wMinute

, tempTime.wSecond);


FILE* fp = fopen(strFileName.GetString(), "ab");

cppunit::CTestRunner::run(std::auto_ptr<cppunit::CTest>(new cppunit::testgroup::CTestGroupDice), fp);

fclose(fp);


테스트 결과 출력 예