53
생산적인 생산적인 개발을 개발을 위한 위한 지속적인 지속적인 테스트 테스트 남기룡 남기룡([email protected] [email protected] ) ㈜마이에트 마이에트 엔터테인먼트 엔터테인먼트

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

  • Upload
    -

  • View
    2.354

  • Download
    0

Embed Size (px)

Citation preview

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

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

남기룡남기룡(([email protected]@gmail.com))㈜㈜마이에트마이에트 엔터테인먼트엔터테인먼트

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

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

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

낭비낭비 제거제거

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

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

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

CruiseControl.NETCruiseControl.NET

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

CruiseControl.NETCruiseControl.NET

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

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

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

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

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

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

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

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

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

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

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

<?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>

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

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

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

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

FeedbackFeedback

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

사례사례

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

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

다.

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

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));

}

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

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);

}

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

Unit TestUnit Test

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

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));

}

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

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 { }

};

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

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

{

public:

virtual unsigned int GetNowTime()

{

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

}

virtual int RandomNumber(int nMin, int nMax)

{

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

}

};

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

타이밍타이밍, , 랜덤랜덤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;

}

};

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

타이밍타이밍, , 랜덤랜덤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());

}

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

싱글턴싱글턴, , 전역전역 변수변수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);

}

};

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

싱글턴싱글턴, , 전역전역 변수변수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());

}

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

Refactoring Test CodeRefactoring Test Code• Mock Object

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

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

Refactoring Test Code• UnitTestHelper

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

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

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

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

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

class FBaseItem;

class FBaseNPC;

class FBaseMap;

class FBaseNetClient;

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

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

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

public FBaseNetClient,

public FBasePlayer,

public FBaseMap,

public FBaseMapMgr,

public FBasePlayer

{

};

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

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());

}

}

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

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();

}

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

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

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

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

Database Unit TestDatabase Unit Test• 테스트 단계

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

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

3. 테스트 케이스 실행

4. 데이터 변경 검증

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

Seed Seed DataSetDataSet

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

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());

}

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

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

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

값인지 테스트

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

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

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

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

• 호환성 테스트

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

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

RecvPacketLocalEvent

SendPacket

Local복사복사복사복사

ReplayQueue

게임 루프ReplayQueue

복사복사복사복사

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

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

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

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

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

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

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

• 예제• 예제

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

Resource Resource ValidatorValidator

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

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

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

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

Runtime Runtime ValidatorValidator

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

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

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

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

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

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

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

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

Crash Dump ReporterCrash Dump Reporter

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

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

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

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

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

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

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

Crash Dump AnalyzerCrash Dump Analyzer

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

Crash Dump AnalyzerCrash Dump Analyzer

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

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

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

1. 1. 자동화자동화

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

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

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

감사합니다감사합니다

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

Q/AQ/[email protected]

http://mypage.sarang.net