c++ 프로젝트에 단위 테스트 도입하기

Post on 11-May-2015

6.431 Views

Category:

Travel

6 Downloads

Preview:

Click to see full reader

TRANSCRIPT

C++ 프로젝트에 단위 테스트 도입하기

최우영위메이드 엔터테인먼트

강연자 소개

최우영 ( 주 ) 위메이드 엔터테인먼트

신규 게임 개발팀 - 서버 파트 ( 주 ) 라온 엔터테인먼트

테일즈런너 신규 게임 개발

Test?

통합 테스트 VS 단위 테스트

통합 테스트 둘 이상의 모듈을 하나의 그룹으로 테스트 하는 것 소프트웨어에 기대되는 결과를 확인 하는 것 . 많은 단위들을 실행

단위 테스트 단일 단위를 분리하여 실행하는 테스트

단위 테스트란

다른 코드를 호출한 후 몇 가지 가정이 성립하는지 검사하는 코드

여기서 ' 단위 (unit)' 란 메서드나 함수를 의미

X + Y = Z ?

좋은 단위 테스트

자동화 반복 실행 쉬운 구현 쉬운 실행 빠른 속도 로직 없는 테스트

테스트 프레임워크

CppUnit, UnitTest++, TUT, … 사용하기 쉽고 신뢰할 수 있는 GoogleTest http://code.google.com/p/googletest/ 구글의 제품에 사용 중

Chromium, Protocol Buffers, … 등등

Gtest 사용하기

gtest 라이브러리 다운로드 gtest.h 파일 include 라이브러리 빌드 후 lib 파일 링크#include <gtest\gtest.h>#pragma comment(lib, “gtest.lib”)

int main(int argc, _TCHAR* argv[]){

::testing::InitGoogleTest(&argc, argv);

return RUN_ALL_TESTS();}

테스트 문법의 기본

ASSERT_TRUE( ACTUAL )ASSERT_FALSE( ACTUAL )

ASSERT_EQ( EXPECTED, ACTUAL )ASSERT_NE( EXPECTED, ACTUAL )

ASSERT_FLOAT_EQ( EXPECTED, ACTUAL )

ASSERT_STREQ( EXPECTED, ACTUAL)

테스트 작성TEST( test_suite, test_case ){

...ASSERT_XXX()...

}

간단한 테스트의 작성

swap() 함수 구현 X, Y 를 인자로 받고 X = Y, Y = X 로

교환

테스트 파일 생성 Ex) Swap_Test.cpp

테스트 스위트 , 케이스 이름 결정 보수의 용이성을 위해 테스트 이름을 정한다 테스트 스위트는 테스트 카테고리 케이스 이름은 세부적인 테스트를 나타낸다

#include <gtest\gtest.h> TEST( Swap_Test, Swap_True ){

}

