• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
C++ 프로젝트에 단위 테스트 도입하기
 

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

on

  • 2,521 views

 

Statistics

Views

Total Views
2,521
Views on SlideShare
1,851
Embed Views
670

Actions

Likes
2
Downloads
32
Comments
0

2 Embeds 670

http://ongameserver.tistory.com 667
http://www.hanrss.com 3

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

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

    • 최우영위메이드 엔터테인먼트
    • 강연자 소개 최우영 (주)위메이드 엔터테인먼트  신규 게임 개발팀 - 서버 파트 (주)라온 엔터테인먼트  테일즈런너  신규 게임 개발
    • 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 <gtestgtest.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 <gtestgtest.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.
    • #include <gtestgtest.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 => fail 존재하지 않는 함수 호출로 빌드 실패
    • #include <gtestgtest.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";} build => success test => failure 빌드 성공, 테스트 실패
    • [==========] 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_True1 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_True1 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: 30 …Expected: 15 ASSERT_EQ( 15, x ) << "x must be 15“;x 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
    • #include <gtestgtest.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";} build => success test => pass 테스트 통과
    • [==========] 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 CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() 통신 담당
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket()...TEST( CPacketHandler, ProcessPacketSuccessWithItemBuyReq){ CPacketHandler PacketHandler; MSG_ITEM_BUY_REQ ItemBuyReq; bool bRet = PacketHandler.ProcessPacket( &ItemBuyReq, ItemBuyReq.Size ); ASSERT_TRUE( bRET );}
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket()class CPacketHandler{ CConnectionManager CConnectionManager* m_pConnectionManager; +SendPacket() 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 CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket()class CPacketParser{public: ...... MSG_BASE* ParsePacket( const char* pBuf, size_t packetSize ) { MSG_BASE* pMsg = (MSG_BASE*)pBuf; ... // 검증 코드 및 데이터 채우기 return pMsg; } ......};
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket()class CConnectionManager{ CSendQueue* m_pSendQueue;public: ...... void SendPacket( MSG_BASE* pMsg ) { // 실제로 메시지를 보내는 코드 m_pSendQueue->PostMessage(pMsg); } ......};
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage()class CConnectionManager{ CSendQueue* m_pSendQueue;public: ...... void SendPacket( MSG_BASE* pMsg ) { // 실제로 메시지를 보내는 코드 m_pSendQueue->PostMessage(pMsg); } ......};
    • CPacketHandler CPacketParser+ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage()
    • CPacketHandler CPacketParser+ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage()CTcpSocket CThreadBase
    • CPacketHandler CPacketParser+ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage()CTcpSocket CThreadBase CPacketArchieve
    • 스텁(Stub) 외부 의존물을 대신하기 위해 간접 계층 추가
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CPacketHandler CPacketParser CConnectionManager+ProcessPacket() +ParsePacket() +SendPacket() CConnectionManager CSendQueue +SendPacket() +PostMessage() CTcpSocket CThreadBase CPacketArchieve
    • 간접 계층 외부 의존물에 접근하기 위해 인터페이스 추가
    • CPacketHandler CPacketParser CPacketHandler CPacketParser+ProcessPacket() +ParsePacket() +ProcessPacket() +ParsePacket() CConnectionManager IConnectionManager +SendPacket() +SendPacket() CSendQueue FakeConnectionManager CConnectionManager +PostMessage() +SendPacket() +SendPacket() CSendQueueCTcpSocket CThreadBase CPacketArchieve +PostMessage()
    • CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() IConnectionManager +SendPacket()class CPacketHandler{ IConnectionManager* m_pConnectionManager; FakeConnectionManager CPacketParser* m_pPacketParser; +SendPacket()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; } ...};
    • 스텁 주입하기 생성자 get, set 프로퍼티 매개변수 추상 팩토리
    • 생성자 주입 장점 class CPacketHandler {  테스트 코드의 가독성 향상 IConnectionManager* m_pConnectionManager;  해당 매개 변수가 필수임을 알림 ... 단점 public:  상호 참조의 경우 생성 문제 CPacketHandler(IConnectionManager* pManager) {  매개 변수의 개수가 늘어날 수록 m_pConnectionManager = pManager; 가독성, 관리용이성 악화 ... } ... }; ... FakeConnectionManager FakeManager; CPacketHandler Handler( &FakeManager ); ...
    • get, set 프로퍼티 작성의 용이함 class CPacketHandler { 필수적이지 않은 매개변수 IConnectionManager* m_pConnectionManager; ... public: SetConnectionManager (IConnectionManager* pManager) { m_pConnectionManager = pManager; } ... }; ... FakeConnectionManager FakeManager; CPacketHandler Handler; Handler.SetConnectionManager( &FakeManager ); ...
    • 그 외 대표적 방법들 매개변수 젂달  추상 팩토리  함수를 호출할 때 함께  실제 객체와 스텁을 생 의존물을 넣어준다. 성하는 팩토리를 생성 하는 추상 팩토리를 생 성
    • 캡슐화 문제 테스트 용이성을 높이기 위한 방법 Public 상속 Friend 조건부 컴파일(#ifdef) 상용 프레임워크
    • 픽스쳐class fixture_name;TEST_F( fixture_name, case_name ){ ... ...// Some Tests ASSERT_XXX(); ...}
    • class fixture_name : public testing::Test{ void SetUp(); void TearDown();};TEST_F( fixture_name, case_name ){ ... ...// Some Tests ASSERT_XXX(); ...}
    • 목 객체(Mock Object) 단위 테스트의 통과, 실패를 판단하는 가 짜 객체 하나의 테스트에 하나의 목 객체 사용
    • 스텁 vs 목 스텁: 객체의 대체제. 테스트가 가능하도 록 의존물을 없애는 것 목: 테스트의 통과, 실패를 검증
    • 목 객체의 사용 CPacketHandler가 IConnectionManager:: SendPacket() 메서드를 호출하는지 확인 ConnectionManager의 인터페이스를 추 출, 스텁으로 교체. 스텁의 SendPacket 메서드를 오버라이드 하여 체크 CPacketHandler CPacketParser +ProcessPacket() +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->GetProtocol() );}
    • 격리 프레임워크(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->GetProtocol() );}
    • 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개 목 객체를 재사용 : 목 객체 내부에 ASSERT 삽입 금지 모든 테스트는 격리해서 실행
    • 감사합니다 whoo24@gmail.com Twitter: whoo24 Blog : http://blog.wychoe.net