생산적인 개발을 위한 지속적인 테스트

Post on 17-Jul-2015

2.354 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

생산적인생산적인 개발을개발을 위한위한지속적인지속적인 테스트테스트

남기룡남기룡((birdkr@gmail.combirdkr@gmail.com))㈜㈜마이에트마이에트 엔터테인먼트엔터테인먼트

효율적으로효율적으로개발하려면개발하려면??개발하려면개발하려면??

낭비낭비 제거제거

빠른빠른 피드백이피드백이 중요중요

CruiseControl.NETCruiseControl.NET

CruiseControl.NETCruiseControl.NET

CruiseControl.NETCruiseControl.NET이이 하는하는 일일• 소스 빌드• Asset 통합• 유닛 테스트통합 테스트 등의 각종 테스트• 통합 테스트 등의 각종 테스트

• 코드 정적 분석• 크래쉬 덤프 분석• 문서화(doxygen)• 배포

테스트에테스트에노력을노력을 기울이면기울이면??노력을노력을 기울이면기울이면??

1. 1. 위험을위험을 감소시키고감소시키고 품질을품질을 높인다높인다..

2. 2. 변경에변경에 강해진다강해진다..

33. . 프로젝트프로젝트 가시성을가시성을 높이고높이고, , 자신감을자신감을 얻을얻을 수수 있게있게 한다한다..

테스트테스트 결과결과 로그로그

<?xml version="1.0"?><maiettest-results tests=“2" failedtests=“1" failures="1" time="0.137">

<report text="time : 680 sec" /><report text=“총 클라이언트 개수 : 4650" /><test name=“로그인 반복" time="0.062" >

<success message="success" /><success message="success" /></test><test name=“캐릭터 생성 반복" time="0.062" >

<failure message="Crash!" /></test>

</maiettest-results>

테스트테스트 결과결과 로그로그

• 테스트 결과를 XML로 만들고, XSL을 이용하여 CruseControl.NET에 출력한다.

FeedbackFeedback

사례사례

Unit TestUnit Test• 코드에 대한 논리적인 검사• 모든 테스트 중에서 제일 중요하

다.

Unit TestTEST(TestMathFunctionTruncateToInt)

{

CHECK_EQUAL(0, GMath::TruncateToInt(0.0));

CHECK_EQUAL(5, GMath::TruncateToInt(5.6));

CHECK_EQUAL(13, GMath::TruncateToInt(13.2));

CHECK_EQUAL(13, GMath::TruncateToInt(13.2));

CHECK_EQUAL(-6, GMath::TruncateToInt(-5.6));CHECK_EQUAL(-6, GMath::TruncateToInt(-5.6));

CHECK_EQUAL(-3, GMath::TruncateToInt(-2.1));

}

Unit TestUnit TestTEST(ShieldCanBeDamaged)

{

World world;

world.Create();

Player player;

player.Create(world, vec3(1000,1000,0));

player.SetHealth(1000);player.SetHealth(1000);

Shield shield;

shield.SetHealth(100);

player.Equip(shield);

player.Damage(200);

CHECK(shield.GetHealth() == 0);

CHECK(player.GetHealth() == 900);

}

Unit TestUnit Test

