MAIET 에서 공개한 UnitTest++ 코드 소개 TDD(UnitTest++)

몇가지 유닛 테스트 샘플 코드 소개

 | 프로그래밍
# Posted 2008/09/25 17:24 by
버드

저희 팀에서는 TDD로 코드를 작성하면서 유닛테스트도 함께 만들면서 개발하고 있습니다. TDD의 장점이야 많은 곳에서 언급하고 있으니 따로 얘기할 필요는 없겠지만, TDD, 유닛테스트를 처음 접하고서 어디서부터 어떻게 시작해야될 지 어려워하시는 분들이 많은 것 같네요. 여기서 간단하게나마 저희 팀에서 만든 유닛테스트 코드 몇 개를 소개해볼까 합니다. 이걸로 유닛테스트를 작성하시는데 조금이나마 도움이 되길 바래요. :)


○ 서버에서 지역 채팅하는 로직에 대한 테스트 코드입니다. MockLink라고 가상의 클라이언트를 만들고, 같은 위치에서 Player1이 채팅 메세지를 보냈을 때 Player2한테 실제로 채팅 메세지가 전달되었는지를 테스트합니다.

    TEST(SectorChat)
    {
        GUTHelper helper;      // 유닛 테스트를 도와주는 유틸 함수가 모아져 있는 클래스입니다.
        MockMap* pMap = helper.DefaultMockMap();  // 가상의 맵을 만듭니다.


        GEntityPlayer* pPlayer1 = helper.NewEntityPlayer(pMap, vec3(1000,1000,0));  // 플레이어1을 맵에 생성시킵니다.
        MockLink* pLink1 = helper.NewLink(pPlayer1);    // 플레이어1 객체와 클라이언트 연결 객체를 연결시킵니다.

        GEntityPlayer* pPlayer2 = helper.NewEntityPlayer(pMap, vec3(1000,1000,0));
        MockLink* pLink2 = helper.NewLink(pPlayer2);


        // 플레이어1 클라이언트로부터 패킷을 받는 부분입니다. 실제로 이 OnRecv함수 안에서 받은 패킷에 대한 모든 처리를 하게 됩니다.

        // 그래서 이 곳에서 플레이어로부터 채팅 메세지를 받아서 주위 플레이어들에게 똑같은 메세지를 전달해 주게 됩니다.
        pLink1->OnRecv(MC_MSG_SECTOR_REQ, 1, NEW_STR("Hi! Hello World~"));


        CHECK(pLink2->GetPacketCount() == 1);    // 플레이어2 클라이언트에게 패킷이 하나 날라갔는지 체크합니다. 저희는 패킷 한 단위를 커맨드라고 부릅니다. :)
        CHECK(pLink2->GetPacket(0).GetID() == MC_MSG_SECTOR);  // 플레이어2에게 보낸 첫번째 패킷이 채팅 메세지 패킷인지 체크합니다.
       
        const char* szMsg = pLink2->GetParam<const char*>(0, 1);
        CHECK(!strcmp(szMsg, "Hi! Hello World~"));  // 실제로 플레이어1이 보낸 메세지와 같은 메세지가 날라갔는지 확인합니다.

        pMap->Destroy();
        helper.ClearLinks();
    }

 


○ 캐스팅 시간이 없는 스킬을 쓸 때 스킬 상태를 체크하는 테스트 코드입니다.
 TEST(TestSkillPhaseCheck_WhenCastingTimeIsZero)
{
    GUTHelper helper;

    GSkillInfo skill_info;
    helper.SetMagicMissileSkillInfo(&skill_info, 1);
    skill_info.m_fCastingTime = 0.0f;

    float fElapsedTime = 0.0f;
    MockEntityNPC npc;
    npc.Create(GUnitTestUtil::NewUID());

    GMagicSkill magic_talent(&npc, &skill_info);
    magic_skill.Start();
    magic_skill.Update(0.1f);

    CHECK_EQUAL(magic_skill.GetPhase(), SKILL_PHASE_RUNNING);
}



