UnitTest, Tdd For Games Kgc2007 ParkPD

3,653
-1

Published on

KGC2007 presentation file about UnitTest, TDD applying in game programming

0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
3,653
On Slideshare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
80
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

UnitTest, Tdd For Games Kgc2007 ParkPD

  1. 1. TDD, UnitTest for games 박일(NcSoft. Lineage II) http://ParkPD.egloos.com
  2. 2. 버그?
  3. 3. TDD 란? Test Driven Development 테스트가 개발을 운전(Driven)한다. Programmer Test 프로그래머가 직접 설치하는 자동화된 테스트 White Box Test QA 팀의 테스트는 Black Box Test
  4. 4. 테스트 실패 TDD의 순환 과정 불과 몇 분밖에 걸 리지 않는다. 테스트 코드 작성 작성 TEST (ShieldLevelStartsFull){ TEST (ShieldLevelStartsFull){ Shield shield; Shield shield; CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel()); CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel()); } } Shield::Shield() : m_level (Shield::kMaxLevel){ 체크 인 Shield::Shield() : m_level (Shield::kMaxLevel){ } } 체크 인 리팩토링 테스트 통과 테스트 통과
  5. 5. UnitTest++ 개발자 Noel Llopis Senior Architect High Moon Studios
  6. 6. 피보나치 수열 시연 피보나치 수열 0, 1, 1, 2, 3, 5, 8, 13, 21......의 형태의 수 열. 즉, 첫 번째 항의 값은 0 이고 두 번째 항 의 값은 1일 때 이후의 항들은 이전의 두 항 을 더한 값으로 만들어지는 수열을 말한다. 수열의 공식은 다음과 같다. fn = fn-1 + fn- 2 (단, f0 = 0, f1 = 1, n = 2, 3, 4, ....)
  7. 7. 피보나치 수열 1 fn = fn-1 + fn-2 (단, f0 = 0, f1 = 1, n = 2, 3, 4, ....) 재귀호출을 이용
  8. 8. 피보나치 수열 2
  9. 9. UnitTest++ 기능 TEST() TEST(AfterUserConnectToServerOnline) { CHECK() CHECK(0 < a.GetHP()) CHECK_EQUAL() CHECK_EQUAL(true, a.IsOnline()); CHECK_CLOSE() CHECK_CLOSE(15.42, a.GetAttackFactor(), 0.01); CHECK_ARRAY2D_CLOSE()
  10. 10. UnitTest++ 기능 1/3 FIXTURE TEST_FIXTURE JUnit 의 setUp, tearDown 과 같은 역할 예 : DB 테스트 TEST(DBTest) {{ TEST(DBTest1) {{ TEST(DBTest) TEST(DBTest1) SQL sql; SQL sql; SQL sql; SQL sql; sql.connect(); sql.connect(); sql.connect(); sql.connect(); // 실제 테스트 코드 // 실제 테스트 코드 // 실제 테스트 코드 // 실제 테스트 코드 sql.close(); sql.close(); sql.close(); sql.close(); }} }} struct FixtureSQL { FixtureSQL() { sql.connect(); } ~FixtureSQL() { sql.close() } SQL sql; }; TEST_FIXTURE (FixtureSQL, DBTest) { // sql.xxx 실제 테스트
  11. 11. UnitTest++ 기능 2/3 TimeConstraint 실행 시간이 일정 이상 지나면 테스트 fail 로 간주. TestResult r; TimeConstraint t(10, result, TestDetails(“”, “”, “”, 0); TimeHelpers::SleepMs(20); CHECK_EQUAL(1, result.GetFailureCount()); Crash 검사
  12. 12. UnitTest++ 기능 3/3 Suite Two Stage Test 1단계 리소스 로딩 이전에 로직 테스트, 순수한 의미의 UnitTest 2단계 리소스 로딩 후에 월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트 지형의 이동 가능 여부 등 성능 테스트 같은 함수를 100만번 부를 때 0.01초 내에 리턴되는지 검사 매번 검사하기 부담스러우므로 command 명령으로 가끔씩 수동 으로 테스트하기.
  13. 13. Unit Test 예제 world; World world; const initialHealth = 60; player(initialHealth); Player player(initialHealth); world.Add(&player, Transform(AxisY, world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10)); powerup; HealthPowerup powerup; world.Add(&powerup, Transform(AxisY, Vector3(- world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20); world.Update(0.1f); CHECK_EQUAL(initialHealth, player.GetHealth()); CHECK_EQUAL(initialHealth, player.GetHealth()); (PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup) TEST (PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup) { world; World world; const initialHealth = 60; Player player(initialHealth); player(initialHealth); world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10)); world.Add(&player, Transform(AxisY, powerup; HealthPowerup powerup; world.Add(&powerup, Transform(AxisY, Vector3(- world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20); world.Update(0.1f); CHECK_EQUAL(initialHealth, player.GetHealth()); CHECK_EQUAL(initialHealth, player.GetHealth()); }
  14. 14. TEST(ActorDoesntMoveIfPelvisBodyIsInSamePositionAsPelvisAnim) 최상의 관행: 간결한 검사 { component = ConstructObject<UAmpPhysicallyDrivableSkeletalComponent>(); component->physicalPelvisHandle = NULL; component->SetOwner(owner); component->SkeletalMesh = skelMesh; TEST (ShieldStartsAtInitialLevel) component->Animations = CreateReadable2BoneAnimSequenceForAmpRagdollGetup(component, skelMesh, { 10.0f, 0.0f); component->PhysicsAsset = physicsAsset; ShieldComponent shield(100); component->SpaceBases.AddZeroed(2); component->InitComponentRBPhys(false); CHECK_EQUAL (100, shield.GetLevel()); component->LocalToWorld = FMatrix::Identity; const FVector actorPos(100,200,300); } const FVector pelvisBodyPositionWS(100,200,380); const FTranslationMatrix actorToWorld(actorPos); owner->Location = actorPos; component->ConditionalUpdateTransform(actorToWorld); TEST (ShieldTakesDamage) INT pelvisIndex = physicsAsset->CreateNewBody(TEXT(quot;Bone1quot;)); { URB_BodySetup* pelvisSetup = physicsAsset->BodySetup(pelvisIndex); FPhysAssetCreateParams params = GetGenericCreateParamsForAmpRagdollGetup(); ShieldComponent shield(100); physicsAsset->CreateCollisionFromBone( pelvisSetup, skelMesh, shield.Damage(30); 1, params, CHECK_EQUAL (70, shield.GetLevel()); boneThings); } URB_BodyInstance* pelvisBody = component->PhysicsAssetInstance->Bodies(0); NxActor* pelvisNxActor = pelvisBody->GetNxActor(); SetRigidBodyPositionWSForAmpRagdollGetup(*pelvisNxActor, pelvisBodyPositionWS); TEST (LevelCannotDropBelowZero) component->UpdateSkelPose(0.016f); { component->RetransformActorToMatchCurrrentRoot(TransformManipulator()); ShieldComponent shield(100); const float kTolerance(0.002f); FMatrix expectedActorMatrix;shield.Damage(200); expectedActorMatrix.SetIdentity(); expectedActorMatrix.M[3][0] CHECK_EQUAL (0, shield.GetLevel()); = actorPos.X; } expectedActorMatrix.M[3][1] = actorPos.Y; expectedActorMatrix.M[3][2] = actorPos.Z; const FMatrix actorMatrix = owner->LocalToWorld(); CHECK_ARRAY2D_CLOSE(expectedActorMatrix.M, actorMatrix.M, 4, 4, kTolerance); }
  15. 15. 예시: 캐릭터의 행동 TEST_F( CharacterFixture, SupportedWhenLeapAnimationEndsTransitionsRunning ) { LandingState state(CharacterStateParameters(&character), AnimationIndex::LeapLanding); state.Enter(input); input.deltaTime = character.GetAnimationDuration( AnimationIndex::LeapLanding ) + kEpsilon; character.supported = true; CharacterStateOutput output = state.Update( input ); CHECK_EQUAL(std::string(quot;TransitionStatequot;), output.nextState->GetClassInfo().GetName()); const TransitionState& transition = *output.nextState; CHECK_EQUAL(std::string(quot;RunningStatequot;), transition.endState->GetClassInfo().GetName()); }
  16. 16. Working Effectively with Legacy Code 필요한 이유 Debugging Regression Test
  17. 17. 리니지2 리니지2 업데이트 일지 CHRONICLE 01 - 전란을 부르는 자들 CHRONICLE 02 - 풍요의 시대 CHRONICLE 03 - 눈뜨는 어둠 CHRONICLE 04 - 운명의 계승자들 CHRONICLE 05 - Death of Blood 혼돈의 왕좌 Interlude - 그 시작을 말하다 혼돈의 왕좌 - The kamael (2007) 계속되는 업데이트 & 변경되는 기획
  18. 18. 왜 개발자가 Test 까지? QA 팀이 있으신가요? 없는 회사가 대부분 QA 팀이 있어도 최고의 QA 팀이 있어도 버그는 막을 수 없다. Lineage2 팀의 QA 팀은 최고입니다. 마감직전에 발견되는 버그가 가장 큰 문제를 일으킨다. 결국 욕은 프로그래머가 먹고, 야근도 해야 한다. 미리 Test를 이용, 버그를 막아보자. 버그가 생기면 수익 감소 악플뿐 아니라 웹진기사가 뜨는 경우까지!
  19. 19. QA 팀은 역시 필요합니다. 수동 자동 사용성 테스팅 스토리 테스트 비즈니스 의도 탐색적 테스팅 (제품 설계) 단위 테스트 특성 테스팅 개발자 의도 보안 테스팅 부하 테스팅 (코드 설계) 조합 테스팅 … 자동 도구
  20. 20. Test Driven Debugging? 일반적인 디버깅 방법은? 1. 버그 리포트 시스템에 새로운 버그 추가 2. 게임 스크립트 데이타 받아서 컴파일 3. 서버들 빌드 후 loading 1. 여기까지 5~10분은 걸림. 4. 클라이언트 1개~3개 실행 1. 역시나 3분 이상 소모됨 5. 재현 1. 재현하기 힘든 경우라면? 2. 혈맹 전쟁을 테스트하려면? 혈원 15명 이상이 접속 해야 테스트 가능 6. 코드 수정 7. 3번으로 돌아가서 확인
  21. 21. Test Driven Debugging!! TDD 를 이용할 때 디버그 관리자에 새로운 버그 추가 1. 게임 스크립트 데이타 받아서 컴파일 2. 서버들 빌드 후 loading 3. 여기까지 5~10분은 걸림. 1. 스크립트 없이 테스트 할 수 있는 경우가 많음. 2. 클라이언트 1개~3개 실행 4. 역시나 3분 이상 소모됨 1. 클라이언트 없이 실행 가능. 2. 재현 5. 재현하기 힘든 경우라면? 1. 혈맹 전쟁을 테스트하려면? 혈원 15명 이상이 접속해야 테스트 가능 2. 직접 확률을 지정하거나, 코드에서 loop 돌릴 수 있다. 3. 코드 수정 6. 3번으로 돌아가서 확인 7. 한 번 만들어진 테스트는 계속 남는다. 8.
  22. 22. Regression Test 변경되지 않은 기능은 ‘예전과 동일하게 동 작함’을 보장하는 테스트 Characterization Test 현재 상태를 그대로 테스트로 추가 CPlayer* pMe = ...; CHECK_EQUAL(0, pMe->GetLife()); // Test Failed CHECK_EQUAL(644, pMe->GetLife()); // Test 성공 리펙토링을 하기 전 필수적인 작업 일종의 TLP(Test Last Programming)
  23. 23. Regression Test 2년 전의 전투 관련 서버 코드가 어떻게 돌아 가는지 보고 싶다면 2년 전 Server 소스 snapshot 받아서 빌드 같은 날의 Client 소스 snapshot 받아서 빌드 같은 날의 게임 스크립트 데이타 로딩 DB 스키마 셋팅 등등등...
  24. 24. Regression Test in TDD 2년 전에 전투 관련 서버 코드가 어떻게 돌아 가는지 보고 싶다면 2년 전 Server 소스 snapshot 받아서 빌드 같은 날의 Client 소스 snapshot 받아서 빌드 같은 날의 게임 스크립트 데이타 로딩 DB 스키마 셋팅 등등등... 심지어 예전 코드가 어떻게 실행되는지를 직 접 Break Point 잡고 Trace 할 수 있다.
  25. 25. Branch & Merge Branch 후 Merge 작업 Merge 하면서 다른 팀원이 바꾸어 놓은 코드 때문에 버그 발생 1차적으로는 지속적인 통합을 권장 2차적으로는 UnitTest 를 통해서 다른 팀원들에게 지켜야 할 가이드라인을 제시
  26. 26. Working Effectively with Legacy Code Seams Sprout Method / Class Breaking Dependencies Interception Points Pinch Point Traps Targeted Testing Sensing Variable Construction Test Hack Points
  27. 27. 테스트 방법 리턴값 CHECK_CLOSE(10.5248, CAttacker::GetCritical(p1, p2, ...), 0.001); 객체 상태 pPlayer->GetSkill(1, 1); CHECK_EQUAL(1, pPlayer->GetSkillsNum()); 객체 상호작용 Mock 객체 사용.
  28. 28. TDD Tips 1 가장 쉽게 만들 수 있는 것부터 테스트에 추가한다. Multithread 테스트는 포기한다. #if defined(UnitTestDefined) && defined(_DEBUG) 팀원들을 안심시켜라. Release 빌드에서는 file 에서 오른쪽 버튼 -> general 탭 에서 exclude file from build 테스트를 빠르게 유지 Disk I/O 를 최소화한다. 스크립트, Database dependency 를 최소화 할 수 있다.
  29. 29. TDD Tips 2 기존 코드에 테스트 추가하기 test 없는 private 보다 test 있는 public 이 안전 멤버변수도 parameter 로 넘기면 test 만들기 쉬 워진다. 마찬가지로 전역변수도 parameter 로 넘겨주자. 이제 아예 static 멤버함수로 만들자. 좀 더 쉽게 테스트를 만들 수 있다.
  30. 30. TDD Tips 3 breakpoint -> trace 는 대신 필요한 곳에 CHECK 테스트를 추가한다. 임의성 테스트 Windows 프로그램에서 콘솔 띄우기 TDD 돌릴 것인지 여부를 설정파일로 결정 주의! 직접 테스트도 병행해야 한다.
  31. 31. 임의성 테스트 타격 크리티컬 같이 random 값이 들어가는 계산은 어떻게 테스트 할 수 있을까? int GetRand() const { #if defined(_DEBUG) && defined(UnitTestDefined) if (bSettedRandomValue) { return MyTestUnit ::Inst().m_Random; } #endif return ::rand(); } TEST_FIXTURE(FixtureUser2, CheckMagicCritical){ int playerLevel = 60; const double bonus = 50.0; MyTestUnit ::Inst().m_Random = 100.0; // 무조건 성공시키겠다. CHECK_EQUAL(true, IsAttackCritical(player, playerLevel, bonus)); MyTestUnit ::Inst().m_Random = 0.0; // 무조건 실패시키겠다. CHECK_EQUAL(false, IsAttackCritical(...));
  32. 32. Windows 프로그램에서 콘솔 띄우기 // http://dslweb.nwnexus.com/~ast/dload/guicon.htm static const WORD MAX_CONSOLE_LINES = 500; void RedirectIOToConsole() { CONSOLE_SCREEN_BUFFER_INFO coninfo; AllocConsole(); GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); coninfo.dwSize.Y = MAX_CONSOLE_LINES; SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); lStdHandle = (long)GetStdHandle(STD_OUTPUT_HANDLE); hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); // redirect unbuffered STDIN to the console lStdHandle = (long)GetStdHandle(STD_INPUT_HANDLE); hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); lStdHandle = (long)GetStdHandle(STD_ERROR_HANDLE); hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); fp = _fdopen( hConHandle, quot;wquot; ); *stderr = *fp; setvbuf( stderr, NULL, _IONBF, 0 ); ios::sync_with_stdio(); } FreeConsole() 이용
  33. 33. Mock 객체 소켓 통신을 어떻게 테스트할 것인가? 파일 시스템이 꽉 차 있는 경우는 어떻게 테 스트 할 것인가? 진짜 하드를 꽉 채운 후 테스트? DB 관련 원하는 환경을 가짜로 돌아가는 것처럼 만들 어 주는 객체를 이용하자.
  34. 34. Mock 객체 class SecretObject { protected: int m_Age; virtual int GetMyAge() const { return m_Age; } } class MockSecretObject : public SecretObject { public: using SecretObject::m_Age; virtual int GetMyAge() const { return SecretObject::GetMyAge(); } } MockSecretObject a; a.GrownUp(); CHECK_EQUAL(1, a.GetMyAge()); CHECK_EQUAL(1, a.m_Age);
  35. 35. Mock 객체 class CMockPlayer : public CPlayer { virtual CSocket* GetSocket() { return m_pSocket; } CMockSocket* m_pSocket; void Attack(double damage) { GetSocket()->SendMsg(“You got damage %d”, damage); } class CMockSocket : public CSocket { virtual void Send(...) {} virtual bool SendMsg(…) { return true;} }
  36. 36. Mock 시연 – FPS? ☺
  37. 37. Mock 시연 – FPS? ☺
  38. 38. 테스트 - 일반원칙 망가질 가능성이 있는 모든 것을 테스트한다. 망가지는 모든 것을 테스트한다. 새 코드는 무죄가 증명되기 전까지는 유죄. 적어도 제품 코드만큼 테스트 코드를 작성한 다. 컴파일을 할 때마다 지역 테스트를 실행한다. 저장소에 체크인하기 전에 모든 테스트를 실 행해 본다.
  39. 39. 자문해 봐야 할 사항 이 코드가 옳게 동작한다면, 어떻게 그것을 알 수 있는가? 이것을 어떻게 테스트할 것인가? '그밖에' 어떤 것이 잘못될 수 있는가? 이와 똑같은 종류의 문제가 다른 곳에서도 일 어날 수 있을까?
  40. 40. 무엇을 테스트해야 하는가 RIGHT-BICEP Right : 결과가 옳은가? Boundary : 모든 경계 조건이 CORRECT한가? Inverse : 역관계를 확인할 수 있는가? Cross-check : 다른 수단을 사용해서 결과를 교차 확인 할 수 있는가? Error condition : 에러 조건을 강제로 만들어낼 수 있는가? Performance : 성능 특성이 한도내에 있는가?
  41. 41. 좋은 테스트는 A-TRIP해야 한다. Automatic(자동적) Through(철저함) Repeatable(반복 가능) Independent(독립적) Professional(전문적)
  42. 42. CORRECT 경계 조건 Conformance(형식 일치) : 값의 형식이 예상한 형식과 일치하는가? Ordering(순서) : 적절히 순서대로 되어 있거나 그렇지 않은 값인가? Range(범위) : 적당한 최소값과 최대값 사이에 있는 값인가? Reference(참조) : 코드가 자기가 직접 제어하지 않는 외부 코드를 참조하는가? Existence(존재성) : 값이 존재하는가? Cardinality(개체 수) : 확실히 충분한 값이 존재하는가? Time(시간) : 모든 것이 순서대로 일어나는가? 제시간에? 때맞추어? 출처 : 실용주의 프로그래머를 위한 단위 테스트 with JUnit
  43. 43. 테스트 기피를 위한 변명 시간이 오래 걸린다. 개발 초기에는 기획 변경이 잦아서 테스트를 만들어 봐야 소용없다.
  44. 44. 시간이 오래 걸린다 -> 맞습니다 2개월에서 1년까지는 시간이 더 걸립니다. 모 게임사의 XP 실패담. 테스트 코드가 2 만 라인이 안 되는 Product Code 보다 8배 정도 많음. 사람들이 #ifdef 로 테스트 코드를 무시하기 시작함. 다른 사람이 망가뜨린 테스트를 대신 고치는 일이 계속되면서 짜증 증가 CODE
  45. 45. 그러나! 서비스를 오래 하려면? 기획 경화 현상 이거 고쳤다가 잘못 되면 어쩔려고 그래요? 예전 구현을 손 대지 않으려고 땜빵식 구현/기획을 추가하면서 점점 더 고치기 힘들어짐. 버그/핵 에 대처 능력이 떨어지게 된다.
  46. 46. 기획이 자주 변경된다. Fragile Test 지금 아니면 할 수 없습니다. 초반부터 테스트 코드를 추가하면, 더욱 더 단단한 코드를 얻을 수 있고, 신뢰할 수 있는 테스트 집합을 구축할 수 있다.
  47. 47. TDD 적용하기 스스로 먼저 확신을 가질 수 있도록 먼저 해 보기
  48. 48. UnitTest 의 어려운 점 팀원들에게 같이 하자고 꼬시는 게 가장 어려움 왜 일을 더 해야 하는지(테스트 코딩)를 설득하기가 어려움 일부만 UnitTest 를 한다면 다른 팀원이 수정한 내용이 Test 를 실패시키는 바람에 갈등 유발 Mock 객체를 부주의하게 사용해서 UnitTest define 을 끈 채로 빌드하면 에러 발생! 비정상적인 로직이 동작하게 할 수 있음 기존 가정을 깨는 Seam Code 를 추가하는 도중에 없던 문제를 발생시킬 수 있음 테스트 코드는 제품코드가 아니라는 생각 때문에 막 코딩해 버림 테스트 코드 자체가 주체할 수 없게 됨 그럼에도 불구하고 지켜봐 주고 도와준 팀원들에게 감사!!!
  49. 49. 결론 테스트 프레임워크 구축은 쉽지 않다. 그러나 노력한 만큼 복리로 돌려받을 수 있다. 테스터의 입장에서 코드를 바라보게 된다. (코드 품질이 향상되고, 좋은 버릇이 생긴다.) 모든 방법을 동원해서 테스트하라. 상상력이 필요합니다. TDD 는 도구이지 목표가 아니다. 1900 년 초부터 UnitTest 는 시작되었습니다.
  50. 50. 참고자료 http://unittest-cpp.sourceforge.net/ UnitTest++ 소스 받는 곳 http://www.gamesfromwithin.com Noel Llopis - llopis@convexhull.com GDC2006 발표자료 http://andstudy.com/andwiki/wiki.php/BackwardsIsFo rward 위 자료를 번역해 놓은 PPT 및 노트
  51. 51. 책 테스트 주도 개발 단위 테스트 with JUnit
  52. 52. 책 Working Effectively with Legacy Code xUnit Test Patterns
  53. 53. Q&A
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×