Unit TestUnit TestTEST_FIXTURE(FLogin, TestLogin_MC_COMM_REQUEST_LOGIN_SERVER_Success){

TD_LOGIN_INFOTD_LOGIN_INFOTD_LOGIN_INFOTD_LOGIN_INFO tdLoginInfotdLoginInfotdLoginInfotdLoginInfo = = = = MakeParam_TD_LOGIN_INFOMakeParam_TD_LOGIN_INFOMakeParam_TD_LOGIN_INFOMakeParam_TD_LOGIN_INFO();();();();OnRecv_MMC_COMM_REQUEST_LOGIN_SERVEROnRecv_MMC_COMM_REQUEST_LOGIN_SERVEROnRecv_MMC_COMM_REQUEST_LOGIN_SERVEROnRecv_MMC_COMM_REQUEST_LOGIN_SERVER((((m_nRequestIDm_nRequestIDm_nRequestIDm_nRequestID, , , , m_nConnectionKeym_nConnectionKeym_nConnectionKeym_nConnectionKey, &, &, &, &tdLoginInfotdLoginInfotdLoginInfotdLoginInfo););););

// 로그인 하기 전의 값 체크CHECK_EQUAL(0, gmgr.pPlayerObjectManager->GetPlayersCount());

// 클라이언트로부터 존 입장 패킷 받음OnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVER((((m_nConnectionKeym_nConnectionKeym_nConnectionKeym_nConnectionKey););););OnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVEROnRecv_MC_COMM_REQUEST_LOGIN_SERVER((((m_nConnectionKeym_nConnectionKeym_nConnectionKeym_nConnectionKey););););

// 마스터 서버로 인증키 확인 패킷 보냈는지 체크CHECK_EQUALCHECK_EQUALCHECK_EQUALCHECK_EQUAL(MC_COMM_RESPONSE_LOGIN_SERVER, (MC_COMM_RESPONSE_LOGIN_SERVER, (MC_COMM_RESPONSE_LOGIN_SERVER, (MC_COMM_RESPONSE_LOGIN_SERVER, m_pLinkm_pLinkm_pLinkm_pLink---->>>>GetCommandIDGetCommandIDGetCommandIDGetCommandID(0));(0));(0));(0));CHECK_EQUALCHECK_EQUALCHECK_EQUALCHECK_EQUAL(RESULT_SUCCESS, (RESULT_SUCCESS, (RESULT_SUCCESS, (RESULT_SUCCESS, m_pLinkm_pLinkm_pLinkm_pLink---->>>>GetParamGetParamGetParamGetParam<<<<intintintint>(0, 0));>(0, 0));>(0, 0));>(0, 0));CHECK_EQUALCHECK_EQUALCHECK_EQUALCHECK_EQUAL((((m_pLinkm_pLinkm_pLinkm_pLink---->>>>GetUIDGetUIDGetUIDGetUID(), (), (), (), m_pLinkm_pLinkm_pLinkm_pLink---->>>>GetParamGetParamGetParamGetParam<<<<MUIDMUIDMUIDMUID>(0, 1));>(0, 1));>(0, 1));>(0, 1));

// 로그인 후 값 체크CHECK_EQUAL(1, gmgr.pPlayerObjectManager->GetPlayersCount());

GPlayerObject* pPlayerObject = gmgr.pPlayerObjectManager->GetPlayer(m_pLink->GetUID());CHECK(pPlayerObject != NULL);CHECK_EQUAL(m_nGUID, pPlayerObject->GetAccountInfo().nGUID);CHECK_EQUAL(string(“birdkr”), string(pPlayerObject->GetAccountInfo().strID));

}

Mock ObjectMock Objectclass MockPlayer : public GPlayer

{

public:

MockPlayer() {};

virtual ~ MockPlayer() {};

virtual void SendToThisSector (MPacket* pPacket) override { }virtual void SendToThisSector (MPacket* pPacket) override { }

virtual void SendToMe(MPacket * pPacket) override { }

virtual void SendToGuild(MPacket* pPacket) override { }

};

타이밍타이밍, , 랜덤랜덤class XSystem

{

public:

virtual unsigned int GetNowTime()

{

return timeGetTimetimeGetTimetimeGetTimetimeGetTime()()()(); return timeGetTimetimeGetTimetimeGetTimetimeGetTime()()()();

}

virtual int RandomNumber(int nMin, int nMax)

{

return (randrandrandrand()()()() % (nMax - nMin + 1)) + nMin;

}

};

타이밍타이밍, , 랜덤랜덤class MockSystem : public XSystem