○ 이건 파티에 가입했는지 확인하는 테스트 코드입니다. 좀 더 복잡하네요. :)

 

    // 파티 관련 테스트에서 공통으로 사용하는 Fixture입니다.

   // 파티 관련 테스트를 시작할 때 자동으로 Fixture의 생성자를 불러 테스트에 필요한 초기화 등을 해주고, 테스트가 끝나면 소멸자를 불러 테스트에 필요한 자원 해제 등을 하게됩니다.

    struct Fixture
    {
        Fixture()
        {

           // 테스트용 파티 로직을 담당하는 시스템, 파티 인스턴스를 관리하는 매니저를 할당 받습니다.
            m_pPartySystem = m_PartySystemWrapper.Get();
            m_pPartyManager = m_PartyManagerWrapper.Get();


            party.SetUID(MUID(1000, 0));
            uidPlayer1.Value = 101;
            uidPlayer2.Value = 102;
            uidPlayer3.Value = 103;
            pPlayerObject1 = CreateTestPlayerObject(uidPlayer1);
            pPlayerObject2 = CreateTestPlayerObject(uidPlayer2);
            pPlayerObject3 = CreateTestPlayerObject(uidPlayer3);
            pPlayer1 = pPlayerObject1->GetMockEntity();
            pPlayer2 = pPlayerObject2->GetMockEntity();
            pPlayer3 = pPlayerObject3->GetMockEntity();           
           
            gmgr.pPlayerObjectManager->AddPlayer(pPlayerObject1);
            gmgr.pPlayerObjectManager->AddPlayer(pPlayerObject2);
            gmgr.pPlayerObjectManager->AddPlayer(pPlayerObject3);

            nOldPartyMemberCounter = GConst::PARTY_LIMIT_MEMBER_COUNTER;

            m_pPartyManager->Clear();
        }

        ~Fixture()
        {
            gmgr.pPlayerObjectManager->DeletePlayer(pPlayerObject3->GetUID());
            gmgr.pPlayerObjectManager->DeletePlayer(pPlayerObject2->GetUID());
            gmgr.pPlayerObjectManager->DeletePlayer(pPlayerObject1->GetUID());
            GConst::PARTY_LIMIT_MEMBER_COUNTER = nOldPartyMemberCounter;
        }

        MockPlayerObject* CreateTestPlayerObject(MUID& uid)
        {
            MockPlayerObject* p = new MockPlayerObject(uid);
            p->Create();
            return p;
        }

        void SetPlayerName__Player1_And_Player2_IsSame()
        {
            const char* pszPlayer1Name = "pPlayer1";
            const char* pszPlayer3Name = "OtherPlayer3";
            strcpy_s(pPlayer1->GetPlayerInfo()->szName, pszPlayer1Name);
            strcpy_s(pPlayer2->GetPlayerInfo()->szName, pszPlayer1Name);    // Reconnected Player #1
            strcpy_s(pPlayer3->GetPlayerInfo()->szName, pszPlayer3Name);
        }
       
        GUTHelper        m_Helper;
        GParty party;  // 파티 인스턴스를 미리 할당해 놓습니다.
        GTestSysWrapper2<GPartySystem, TestPartySystem>        m_PartySystemWrapper;  // 전역적으로 사용하는 시스템 객체를 테스트용 객체로 바꿉니다.
        GTestMgrWrapper2<GPartyManager, TestPartyManager>    m_PartyManagerWrapper; // 전역적으로 사용하는 매니저 객체를 테스트용 객체로 바꿉니다.
        TestPartySystem*    m_pPartySystem;
        TestPartyManager*    m_pPartyManager;

        MockEntityPlayer* pPlayer1;
        MockEntityPlayer* pPlayer2;
        MockEntityPlayer* pPlayer3;
        MockPlayerObject* pPlayerObject1;
        MockPlayerObject* pPlayerObject2;
        MockPlayerObject* pPlayerObject3;
        MUID    uidPlayer1;  // MUID는 유니크 아이디입니다. int64
        MUID    uidPlayer2;
        MUID    uidPlayer3;
        int        nOldPartyMemberCounter;
    };

    // 파티 가입을 테스트하는 코드입니다.

    TEST_FIXTURE(Fixture, JoinParty)
    {   

        // 플레이어 1을 파티장으로 파티를 만듭니다. 실제로 성공했는지 테스트합니다.
        CHECK_EQUAL(m_pPartySystem->CreateParty(pPlayer1), CR_SUCCESS);


        // 플레이어2를 가입시키는 것을 테스트합니다.
        CHECK_EQUAL(m_pPartySystem->Join(pPlayer1, pPlayer2), CR_SUCCESS);


        // 자기 자신은 가입시키지 못하는 것을 테스트합니다.
        CHECK_EQUAL(m_pPartySystem->Join(pPlayer1, pPlayer1), CR_FAIL_PARTY_NOT_INVITE_SELF);    // 자기 자신을 초대할 수 없다.


        CHECK_EQUAL(m_pPartySystem->Join(pPlayer1, pPlayer2), CR_FAIL_PARTY_TARGET_ALEADY_HAS_PARTY);    // 이미 초대한 파티원을 또 초대할 수 없다.
        CHECK_EQUAL(pPlayer2->HasParty(), true);
    }



