1 / 24

TDD for games

TDD for games. 박일. TDD 란 ?. Programmer Test 프로그래머가 직접 설치하는 자동화된 테스트 White Box Test QA 팀의 테스트는 Black Box Test. 테스트 실패. 테스트 통과. 테스트 통과. TDD 의 순환 과정. 불과 몇 분밖에 걸리지 않는다. 테스트 작성. 코드 작성. TEST (ShieldLevelStartsFull){ Shield shield;

anika-combs
Download Presentation

TDD for games

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. TDD for games 박일

  2. TDD 란? • Programmer Test • 프로그래머가 직접 설치하는 자동화된 테스트 • White Box Test • QA 팀의 테스트는 Black Box Test

  3. 테스트 실패 테스트 통과 테스트 통과 TDD의 순환 과정 불과 몇 분밖에 걸리지 않는다. 테스트 작성 코드 작성 TEST (ShieldLevelStartsFull){ Shield shield; CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel()); } 체크 인 Shield::Shield() : m_level (Shield::kMaxLevel){ } 체크 인 리팩토링

  4. 왜 Test? • 리니지2 업데이트 일지 • CHRONICLE 01 - 전란을 부르는 자들 • CHRONICLE 02 - 풍요의 시대 • CHRONICLE 03 - 눈뜨는 어둠 • CHRONICLE 04 - 운명의 계승자들 • CHRONICLE 05 - Death of Blood • 혼돈의 왕좌 Interlude - 그 시작을 말하다 • 혼돈의 왕좌 - The kamael (2007) • 계속되는 업데이트 & 변경되는 기획 • 리펙토링! • 수정되었던 버그가 다시 출현 • Code Freeze • QA 팀의 업무 증가 • 현재 Lineage2 팀의 QA 팀 인원은? • 최고의 QA 팀이 있어도 버그는 막을 수 없다.

  5. UnitTest++ • 개발자 Noel Llopis • Senior Architect • High Moon Studios

  6. 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()

  7. UnitTest++ 기능 • FIXTURE • TEST_FIXTURE • JUnit 의 setUp, tearDown 과 같은 역할 struct CharMaker { Char tester; CharMaker() { tester = new Char(); } ~CharMaker() { delete tester; } TEST_FIXTURE(CharMaker, SomeThing...) { CHECK_EQUAL(true, tester.HasSomeThing()); • TimeConstraint • 실행 시간이 일정 이상 지나면 테스트 fail 로 간주. TestResult r; TimeContraint t(10, result, TestDetails(“”, “”, “”, 0); TimeHelpers::SleepMs(20); CHECK_EQUAL(1, result.GetFailureCount());

  8. 피보나치 예제 • 예제 보기

  9. Unit Test 예제 World world; const initialHealth = 60; Player player(initialHealth); world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10)); HealthPowerup powerup; world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20); world.Update(0.1f); CHECK_EQUAL(initialHealth, player.GetHealth()); TEST (PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup) { World world; const initialHealth = 60; Player player(initialHealth); world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10)); HealthPowerup powerup; world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20); world.Update(0.1f); CHECK_EQUAL(initialHealth, player.GetHealth()); }

  10. 최상의 관행: 간결한 검사 TEST(ActorDoesntMoveIfPelvisBodyIsInSamePositionAsPelvisAnim) { component = ConstructObject<UAmpPhysicallyDrivableSkeletalComponent>(); component->physicalPelvisHandle = NULL; component->SetOwner(owner); component->SkeletalMesh = skelMesh; component->Animations = CreateReadable2BoneAnimSequenceForAmpRagdollGetup(component, skelMesh, 10.0f, 0.0f); component->PhysicsAsset = physicsAsset; component->SpaceBases.AddZeroed(2); component->InitComponentRBPhys(false); 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); INT pelvisIndex = physicsAsset->CreateNewBody(TEXT("Bone1")); URB_BodySetup* pelvisSetup = physicsAsset->BodySetup(pelvisIndex); FPhysAssetCreateParams params = GetGenericCreateParamsForAmpRagdollGetup(); physicsAsset->CreateCollisionFromBone( pelvisSetup, skelMesh, 1, params, boneThings); URB_BodyInstance* pelvisBody = component->PhysicsAssetInstance->Bodies(0); NxActor* pelvisNxActor = pelvisBody->GetNxActor(); SetRigidBodyPositionWSForAmpRagdollGetup(*pelvisNxActor, pelvisBodyPositionWS); component->UpdateSkelPose(0.016f); component->RetransformActorToMatchCurrrentRoot(TransformManipulator()); const float kTolerance(0.002f); FMatrix expectedActorMatrix; expectedActorMatrix.SetIdentity(); expectedActorMatrix.M[3][0] = 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); } TEST (ShieldStartsAtInitialLevel) { ShieldComponent shield(100); CHECK_EQUAL (100, shield.GetLevel()); } TEST (ShieldTakesDamage) { ShieldComponent shield(100); shield.Damage(30); CHECK_EQUAL (70, shield.GetLevel()); } TEST (LevelCannotDropBelowZero) { ShieldComponent shield(100); shield.Damage(200); CHECK_EQUAL (0, shield.GetLevel()); }

  11. 예시: 캐릭터의 행동 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("TransitionState"), output.nextState->GetClassInfo().GetName()); const TransitionState& transition = *output.nextState; CHECK_EQUAL(std::string("RunningState"), transition.endState->GetClassInfo().GetName()); }

  12. Working Effectively with Legacy Code • Debugging • Regression Test

  13. Test Driven Debugging? • 일반적인 디버깅 방법은? • 버그 리포트 시스템에 새로운 버그 추가 • 게임 스크립트 데이타 받아서 컴파일 • 서버들 빌드 후 loading • 최소의 셋팅으로도 5~10분은 걸림. • 클라이언트 1개~3개 실행 • 역시나 3분 이상 소모됨 • 재현 • 재현하기 힘든 경우라면? • 좋은 버프만 동시에 30 개를 받는 경우는? • 코드 수정 • 3번으로 돌아가서 확인

  14. Test Driven Debugging!! • TDD 를 이용할 때 • 디버그 관리자에 새로운 버그 추가 • 게임 스크립트 데이타 받아서 컴파일 • 서버들 빌드 후 loading • 최소의 셋팅으로도 5~10분은 걸림. • 스크립트 없이 테스트 할 수 있는 경우가 많음. • 클라이언트 1개~3개 실행 • 역시나 3분 이상 소모됨 • 클라이언트 없이 실행 가능. • 재현 • 재현하기 힘든 경우라면? • 좋은 버프만 동시에 30 개를 받는 경우는? • 직접 확률을 지정하거나, 코드에서 loop 돌릴 수 있다. • 코드 수정 • 3번으로 돌아가서 확인 • 한 번 만들어진 테스트는 계속 남는다.

  15. Regression Test • 변경되지 않은 기능은 ‘예전과 동일하게 동작함’을 보장하는 테스트 • Characterization Test • 현재 상태를 그대로 테스트로 추가 CUser* pMe = ...; CHECK_EQUAL(0, pMe->GetLife()); // Test Failed CHECK_EQUAL(644, pMe->GetLife()); // Test 성공 • 리펙토링을 하기 전 필수적인 작업 • 버그가 생기면 • 수익 감소 • 웹진에 소식으로 올라올 수도?!

  16. 테스트 방법 • 리턴값 CHECK_CLOSE(10.5248, CAttacker::GetCritical(p1, p2, ...), 0.001); • 객체 상태 pUser->GetSkill(1, 1); CHECK_EQUAL(1, pUser->GetSkillNum()); • 객체 상호작용 • Mock 객체 사용.

  17. TDD Tips • #if defined(USING_TDD) && defined(_DEBUG) • 팀원들을 안심시켜라. • Release 빌드에서는 file 에서 오른쪽 버튼 -> general 탭 에서 exclude file from build • 테스트를 빠르게 유지 • 파일 I/O 를 최소화한다. • TDD 돌릴 것인지 여부를 ini 파일로 결정 • test 없는 private 보다 test 있는 public 이 안전 • 멤버변수도 파라메타로 넘기면 test 만들기 쉬워진다. • 마찬가지로 전역변수도 파라메타로 넘겨주자. • 이제 아예 static 멤버함수로 만들자. • 좀 더 쉽게 테스트를 만들 수 있다. • breakpoint -> trace 는 쓰지 말자. • 대신 모든 검사에 CHECK 를 이용한다. • 임의성 테스트 • Windows 프로그램에서 콘솔 띄우기

  18. 임의성 테스트 타격 크리티컬 같이 random 값이 들어가는 계산은 어떻게 테스트 할 수 있을까? #if defined(_DEBUG) && defined(USING_TDD) if (IsSetRandomNumber()) { return GetTDDRandomNumber(); } #endif double GetTDDRandomNumber() { return MyTestUnit::Inst().m_Random; } TEST_FIXTURE(FixtureUser2, CheckMagicCritical){ int userLevel = 60; const double bonus = 50.0; MyTestUnit ::Inst().m_Random = 100.0; // 무조건 성공시키겠다. CHECK_EQUAL(true, GetMagicCritical(user, userLevel, bonus)); MyTestUnit ::Inst().m_Random = 0.0; // 무조건 실패시키겠다. CHECK_EQUAL(false, GetMagicCritical(...));

  19. 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, "w" ); *stderr = *fp; setvbuf( stderr, NULL, _IONBF, 0 ); ios::sync_with_stdio(); } FreeConsole() 이용

  20. Two Stage Test • 1단계 • 리소스 로딩 이전에 • 로직 테스트, 순수한 의미의 UnitTest • 2단계 • 리소스 로딩 후에 • 월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트 • 지형의 이동 가능 여부 등 • Suite, TestList 와 CHECK_EX 를 이용하면 two stage test 도 가능하다.

  21. Mock 객체 • 소켓 통신을 어떻게 테스트할 것인가? • 파일 시스템이 꽉 차 있는 경우는 어떻게 테스트 할 것인가? • 진짜 하드를 꽉 채운 후 테스트? • DB 관련 • 원하는 환경을 가짜로 돌아가는 것처럼 만들어 주는 객체를 이용하자.

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

  23. 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;} }

  24. 참고자료 • http://unittest-cpp.sourceforge.net/ • UnitTest++ 소스 받는 곳 • http://www.gamesfromwithin.com • Noel Llopis - llopis@convexhull.com • GDC2006 발표자료 • http://andstudy.com/andwiki/wiki.php/BackwardsIsForward • 위 자료를 번역해 놓은 PPT 및 노트

More Related