{

protected:

unsigned int m_nExpectedNowTime;

public:

virtual unsigned int GetNowTime()

{{

if (m_nExpectedNowTime != 0)

return m_nExpectedNowTime;

return XSystem::GetNowTime();

}

void ExpectNowTime(unsigned int nNowTime)

{

m_nExpectedNowTime = nNowTime;

}

};

타이밍타이밍, , 랜덤랜덤TEST_FIXTURE(FPlayerInOut2, TestObjectCacheDelete)

{

vec3 vNewPos = vec3(100.0f, 100.0f, 0.0f);

CHECK_EQUAL(2, gg.omgr->GetCount());

m_pNet->OnRecv( MC_ENTITY_WARP, 3, NEW_ID(m_pMyPlayer->GetID

Update(0.1f);

CHECK_CLOSE(100.0f, m_pMyPlayer->GetPosition().x, 0.001f);

CHECK_CLOSE(100.0f, m_pMyPlayer->GetPosition().y, 0.001f);

XExpectNowTime(XGetNowTime() + 10000 );

Update(10.0f);

// 멀리 있는 다른 플레이어가 지워졌다.

CHECK_EQUAL(1, gg.omgr->GetCount());

}

싱글턴싱글턴, , 전역전역 변수변수template <class Type>

class GTestMgrWrapper : public MInstanceChanger<Type>

{

public:

GTestMgrWrapper() : MInstanceChanger()

{

m_pOriginal = gmgr.Change(m_pTester);m_pOriginal = gmgr.Change(m_pTester);

}

~GTestMgrWrapper()

{

gmgr.Change(m_pOriginal);

}

};

싱글턴싱글턴, , 전역전역 변수변수TEST_FIXTURE(FChangeMode, TestNPC_SightRange)

{

GTestMgrWrapper<GNPCInfoMgr> m_NPCInfoMgrWrapper;

m_NPCInfo.nSightRange = 1000;

GNPC* pNPC = m_pMap->SpawnTestNPC(&m_NPCInfo);GNPC* pNPC = m_pMap->SpawnTestNPC(&m_NPCInfo);

CHECK_EQUAL(1000, pNPC->GetSightRange());

pNPC->ChangeMode(NPC_MODE_1);

CHECK_EQUAL(500, pNPC->GetSightRange());

}

Refactoring Test CodeRefactoring Test Code• Mock Object

• override 키워드를 적극 활용• Google C++ Mocking Framework!

Refactoring Test Code• UnitTestHelper

– 자주 사용하는 함수들은 Helper로따로 분리한다.

– 예) GUTHelper_NPC::SpawnNPC()– 예) GUTHelper_NPC::SpawnNPC()

Refactoring Test Code• 자주 사용하는 Fixture는 체계적

으로 관리한다.class FBasePlayer;class FBasePlayer;

class FBaseItem;

class FBaseNPC;

class FBaseMap;

class FBaseNetClient;

Refactoring Test CodeRefactoring Test Code• 자주 사용하는 Fixture는 체계적

으로 관리한다.class FForCombatTest : public FBaseMockLink,

public FBaseNetClient,

public FBasePlayer,

public FBaseMap,

public FBaseMapMgr,

public FBasePlayer

{

};

Refactoring Test CodeRefactoring Test CodeClass Fduel // Fixture

{

void CHECK_DuelCancel()

{

CHECK_EQUAL(m_pLinkRequester->GetCommand(0).GetID(), MC_DUEL_CANCEL);

CHECK_EQUAL(m_pLinkTarget->GetCommand(0).GetID(), MC_DUEL_CANCEL);

}

void CHECK_DuelFinished(CPlayer* pWinner, CPlayer* pLoser)

{

MockLink* pWinnerLink = (pWinner==m_pPlayerRequester) ? M_pLinkRequester : m_pLinkT

const Mcommand& Command = pWinnerLink->GetCommand();

CHECK_EQUAL(Command.GetID(), MC_DUEL_FINISHED);

int nWinnerID, nLoserID;

Command.GetParam(&nWinnerID, 0, MPT_INT);

Command.GetParam(&nLoserID, 0, MPT_INT);

CHECK_EQUAL(nWinnerID, pWinner->GetID());

CHECK_EQUAL(nLoserID, pLoser->GetID());

}

}

Refactoring Test CodeRefactoring Test CodeTEST_FIXTURE(FDuel, DuelQuestionRefuse)

{

CHECK_EQUAL(gmgr.pDuelMgr->GetCount(), 0);

DuelRequest();

CHECK_EQUAL(gmgr.pDuelMgr->GetCount(), 1);

BeginCommandRecord();BeginCommandRecord();

DuelResponse(false);

CHECK_DuelCancel();

}

Database Unit TestDatabase Unit Test• 저장 프로시저, 트리거 등에 대한 유닛 테스트

• xDBUnit 프레임워크가 있지만 자체적으로작성했다.작성했다.– UnitTest++ 사용

Database Unit TestDatabase Unit Test• 테스트 단계

1. SandBox에 데이터베이스, Table, SP 등 생성

2. 테스트에 필요한 데이터 집합(Seed Dataset)을 생성

3. 테스트 케이스 실행

4. 데이터 변경 검증

Seed Seed DataSetDataSet

Unit Test CodeUnit Test CodeDBTEST(FGuildDB, CreateGuild)

{

UTestDB.Seed(“GuildTestSeedData.xml”);

uint32 nMasterCID = DBTestHelper::GetCID(“Acc5Char1”);

uint32 nMem1 = DBTestHelper::GetCID(“Acc5Char2”);

uint32 nMem2 = DBTestHelper::GetCID(“Acc5Char3”);

CHECK((0 != nMasterCID) && (0 != nMem1) && (0 != nMem2));

// 길드 생성

TDBRecordSet rs1;

UTestDB.Execute(rs1, “{CALL spGuildCreate (‘%S’, %d)}”, “TGuild4”, nMasterCID);

int nGID = rs1.Field(“GID”).AsInt();

CHECK(0 != nGID);

// 길드가 추가되었는지 레코드 개수 확인

TDBRecordSet rs2;

UTestDB.Execute(rs2, “SELECT COUNT(*) AS cnt FROM dbo.Guild;”);

CHECK_EQUAL(1, rs2.GetFetchedCount());

CHECK_EQUAL(1, rs2.Field(“cnt”).AsInt());

}

스크린샷스크린샷 테스트테스트• 특정 씬을 렌더링하여 원본 이미지와 같은

이미지인지 픽셀별로 비교하여 같은 픽셀

값인지 테스트

• 렌더링에 대한 UnitTest를 만들지 못하여• 렌더링에 대한 UnitTest를 만들지 못하여나온 대안

• 랜덤 요소 제거 등의 추가 작업이 필요함

리플레이리플레이 테스트테스트• 벤치마크 테스트와 유사한 방식• 기능 테스트• 안정성 테스트성능 테스트• 성능 테스트

• 호환성 테스트

리플레이리플레이 구현구현Command Queue

RecvPacketLocalEvent

SendPacket

Local복사복사복사복사

ReplayQueue

게임 루프ReplayQueue

복사복사복사복사

커맨드커맨드커맨드커맨드구조구조구조구조 ID Data

Resource Resource ValidatorValidator• 기획자나 아티스트가 작업한 게임 데이터

(Assets)에 논리적으로 잘못된 값이 입력되었는지 검증

• 예시• 예시– 상점 인터랙션이 설정된 NPC는 비전투형인가?– 아이템 판매 가격이 구매 가격보다 높은가?– 몬스터에 설정된 스킬 애니메이션 파일이 존재하는가?– 맵의 포탈에 연결된 맵이 실재로 존재하는가?

Resource Resource ValidatorValidator 구현구현• XML 파일은 일차적으로 Schema 검사• 검증 라이브러리를 따로 분리하여 각종 툴등에서 독립적으로 사용할 수 있도록 한다

• 예제• 예제

Resource Resource ValidatorValidator

Runtime Runtime ValidatorValidator• 서버 구동 중에 정적 분석으로 체크할 수없는 에러나 경고를 통지

• 종류– DB 쿼리 실패– DB 쿼리 실패– 성능 경고– AI 스크립트 오류– Assertion

Runtime Runtime ValidatorValidator

네트워크네트워크 테스트테스트• 클라이언트 패킷 핸들링을 XML로 쉽게 만들 수 있도록 한다.– 테스트 케이스

• 로그인 반복, 캐릭터 생성,삭제 반복, 이동 반복 등– 스케줄링

• 스트레스 테스트도 병행• Crash가 발생하거나 성능이 일정 수치 이하이면 테스트 실패로 간주

AI AI 테스트테스트• 각종 몬스터를 랜덤으로 생성시켜 서로 전투시킴

• 테스트 실패– Crash가 발생– Crash가 발생– 성능이 일정 수치 이하

Crash Dump ReporterCrash Dump Reporter

Crash Dump AnalyzerCrash Dump Analyzer• 프로그램에 치명적인 오류가 있을 경우 덤프 파일을 서버에 전송

• 수집된 덤프 파일을 함수 별로 분류• 정기적으로 새로 올라온 덤프 파일 목록을• 정기적으로 새로 올라온 덤프 파일 목록을개발자들에게 메일로 보고

Crash Dump Analyzer Crash Dump Analyzer 구현구현• 덤프 리포터• 심볼 서버 구축• WinDbg 의 Command-Line을 이용하여분석분석

• 최신 덤프 목록을 메일로 보내는 간단한툴 제작

Crash Dump AnalyzerCrash Dump Analyzer

Crash Dump AnalyzerCrash Dump Analyzer

지속적인지속적인테스트를테스트를 유지하려면유지하려면??테스트를테스트를 유지하려면유지하려면??

1. 1. 자동화자동화

2. 2. 테스트테스트 실패에실패에 대한대한 빠른빠른 대처대처2. 2. 테스트테스트 실패에실패에 대한대한 빠른빠른 대처대처

33. . 유지보수가유지보수가 가능하도록가능하도록 최대한최대한간단하게간단하게 제작제작

감사합니다감사합니다

Q/AQ/Abirdkr@gmail.com

http://mypage.sarang.net

top related