○ 이건 간단한 수학 함수를 테스트하는 코드입니다.

 

    TEST(TestMathFunctionTruncateToInt)
    {
        CHECK_EQUAL(GMath::TruncateToInt(0.0), 0);
        CHECK_EQUAL(GMath::TruncateToInt(5.6), 5);
        CHECK_EQUAL(GMath::TruncateToInt(13.2), 13);
        CHECK_EQUAL(GMath::TruncateToInt(13.2), 13);
        CHECK_EQUAL(GMath::TruncateToInt(-5.6), -6);
        CHECK_EQUAL(GMath::TruncateToInt(-2.1), -3);
        CHECK_EQUAL(GMath::TruncateToInt(-2.0), -2);
        CHECK_EQUAL(GMath::TruncateToInt(-2585858.0), -2585858);
        CHECK_EQUAL(GMath::TruncateToInt(-2533858.12312), -2533859);
    }

    // 경험치 테스트입니다.

    TEST(TestCalcLevelFromExp)
    {
        CHECK_EQUAL(GCalculator::CalcLevelFromExp(4550, 1), 1);
        CHECK_EQUAL(GCalculator::CalcLevelFromExp(7510, 1), 2);
        CHECK_EQUAL(GCalculator::CalcLevelFromExp(10500, 1), 3);

        GLevelTable lt;
        int64 exp = lt.GetRequiredTotalXP(18) - 1;
        CHECK_EQUAL(GCalculator::CalcLevelFromExp(exp, 1), 18);
        CHECK_EQUAL(GCalculator::CalcLevelFromExp(exp, 32), 32);
    }



