240 likes | 450 Views
TDD for games. 박일. TDD 란 ?. Programmer Test 프로그래머가 직접 설치하는 자동화된 테스트 White Box Test QA 팀의 테스트는 Black Box Test. 테스트 실패. 테스트 통과. 테스트 통과. TDD 의 순환 과정. 불과 몇 분밖에 걸리지 않는다. 테스트 작성. 코드 작성. TEST (ShieldLevelStartsFull){ Shield shield;
E N D
TDD 란? • Programmer Test • 프로그래머가 직접 설치하는 자동화된 테스트 • White Box Test • QA 팀의 테스트는 Black Box Test
테스트 실패 테스트 통과 테스트 통과 TDD의 순환 과정 불과 몇 분밖에 걸리지 않는다. 테스트 작성 코드 작성 TEST (ShieldLevelStartsFull){ Shield shield; CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel()); } 체크 인 Shield::Shield() : m_level (Shield::kMaxLevel){ } 체크 인 리팩토링
왜 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 팀이 있어도 버그는 막을 수 없다.
UnitTest++ • 개발자 Noel Llopis • Senior Architect • High Moon Studios
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()
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());
피보나치 예제 • 예제 보기
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()); }
최상의 관행: 간결한 검사 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()); }
예시: 캐릭터의 행동 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()); }
Working Effectively with Legacy Code • Debugging • Regression Test
Test Driven Debugging? • 일반적인 디버깅 방법은? • 버그 리포트 시스템에 새로운 버그 추가 • 게임 스크립트 데이타 받아서 컴파일 • 서버들 빌드 후 loading • 최소의 셋팅으로도 5~10분은 걸림. • 클라이언트 1개~3개 실행 • 역시나 3분 이상 소모됨 • 재현 • 재현하기 힘든 경우라면? • 좋은 버프만 동시에 30 개를 받는 경우는? • 코드 수정 • 3번으로 돌아가서 확인
Test Driven Debugging!! • TDD 를 이용할 때 • 디버그 관리자에 새로운 버그 추가 • 게임 스크립트 데이타 받아서 컴파일 • 서버들 빌드 후 loading • 최소의 셋팅으로도 5~10분은 걸림. • 스크립트 없이 테스트 할 수 있는 경우가 많음. • 클라이언트 1개~3개 실행 • 역시나 3분 이상 소모됨 • 클라이언트 없이 실행 가능. • 재현 • 재현하기 힘든 경우라면? • 좋은 버프만 동시에 30 개를 받는 경우는? • 직접 확률을 지정하거나, 코드에서 loop 돌릴 수 있다. • 코드 수정 • 3번으로 돌아가서 확인 • 한 번 만들어진 테스트는 계속 남는다.
Regression Test • 변경되지 않은 기능은 ‘예전과 동일하게 동작함’을 보장하는 테스트 • Characterization Test • 현재 상태를 그대로 테스트로 추가 CUser* pMe = ...; CHECK_EQUAL(0, pMe->GetLife()); // Test Failed CHECK_EQUAL(644, pMe->GetLife()); // Test 성공 • 리펙토링을 하기 전 필수적인 작업 • 버그가 생기면 • 수익 감소 • 웹진에 소식으로 올라올 수도?!
테스트 방법 • 리턴값 CHECK_CLOSE(10.5248, CAttacker::GetCritical(p1, p2, ...), 0.001); • 객체 상태 pUser->GetSkill(1, 1); CHECK_EQUAL(1, pUser->GetSkillNum()); • 객체 상호작용 • Mock 객체 사용.
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 프로그램에서 콘솔 띄우기
임의성 테스트 타격 크리티컬 같이 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(...));
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() 이용
Two Stage Test • 1단계 • 리소스 로딩 이전에 • 로직 테스트, 순수한 의미의 UnitTest • 2단계 • 리소스 로딩 후에 • 월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트 • 지형의 이동 가능 여부 등 • Suite, TestList 와 CHECK_EX 를 이용하면 two stage test 도 가능하다.
Mock 객체 • 소켓 통신을 어떻게 테스트할 것인가? • 파일 시스템이 꽉 차 있는 경우는 어떻게 테스트 할 것인가? • 진짜 하드를 꽉 채운 후 테스트? • DB 관련 • 원하는 환경을 가짜로 돌아가는 것처럼 만들어 주는 객체를 이용하자.
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);
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;} }
참고자료 • http://unittest-cpp.sourceforge.net/ • UnitTest++ 소스 받는 곳 • http://www.gamesfromwithin.com • Noel Llopis - llopis@convexhull.com • GDC2006 발표자료 • http://andstudy.com/andwiki/wiki.php/BackwardsIsForward • 위 자료를 번역해 놓은 PPT 및 노트