[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from Swap_Test[ RUN ] Swap_Test.Swap_True[ OK ] Swap_Test.Swap_True (0 ms)[----------] 1 test from Swap_Test (0 ms total)

[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 1 test.

build => fail 존재하지 않는 함수 호출로 빌드 실패

#include <gtest\gtest.h> TEST( Swap_Test, Swap_True ){

int x = 30;int y = 15;swap( x, y );ASSERT_EQ( 15, x ) << "x must be 15";ASSERT_EQ( 30, y ) << "y must be 30";

}

build => successtest => failure

빌드 성공 , 테스트 실패

#include <gtest\gtest.h> void swap(int& x, int& y){}

TEST( Swap_Test, Swap_True ){int x = 30;int y = 15;swap( x, y );ASSERT_EQ( 15, x ) << "x must be 15";ASSERT_EQ( 30, y ) << "y must be 30";

}

[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from Swap_Test[ RUN ] Swap_Test.Swap_Truec:\....\Swap_Test.cpp(124): error: Value of: x Actual: 30Expected: 15x must be 15[ FAILED ] Swap_Test.Swap_True (0 ms)[----------] 1 test from Swap_Test (0 ms total)

[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 0 tests.[ FAILED ] 1 test, listed below:[ FAILED ] Swap_Test.Swap_True

1 FAILED TEST

[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from Swap_Test[ RUN ] Swap_Test.Swap_Truec:\....\Swap_Test.cpp(124): error: Value of: x Actual: 30Expected: 15x must be 15[ FAILED ] Swap_Test.Swap_True (0 ms)[----------] 1 test from Swap_Test (0 ms total)

[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 0 tests.[ FAILED ] 1 test, listed below:[ FAILED ] Swap_Test.Swap_True

1 FAILED TEST

[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from Swap_Test[ RUN ] Swap_Test.Swap_Truec:\....\Swap_Test.cpp(124): error: Value of: x Actual: 30Expected: 15x must be 15[ FAILED ] Swap_Test.Swap_True (0 ms)[----------] 1 test from Swap_Test (0 ms total)

[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 0 tests.[ FAILED ] 1 test, listed below:[ FAILED ] Swap_Test.Swap_True

1 FAILED TEST

…ASSERT_EQ( 15, x ) << "x must be 15“;…

build => successtest => pass

테스트 통과

#include <gtest\gtest.h> void swap(int& x, int& y){

int t = x;y = x;x = t;

}TEST( Swap_Test, Swap_True ){

int x = 30;int y = 15;swap( x, y );ASSERT_EQ( 15, x ) << "x must be 15";ASSERT_EQ( 30, y ) << "y must be 30";

}

[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from Swap_Test[ RUN ] Swap_Test.Swap_True[ OK ] Swap_Test.Swap_True (0 ms)[----------] 1 test from Swap_Test (0 ms total)

[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 1 test.

의존성 제거

클래스 간 복합적인 관계 의존성 존재 테스트 저해 설계 : 파일 , 스레드 , 통신등

외부 의존물을 사용 통합테스트 => 단위 테스트

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

통신 담당

패킷 변환테스트 대상

...TEST( CPacketHandler, ProcessPacketSuccessWithItemBuyReq){

CPacketHandler PacketHandler;

MSG_ITEM_BUY_REQ ItemBuyReq;

bool bRet = PacketHandler.ProcessPacket( &ItemBuyReq, ItemBuyReq.Size );

ASSERT_TRUE( bRET );}

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

class CPacketHandler {

CConnectionManager* m_pConnectionManager;CPacketParser* m_pPacketParser;

public:CPacketHandler() {

m_pConnectionManager = new CConnectionMan-ager();

m_pPacketParser = new CPacketParser();...

}

BOOL ProcessPacket ( const char * pBuf, size_t packetSize ) {

MSG_BASE* pMsg = m_pPacketParser->ParsePacket( pBuf, packetSize );

...if( pMsg->GetProtocol() ==

MSG_PROTOCOL_ITEM_BUY_REQ ) {

m_pConnectionManager->SendPacket( new PACKET_ITEM_BUY_ANS() );

return TRUE;}...return FALSE;

}...

};

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

class CPacketParser {public:

......

MSG_BASE* ParsePacket( const char* pBuf, size_t packetSize ) {

MSG_BASE* pMsg = (MSG_BASE*)pBuf;... // 검증 코드 및 데이터 채우기return pMsg;

}

......};

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

class CConnectionManager {

CSendQueue* m_pSendQueue;public:

......

void SendPacket( MSG_BASE* pMsg ) {

// 실제로 메시지를 보내는 코드m_pSendQueue->PostMessage(pMsg);

}

......};

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

class CConnectionManager {

CSendQueue* m_pSendQueue;public:

......

void SendPacket( MSG_BASE* pMsg ) {

// 실제로 메시지를 보내는 코드m_pSendQueue->PostMessage(pMsg);

}

......};

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

스텁 (Stub)

외부 의존물을 대신하기 위해 간접 계층 추가

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

간접 계층

외부 의존물에 접근하기 위해 인터페이스 추가

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

IConnectionManager

+SendPacket()

FakeConnectionManager

+SendPacket()

class CPacketHandler {

IConnectionManager* m_pConnectionManager;CPacketParser* m_pPacketParser;

public:CPacketHandler() {

m_pConnectionManager = new CConnectionManager();m_pPacketParser = new CPacketParser();...

}

BOOL ProcessPacket ( const char * pBuf, size_t packetSize ) {

MSG_BASE* pMsg = m_pPacketParser->ParsePacket( pBuf, pack-etSize );

...if( pMsg->GetProtocol() == MSG_PROTOCOL_ITEM_BUY_REQ ) {

m_pConnectionManager->SendPacket( new PACKET_ITEM_BUY_ANS() );

return TRUE;}...return FALSE;

}...

};

CPacketHandler

+ProcessPacket()

IConnectionManager

+SendPacket()

FakeConnectionManager

+SendPacket()

CPacketParser

+ParsePacket()

스텁 주입하기

생성자 get, set 프로퍼티 매개변수 추상 팩토리

생성자 주입

장점 테스트 코드의 가독성 향상 해당 매개 변수가 필수임을 알림

단점 상호 참조의 경우 생성 문제 매개 변수의 개수가 늘어날 수록

가독성 , 관리용이성 악화

class CPacketHandler {

IConnectionManager* m_pConnectionManager;...

public:CPacketHandler(IConnectionManager* pMan-

ager) {

m_pConnectionManager = pMan-ager;

...}

...};

...FakeConnectionManager FakeManager;CPacketHandler Handler( &FakeManager );...

class CPacketHandler {

IConnectionManager* m_pConnectionManager;...

public:SetConnectionManager

(IConnec-tionManager* pManager)

{m_pConnectionManager = pMan-

ager;}

...};

...FakeConnectionManager FakeManager;CPacketHandler Handler;Handler.SetConnectionManager( &FakeManager );...

get, set 프로퍼티

작성의 용이함 필수적이지 않은 매개변수

그 외 대표적 방법들

매개변수 전달 함수를 호출할 때 함께

의존물을 넣어준다 .

추상 팩토리 실제 객체와 스텁을

생성하는 팩토리를 생성하는 추상 팩토리를 생성

캡슐화 문제

테스트 용이성을 높이기 위한 방법 Public 상속 Friend 조건부 컴파일 (#ifdef) 상용 프레임워크

픽스쳐

class fixture_name;

TEST_F( fixture_name, case_name ){

...

...// Some TestsASSERT_XXX();...

}

class fixture_name : public testing::Test{

void SetUp();void TearDown();

};

TEST_F( fixture_name, case_name ){

...

...// Some TestsASSERT_XXX();...

}

목 객체 (Mock Object)

단위 테스트의 통과 , 실패를 판단하는 가짜 객체

하나의 테스트에 하나의 목 객체 사용

스텁 vs 목

스텁 : 객체의 대체제 . 테스트가 가능하도록 의존물을 없애는 것

목 : 테스트의 통과 , 실패를 검증

목 객체의 사용

CPacketHandler 가 IConnectionMan-ager::SendPacket() 메서드를 호출하는지 확인

ConnectionManager 의 인터페이스를 추출 , 스텁으로 교체 .

스텁의 SendPacket 메서드를 오버라이드 하여 체크 CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

IConnectionManager

+SendPacket()

class FakeConnectionManager: public IConnectionManager{public:

int m_nCalled;MSG_BASE* m_pPacket;...

virtual void SendPacket( MSG_BASE* pPacket ){++m_nCalled;m_pPacket = pPacket;

}};

TEST( PacketHandler, ProcessPacketWithItemBuyReq ){...Handler.ProcessPacket( &ItemBuyReq );

ASSERT_EQ( 1, FakeManager.m_nCalled );ASSERT_EQ( MSG_PROTOCOL_ITEM_BUY_ANS, FakeManager.m_pPacket->Get-

Protocol() );}

격리 프레임워크 (Isolation Framework)

목과 스텁 객체를 쉽게 생성할 수 있게 해주는 API 의 모음

테스트의 반복 작성시 도움을 받을 수 있다 . 예상값 ( 기대값 ) 의 측정

함수 호출 호출 횟수 인자

Google Mock 사용법

목 오브젝트 생성

class Impl{public:

virtual void SetPos(float x, float y);virtual float GetX() const;

}

class MockImpl: public Impl {public:

MOCK_METHOD2( SetPos, void( float x, float y );MOCK_CONST_METHOD0( GetX, float() );

};

함수 호출 측정

TEST( MockExample, Expect_call ){

MockImpl impl;EXPECT_CALL( impl, GetPos() );EXPECT_CALL( impl, SetPos( _, _ ) );…// calls function…

}

함수 호출 횟수 측정

TEST( MockExample, Expect_call ){

MockImpl impl;EXPECT_CALL( impl, GetPos() )

.Times( 3 );…// calls function…

}

리턴값 지정

TEST( MockExample, Expect_call ){

MockImpl impl;// ON_CALL( impl, GetPos() )EXPECT_CALL( impl, GetPos() )

.WillByDefault(Return(50.0f));…// calls function…

}

Class FakeConnectionManager: public IConnectionManager{public:

int m_nCalled;MSG_BASE* m_pPacket;...

virtual void SendPacket( MSG_BASE* pPacket ){++m_nCalled;m_pPacket = pPacket;

}};

TEST( PacketHandler, ProcessPacketWithItemBuyReq ){FakeConnectionManager FakeManager;

PacketHandler Handler(&FakeManager);...Handler.ProcessPacket( &ItemBuyReq );

ASSERT_EQ( 1, FakeManager.m_nCalled );ASSERT_EQ( MSG_PROTOCOL_ITEM_BUY_ANS, FakeManager.m_pPacket->Get-

Protocol() );}

class FakeConnectionManager: public IConnectionManager{public:

MOCK_METHOD1( SendPakcet, void(MSG_BASE* pPacket);};

int IsPacket(MSG_BASE *p){return (p != NULL) && (p->GetProtocol() ==

MSG_PROTOCOL_ITEM_BUY_ANS);}

TEST( PacketHandler, ProcessPacketWithItemBuyReq ){FakeConnectionManager FakeManager;EXPECT_CALL( FakeManager, SendPacket( Truly(IsPacket) ) )

.Times( AtLeast(1) );

PacketHandler Handler( &FakeManager );...Handler.ProcessPacket( &ItemBuyReq );

}

목 객체는 테스트 당 1 개 목 객체 , 테스트 객체를 제외한 모든

의존물은 Stub 으로 대체 ASSERT 는 가급적 테스트 당 1 개 목 객체를 재사용 : 목 객체 내부에 AS-

SERT 삽입 금지 모든 테스트는 격리해서 실행

감사합니다

whoo24@gmail.com Twitter: whoo24 Blog : http://blog.wychoe.net

top related