○ 이건 클라이언트에서 처음 맵에 접속했을 때 주위 NPC들의 애니메이션 상태를 체크하는 테스트 코드입니다.

 

    TEST_FIXTURE(SimpleTestFixture, NPCAnimationCheckWhenEntry)
    {
        int nNPCID = 1;
        vec3 vPos = vec3(1000.0f, 1000.0f, 0.0f);
        MUID uidNPC = global.system->GenerateLocalUID();

       // 서버로부터 받은 NPC 정보입니다.
        TD_UPDATE_CACHE_NPC td_update_cache_npc;
        td_update_cache_npc.uid = uidNPC;

        td_update_cache_npc.nStatusFlag = 0;
        td_update_cache_npc.nNPCID = nNPCID;
        td_update_cache_npc.vPos = vPos;

        // 여기서 서버로부터 NPC 정보를 받게 됩니다.
        m_Helper.InNPCs(&td_update_cache_npc, 1, &m_NPCAniDataSet);


        // 인스턴스 매니저에 실제로 NPC인스턴스가 생겼는지 테스트합니다.
        XNonPlayer* pNPC = gg.omgr->FindNPC(uidNPC);
        CHECK(pNPC != NULL);

        if (pNPC == NULL) return;


        // nStatusFlag에 아무값이 없으므로 가만히 서있는 idle 애니메이션인지 테스트합니다.
        CHECK_EQUAL(pNPC->GetCurrMotion(), string(MOTION_NAME_NPC_IDLE));

        gg.game->Update(0.1f);


        // 틱이 지난 후에도 계속 idle 애니메이션인지 테스트합니다.
        CHECK_EQUAL(pNPC->GetCurrMotion(), string(MOTION_NAME_NPC_IDLE));

        // 달리는 상태 ------------
        uidNPC = global.system->GenerateLocalUID();

        td_update_cache_npc.uid = uidNPC;
        td_update_cache_npc.nStatusFlag = UNS_RUN;        // 달리는 상태
        td_update_cache_npc.fSpeed = 100.0f;
        td_update_cache_npc.vTarPos = vec3(0.0f, 0.0f, 0.0f);

        m_Helper.InNPCs(&td_update_cache_npc, 1, &m_NPCAniDataSet);
        gg.game->Update(0.1f);

        pNPC = gg.omgr->FindNPC(uidNPC);
        CHECK(pNPC != NULL);
        if (pNPC == NULL) return;

        // 달리는 중이므로 run 모션 테스트
        CHECK_EQUAL(pNPC->GetCurrMotion(), string(MOTION_NAME_NPC_RUN));

        // 걷는 상태 ------------
        uidNPC = global.system->GenerateLocalUID();

        td_update_cache_npc.uid = uidNPC;
        td_update_cache_npc.nStatusFlag = UNS_WALK;        // 걷는 상태
        td_update_cache_npc.fSpeed = 100.0f;
        td_update_cache_npc.vTarPos = vec3(0.0f, 0.0f, 0.0f);

        m_Helper.InNPCs(&td_update_cache_npc, 1, &m_NPCAniDataSet);
        gg.game->Update(0.1f);

        pNPC = gg.omgr->FindNPC(uidNPC);
        CHECK(pNPC != NULL);
        if (pNPC == NULL) return;


        // 걷는 중이므로 walk 모션
        CHECK_EQUAL(pNPC->GetCurrMotion(), string(MOTION_NAME_NPC_WALK));
    }



그리고 이러한 테스트 코드들은 누군가 커밋하는 순간 자동으로 테스트됩니다.
그리고 실패하면 바로 알려주죠.

ccnet.jpg


사실 유닛 테스트를 처음으로 도입하는 것은 쉽지 않습니다. 저희팀도 처음 도입하고서 꽤 많은 삽질을 했습니다만(저희팀의 표님 블로그 참고..), 지금은 유닛 테스트 없이는 개발하기 힘들 정도로 많은 도움을 얻고 있습니다.


처음 유닛테스트를 접하셨다면 다음 서적을 권해드립니다.

200412020003.jpg

TDD의 바이블입니다. :)


200411020003.gif

자바로 설명되어 있지만 실용주의 책답게 쉽고 간결하게 설명되어 있습니다.


5545228.jpg

레가시 코드에 테스트 코드 만드는 방법이 잘 설명되어 있습니다.


처음 TDD를 접하신 분들을 위한 몇가지 링크입니다.

  • LineReader TDD 동영상  - 김창준과 강규영님의 실제로 TDD로 개발하는 모습을 담은 동영상입니다.
  • Unit Testing Guidelines - Geotechnical Software Services의 유닛테스트 가이드라인입니다.
  • chromuim - 웹브라우져인 구글 크롬의 프로젝트 페이지입니다. 구글 크롬의 테스트 코드가 실제로 어떻게 만들어졌나 감상해 보세요. :)

핑백

덧글

  • 박PD 2010/03/15 09:42 # 답글

    좋은 정보 감사합니다. 대인배 회사 마이에트 화이팅~
  • 2014/08/06 13:03 # 삭제 답글

    보통 내가 블로그에 글을 읽지 않는다, 그러나 나는이 쓰기까지 매우 확인하고 이렇게 저를 강제로 말을하고 싶습니다! 당신의 쓰기의 맛이 저를 깜짝 놀라게하고있다. 감사합니다, 아주 좋은 게시 할 수 있습니다.
  • 獨孤 2014/09/11 04:05 # 삭제 답글

    와우! 이것은 우리가 지금까지이 주제에 걸쳐 도착했습니다 가장 유익한 블로그의 특정 하나가 될 수 있습니다. 기본적으로 환상적인. 그래서 저는 당신의 노력을 이해할 수있다 또한이 항목의 전문가입니다.
댓글 입력 영역