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

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

May 16, 2015

Download

Documents

OnGameServer
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: C++ 프로젝트에 단위 테스트 도입하기

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

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

강연자 소개

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

신규 게임 개발팀 - 서버 파트

(주)라온 엔터테인먼트

테일즈런너

신규 게임 개발

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

Test?

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

통합 테스트 VS 단위 테스트

통합 테스트

둘 이상의 모듈을 하나의 그룹으로 테스트 하는 것

소프트웨어에 기대되는 결과를 확인 하는 것.

많은 단위들을 실행

단위 테스트

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

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

단위 테스트란

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

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

X + Y = Z ?

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

좋은 단위 테스트

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

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

테스트 프레임워크

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

Chromium, Protocol Buffers, … 등등

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

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();

}

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

테스트 문법의 기본

ASSERT_TRUE( ACTUAL )ASSERT_FALSE( ACTUAL )

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

ASSERT_FLOAT_EQ( EXPECTED, ACTUAL )

ASSERT_STREQ( EXPECTED, ACTUAL)

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

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

...ASSERT_XXX()...

}

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

간단한 테스트의 작성

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

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

테스트 파일 생성

Ex) Swap_Test.cpp

테스트 스위트, 케이스 이름 결정

보수의 용이성을 위해 테스트 이름을 정한다

테스트 스위트는 테스트 카테고리

케이스 이름은 세부적인 테스트를 나타낸다

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

#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.

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

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";

}

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

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";

}

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

[==========] 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: xActual: 30

Expected: 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

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

[==========] 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: xActual: 30

Expected: 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

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

[==========] 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: xActual: 30

Expected: 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“;…

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

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";

}

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

[==========] 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.

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

의존성 제거

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

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

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

통신 담당

패킷 변홖테스트 대상

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

...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()

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

class CPacketHandler{

CConnectionManager* 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, 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()

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

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()

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

class CConnectionManager{

CSendQueue* m_pSendQueue;public:

......

void SendPacket( MSG_BASE* pMsg ) {

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

}

......};

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

class CConnectionManager{

CSendQueue* m_pSendQueue;public:

......

void SendPacket( MSG_BASE* pMsg ) {

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

}

......};

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

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

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

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

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

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

스텁(Stub)

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

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

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

간접 계층

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

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

CPacketHandler

+ProcessPacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

CTcpSocket CThreadBase

CPacketParser

+ParsePacket()

CPacketArchieve

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

CConnectionManager

+SendPacket()

CSendQueue

+PostMessage()

IConnectionManager

+SendPacket()

FakeConnectionManager

+SendPacket()

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

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, packetSize );...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()

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

스텁 주입하기

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

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

생성자 주입

장점

테스트 코드의 가독성 향상

해당 매개 변수가 필수임을 알림

단점

상호 참조의 경우 생성 문제

매개 변수의 개수가 늘어날 수록가독성, 관리용이성 악화

class CPacketHandler{

IConnectionManager* m_pConnectionManager;...

public:CPacketHandler(IConnectionManager* pManager) {

m_pConnectionManager = pManager;...

}...};

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

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

class CPacketHandler{

IConnectionManager* m_pConnectionManager;...

public:SetConnectionManager

(IConnectionManager* pManager) {

m_pConnectionManager = pManager;}...

};

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

get, set 프로퍼티

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

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

그 외 대표적 방법들

매개변수 젂달

함수를 호출할 때 함께의존물을 넣어준다.

추상 팩토리

실제 객체와 스텁을 생성하는 팩토리를 생성하는 추상 팩토리를 생성

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

캡슐화 문제

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

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

픽스쳐

class fixture_name;

TEST_F( fixture_name, case_name ){

...

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

}

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

class fixture_name : public testing::Test{

void SetUp();void TearDown();

};

TEST_F( fixture_name, case_name ){

...

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

}

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

목 객체(Mock Object)

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

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

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

스텁 vs 목

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

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

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

목 객체의 사용

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

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

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

CPacketHandler

+ProcessPacket()

CPacketParser

+ParsePacket()

IConnectionManager

+SendPacket()

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

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->GetProtocol() );

}

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

격리 프레임워크(Isolation Framework)

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

테스트의 반복 작성시 도움을 받을 수 있다.

예상값(기대값)의 측정

함수 호출

호출 횟수

인자

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

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() );

};

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

함수 호출 측정

TEST( MockExample, Expect_call ){

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

}

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

함수 호출 횟수 측정

TEST( MockExample, Expect_call ){

MockImpl impl;EXPECT_CALL( impl, GetPos() )

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

}

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

리턴값 지정

TEST( MockExample, Expect_call ){

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

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

}

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

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->GetProtocol() );

}

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

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 );

}

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

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

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

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

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

감사합니다

[email protected] Twitter: whoo24 Blog : http://blog.wychoe.net