C++ 프로젝트에 단위 테스트 도입하기
Upcoming SlideShare
Loading in...5
×
 

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

on

  • 5,638 views

 

Statistics

Views

Total Views
5,638
Views on SlideShare
5,459
Embed Views
179

Actions

Likes
10
Downloads
70
Comments
0

3 Embeds 179

http://hyulim.tistory.com 148
http://lifeiscool.tistory.com 25
http://10.1.1.10 6

Accessibility

Upload Details

Uploaded via as Microsoft PowerPoint

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

  • 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 <gtestgtest.h>
    #pragma comment(lib, “gtest.lib”)
    int main(intargc, _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 => successtest => 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
    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

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

  • #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 => successtest => 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.
  • 의존성 제거
    클래스 간 복합적인 관계 의존성 존재
    테스트 저해 설계 : 파일, 스레드, 통신등 외부 의존물을 사용
    통합테스트 => 단위 테스트
  • 패킷 변환
    테스트 대상
    통신 담당
  • ...
    TEST( CPacketHandler, ProcessPacketSuccessWithItemBuyReq){
    CPacketHandlerPacketHandler;
    MSG_ITEM_BUY_REQ ItemBuyReq;
    boolbRet = PacketHandler.ProcessPacket( &ItemBuyReq, ItemBuyReq.Size );
    ASSERT_TRUE( bRET );
    }
  • class CPacketHandler
    {
    CConnectionManager* m_pConnectionManager;
    CPacketParser* m_pPacketParser;
    public:
    CPacketHandler()
    {
    m_pConnectionManager= new CConnectionManager();
    m_pPacketParser = new CPacketParser();
    ...
    }
    BOOL ProcessPacket ( const char * pBuf, size_tpacketSize )
    {
    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;
    }
    ...
    };
  • class CPacketParser
    {
    public:
    ......
    MSG_BASE* ParsePacket( const char* pBuf, size_tpacketSize )
    {
    MSG_BASE* pMsg = (MSG_BASE*)pBuf;
    ... // 검증 코드 및 데이터 채우기
    return pMsg;
    }
    ......
    };
  • class CConnectionManager
    {
    CSendQueue* m_pSendQueue;
    public:
    ......
    void SendPacket( MSG_BASE* pMsg )
    {
    // 실제로 메시지를 보내는 코드
    m_pSendQueue->PostMessage(pMsg);
    }
    ......
    };
  • class CConnectionManager
    {
    CSendQueue* m_pSendQueue;
    public:
    ......
    void SendPacket( MSG_BASE* pMsg )
    {
    // 실제로 메시지를 보내는 코드
    m_pSendQueue->PostMessage(pMsg);
    }
    ......
    };
  • 스텁(Stub)
    외부 의존물을 대신하기 위해 간접 계층 추가
  • 간접 계층
    외부 의존물에 접근하기 위해 인터페이스 추가
  • class CPacketHandler
    {
    IConnectionManager* m_pConnectionManager;
    CPacketParser* m_pPacketParser;
    public:
    CPacketHandler()
    {
    m_pConnectionManager= new CConnectionManager();
    m_pPacketParser = new CPacketParser();
    ...
    }
    BOOL ProcessPacket ( const char * pBuf, size_tpacketSize )
    {
    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;
    ...
    }
    ...
    };
    ...
    FakeConnectionManagerFakeManager;
    CPacketHandlerHandler( &FakeManager );
    ...
  • class CPacketHandler
    {
    IConnectionManager* m_pConnectionManager;
    ...
    public:
    SetConnectionManager
    (IConnectionManager* pManager)
    {
    m_pConnectionManager = pManager;
    }
    ...
    };
    ...
    FakeConnectionManagerFakeManager;
    CPacketHandler Handler;
    Handler.SetConnectionManager( &FakeManager );
    ...
    get, set 프로퍼티
    작성의 용이함
    필수적이지 않은 매개변수
  • 그 외 대표적 방법들
    매개변수 전달
    함수를 호출할 때 함께 의존물을 넣어준다.
    추상 팩토리
    실제 객체와 스텁을 생성하는 팩토리를 생성하는 추상 팩토리를 생성
  • 캡슐화 문제
    테스트 용이성을 높이기 위한 방법
    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메서드를오버라이드 하여 체크
  • class FakeConnectionManager: public IConnectionManager{
    public:
    intm_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 )
    {
    MockImplimpl;
    EXPECT_CALL( impl, GetPos() );
    EXPECT_CALL( impl, SetPos( _, _ ) );

    // calls function

    }
  • 함수 호출 횟수 측정
    TEST( MockExample, Expect_call )
    {
    MockImplimpl;
    EXPECT_CALL( impl, GetPos() )
    .Times( 3 );

    // calls function

    }
  • 리턴값 지정
    TEST( MockExample, Expect_call )
    {
    MockImplimpl;
    // ON_CALL( impl, GetPos() )
    EXPECT_CALL( impl, GetPos() )
    .WillByDefault(Return(50.0f));

    // calls function

    }
  • Class FakeConnectionManager: public IConnectionManager{
    public:
    intm_nCalled;
    MSG_BASE* m_pPacket;
    ...
    virtual void SendPacket( MSG_BASE* pPacket ){
    ++m_nCalled;
    m_pPacket = pPacket;
    }
    };
    TEST( PacketHandler, ProcessPacketWithItemBuyReq ){
    FakeConnectionManagerFakeManager;
    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);
    };
    intIsPacket(MSG_BASE *p){
    return (p != NULL) && (p->GetProtocol() == MSG_PROTOCOL_ITEM_BUY_ANS);
    }
    TEST( PacketHandler, ProcessPacketWithItemBuyReq ){
    FakeConnectionManagerFakeManager;
    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