최우영
위메이드 엔터테인먼트
강연자 소개

   최우영
   (주)위메이드 엔터테인먼트
     신규 게임 개발팀 - 서버 파트
   (주)라온 엔터테인먼트
     테일즈런너
     신규 게임 개발
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_True
c:....Swap_Test.cpp(124): error: Value of: x
  Actual: 30
Expected: 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
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Swap_Test
[ RUN      ] Swap_Test.Swap_True
c:....Swap_Test.cpp(124): error: Value of: x
  Actual: 30
Expected: 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
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Swap_Test
[ RUN      ] Swap_Test.Swap_True
c:....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()




                                                                                                       CSendQueue
CTcpSocket          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 Frame
work)
   목과 스텁 객체를 쉽게 생성할 수 있게 해
    주는 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

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

  • 1.
  • 2.
    강연자 소개  최우영  (주)위메이드 엔터테인먼트  신규 게임 개발팀 - 서버 파트  (주)라온 엔터테인먼트  테일즈런너  신규 게임 개발
  • 3.
  • 4.
    통합 테스트 VS단위 테스트  통합 테스트  둘 이상의 모듈을 하나의 그룹으로 테스트 하 는것  소프트웨어에 기대되는 결과를 확인 하는 것.  많은 단위들을 실행  단위 테스트  단일 단위를 분리하여 실행하는 테스트
  • 5.
    단위 테스트란  다른 코드를 호출한 후 몇 가지 가정이 성 립하는지 검사하는 코드  여기서 '단위(unit)'란 메서드나 함수를 의 미  X+Y=Z?
  • 6.
    좋은 단위 테스트  자동화  반복 실행  쉬운 구현  쉬운 실행  빠른 속도  로직 없는 테스트
  • 7.
    테스트 프레임워크  CppUnit, UnitTest++, TUT, …  사용하기 쉽고 신뢰할 수 있는 GoogleTest  http://code.google.com/p/googletest/  구글의 제품에 사용 중  Chromium, Protocol Buffers, … 등등
  • 8.
    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(); }
  • 9.
    테스트 문법의 기본  ASSERT_TRUE( ACTUAL ) ASSERT_FALSE( ACTUAL )  ASSERT_EQ( EXPECTED, ACTUAL ) ASSERT_NE( EXPECTED, ACTUAL )  ASSERT_FLOAT_EQ( EXPECTED, ACTUAL )  ASSERT_STREQ( EXPECTED, ACTUAL)
  • 10.
     테스트 작성 TEST(test_suite, test_case ) { ... ASSERT_XXX() ... }
  • 11.
    간단한 테스트의 작성  swap() 함수 구현  X, Y 를 인자로 받고 X = Y, Y = X 로 교홖
  • 12.
    테스트 파일 생성  Ex) Swap_Test.cpp  테스트 스위트, 케이스 이름 결정  보수의 용이성을 위해 테스트 이름을 정한다  테스트 스위트는 테스트 카테고리  케이스 이름은 세부적인 테스트를 나타낸다
  • 13.
    #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.
  • 14.
    #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  존재하지 않는 함수 호출로 빌드 실패
  • 15.
    #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  빌드 성공, 테스트 실패
  • 16.
    [==========] Running 1test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from Swap_Test [ RUN ] Swap_Test.Swap_True c:....Swap_Test.cpp(124): error: Value of: x Actual: 30 Expected: 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
  • 17.
    [==========] Running 1test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from Swap_Test [ RUN ] Swap_Test.Swap_True c:....Swap_Test.cpp(124): error: Value of: x Actual: 30 Expected: 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
  • 18.
    [==========] Running 1test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from Swap_Test [ RUN ] Swap_Test.Swap_True c:....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
  • 19.
    #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  테스트 통과
  • 20.
    [==========] 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.
  • 21.
    의존성 제거  클래스 간 복합적인 관계 의존성 존재  테스트 저해 설계 : 파일, 스레드, 통신등 외부 의존물을 사용  통합테스트 => 단위 테스트
  • 22.
    테스트 대상 패킷 변홖 CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() 통신 담당
  • 23.
    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 ); }
  • 24.
    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; } ... };
  • 25.
    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; } ...... };
  • 26.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() class CConnectionManager { CSendQueue* m_pSendQueue; public: ...... void SendPacket( MSG_BASE* pMsg ) { // 실제로 메시지를 보내는 코드 m_pSendQueue->PostMessage(pMsg); } ...... };
  • 27.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage() class CConnectionManager { CSendQueue* m_pSendQueue; public: ...... void SendPacket( MSG_BASE* pMsg ) { // 실제로 메시지를 보내는 코드 m_pSendQueue->PostMessage(pMsg); } ...... };
  • 28.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage()
  • 29.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage() CTcpSocket CThreadBase
  • 30.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CConnectionManager +SendPacket() CSendQueue +PostMessage() CTcpSocket CThreadBase CPacketArchieve
  • 31.
    스텁(Stub)  외부 의존물을 대신하기 위해 간접 계층 추가
  • 32.
    CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() CPacketHandler CPacketParser CConnectionManager +ProcessPacket() +ParsePacket() +SendPacket() CConnectionManager CSendQueue +SendPacket() +PostMessage() CTcpSocket CThreadBase CPacketArchieve
  • 33.
    간접 계층  외부 의존물에 접근하기 위해 인터페이스 추가
  • 34.
    CPacketHandler CPacketParser CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() +ProcessPacket() +ParsePacket() CConnectionManager IConnectionManager +SendPacket() +SendPacket() CSendQueue FakeConnectionManager CConnectionManager +PostMessage() +SendPacket() +SendPacket() CSendQueue CTcpSocket CThreadBase CPacketArchieve +PostMessage()
  • 35.
    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; } ... };
  • 36.
    스텁 주입하기  생성자  get, set 프로퍼티  매개변수  추상 팩토리
  • 37.
    생성자 주입  장점 class CPacketHandler {  테스트 코드의 가독성 향상 IConnectionManager* m_pConnectionManager;  해당 매개 변수가 필수임을 알림 ...  단점 public:  상호 참조의 경우 생성 문제 CPacketHandler(IConnectionManager* pManager) {  매개 변수의 개수가 늘어날 수록 m_pConnectionManager = pManager; 가독성, 관리용이성 악화 ... } ... }; ... FakeConnectionManager FakeManager; CPacketHandler Handler( &FakeManager ); ...
  • 38.
    get, set 프로퍼티  작성의 용이함 class CPacketHandler {  필수적이지 않은 매개변수 IConnectionManager* m_pConnectionManager; ... public: SetConnectionManager (IConnectionManager* pManager) { m_pConnectionManager = pManager; } ... }; ... FakeConnectionManager FakeManager; CPacketHandler Handler; Handler.SetConnectionManager( &FakeManager ); ...
  • 39.
    그 외 대표적방법들  매개변수 젂달  추상 팩토리  함수를 호출할 때 함께  실제 객체와 스텁을 생 의존물을 넣어준다. 성하는 팩토리를 생성 하는 추상 팩토리를 생 성
  • 40.
    캡슐화 문제  테스트 용이성을 높이기 위한 방법  Public  상속  Friend  조건부 컴파일(#ifdef)  상용 프레임워크
  • 41.
    픽스쳐 class fixture_name; TEST_F( fixture_name,case_name ) { ... ...// Some Tests ASSERT_XXX(); ... }
  • 42.
    class fixture_name :public testing::Test { void SetUp(); void TearDown(); }; TEST_F( fixture_name, case_name ) { ... ...// Some Tests ASSERT_XXX(); ... }
  • 43.
    목 객체(Mock Object)  단위 테스트의 통과, 실패를 판단하는 가 짜 객체  하나의 테스트에 하나의 목 객체 사용
  • 44.
    스텁 vs 목  스텁: 객체의 대체제. 테스트가 가능하도 록 의존물을 없애는 것  목: 테스트의 통과, 실패를 검증
  • 45.
    목 객체의 사용  CPacketHandler가 IConnectionManager:: SendPacket() 메서드를 호출하는지 확인  ConnectionManager의 인터페이스를 추 출, 스텁으로 교체.  스텁의 SendPacket 메서드를 오버라이드 하여 체크 CPacketHandler CPacketParser +ProcessPacket() +ParsePacket() IConnectionManager +SendPacket()
  • 46.
    class FakeConnectionManager: publicIConnectionManager{ 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() ); }
  • 47.
    격리 프레임워크(Isolation Frame work)  목과 스텁 객체를 쉽게 생성할 수 있게 해 주는 API의 모음  테스트의 반복 작성시 도움을 받을 수 있 다.  예상값(기대값)의 측정  함수 호출  호출 횟수  인자
  • 48.
    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() ); };
  • 49.
    함수 호출 측정 TEST(MockExample, Expect_call ) { MockImpl impl; EXPECT_CALL( impl, GetPos() ); EXPECT_CALL( impl, SetPos( _, _ ) ); … // calls function … }
  • 50.
    함수 호출 횟수측정 TEST( MockExample, Expect_call ) { MockImpl impl; EXPECT_CALL( impl, GetPos() ) .Times( 3 ); … // calls function … }
  • 51.
    리턴값 지정 TEST( MockExample,Expect_call ) { MockImpl impl; // ON_CALL( impl, GetPos() ) EXPECT_CALL( impl, GetPos() ) .WillByDefault(Return(50.0f)); … // calls function … }
  • 52.
    Class FakeConnectionManager: publicIConnectionManager{ 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() ); }
  • 53.
    class FakeConnectionManager: publicIConnectionManager{ 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 ); }
  • 54.
    팁  목 객체는 테스트 당 1개  목 객체, 테스트 객체를 제외한 모든 의존 물은 Stub으로 대체  ASSERT는 가급적 테스트 당 1개  목 객체를 재사용 : 목 객체 내부에 ASSERT 삽입 금지  모든 테스트는 격리해서 실행
  • 55.
    감사합니다  whoo24@gmail.com  Twitter: whoo24  Blog : http://blog.wychoe.net