SlideShare a Scribd company logo
1 of 53
Download to read offline
TDD, UnitTest for games

    박일(NcSoft. Lineage II)
    http://ParkPD.egloos.com
버그?
TDD 란?
Test Driven Development
 테스트가 개발을 운전(Driven)한다.
Programmer Test
 프로그래머가 직접 설치하는 자동화된 테스트
White Box Test
 QA 팀의 테스트는 Black Box Test
테스트 실패
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){
         }
            }
체크 인
                           리팩토링
테스트 통과
                                                         테스트 통과
UnitTest++
개발자 Noel Llopis
 Senior Architect
 High Moon Studios
피보나치 수열 시연
 피보나치 수열
 0, 1, 1, 2, 3, 5, 8, 13, 21......의 형태의 수
열. 즉, 첫 번째 항의 값은 0 이고 두 번째 항
의 값은 1일 때 이후의 항들은 이전의 두 항
을 더한 값으로 만들어지는 수열을 말한다.
수열의 공식은 다음과 같다. fn = fn-1 + fn-
2 (단, f0 = 0, f1 = 1, n = 2, 3, 4, ....)
피보나치 수열 1
fn = fn-1 + fn-2
(단, f0 = 0, f1 = 1, n = 2, 3, 4, ....)
재귀호출을 이용
피보나치 수열 2
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++ 기능 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 실제 테스트
UnitTest++ 기능 2/3
TimeConstraint
  실행 시간이 일정 이상 지나면
  테스트 fail 로 간주.
TestResult r;
TimeConstraint t(10, result, TestDetails(“”, “”, “”, 0);
TimeHelpers::SleepMs(20);
CHECK_EQUAL(1, result.GetFailureCount());

Crash 검사
UnitTest++ 기능 3/3
Suite
Two Stage Test
  1단계
    리소스 로딩 이전에
    로직 테스트, 순수한 의미의 UnitTest
  2단계
    리소스 로딩 후에
    월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트
    지형의 이동 가능 여부 등
성능 테스트
 같은 함수를 100만번 부를 때 0.01초 내에 리턴되는지 검사
 매번 검사하기 부담스러우므로 command 명령으로 가끔씩 수동
 으로 테스트하기.
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());
}
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);
}
예시: 캐릭터의 행동
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());
}
Working Effectively
        with Legacy Code
필요한 이유
Debugging
Regression Test
리니지2
리니지2 업데이트 일지
 CHRONICLE 01 - 전란을 부르는 자들
 CHRONICLE 02 - 풍요의 시대
 CHRONICLE 03 - 눈뜨는 어둠
 CHRONICLE 04 - 운명의 계승자들
 CHRONICLE 05 - Death of Blood
 혼돈의 왕좌 Interlude - 그 시작을 말하다
 혼돈의 왕좌 - The kamael (2007)
계속되는 업데이트 & 변경되는 기획
왜 개발자가 Test 까지?
QA 팀이 있으신가요?
 없는 회사가 대부분
QA 팀이 있어도
 최고의 QA 팀이 있어도 버그는 막을 수 없다.
  Lineage2 팀의 QA 팀은 최고입니다.
 마감직전에 발견되는 버그가 가장 큰 문제를 일으킨다.
결국 욕은 프로그래머가 먹고,
 야근도 해야 한다. 미리 Test를 이용, 버그를 막아보자.
버그가 생기면
 수익 감소
 악플뿐 아니라 웹진기사가 뜨는 경우까지!
QA 팀은 역시 필요합니다.

                          수동
자동
                사용성 테스팅
     스토리 테스트
     비즈니스 의도    탐색적 테스팅
      (제품 설계)



     단위 테스트     특성 테스팅
      개발자 의도     보안 테스팅
                 부하 테스팅
      (코드 설계)    조합 테스팅
                   …
자동                        도구
Test Driven Debugging?
일반적인 디버깅 방법은?
1. 버그 리포트 시스템에 새로운 버그 추가
2. 게임 스크립트 데이타 받아서 컴파일
3. 서버들 빌드 후 loading
   1. 여기까지 5~10분은 걸림.
4. 클라이언트 1개~3개 실행
   1. 역시나 3분 이상 소모됨
5. 재현
   1. 재현하기 힘든 경우라면?
   2. 혈맹 전쟁을 테스트하려면? 혈원 15명 이상이 접속
      해야 테스트 가능
6. 코드 수정
7. 3번으로 돌아가서 확인
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.
Regression Test
변경되지 않은 기능은 ‘예전과 동일하게 동
작함’을 보장하는 테스트
  Characterization Test
  현재 상태를 그대로 테스트로 추가
CPlayer* pMe = ...;
CHECK_EQUAL(0, pMe->GetLife());     // Test Failed
CHECK_EQUAL(644, pMe->GetLife());   // Test 성공

리펙토링을 하기 전 필수적인 작업
일종의 TLP(Test Last Programming)
Regression Test
2년 전의 전투 관련 서버 코드가 어떻게 돌아
가는지 보고 싶다면
 2년 전 Server 소스 snapshot 받아서 빌드
 같은 날의 Client 소스 snapshot 받아서 빌드
 같은 날의 게임 스크립트 데이타 로딩
 DB 스키마 셋팅
 등등등...
Regression Test in TDD
2년 전에 전투 관련 서버 코드가 어떻게 돌아
가는지 보고 싶다면
 2년 전 Server 소스 snapshot 받아서 빌드
 같은 날의 Client 소스 snapshot 받아서 빌드
 같은 날의 게임 스크립트 데이타 로딩
 DB 스키마 셋팅
 등등등...
심지어 예전 코드가 어떻게 실행되는지를 직
접 Break Point 잡고 Trace 할 수 있다.
Branch & Merge
Branch 후 Merge 작업
 Merge 하면서 다른 팀원이 바꾸어 놓은 코드
 때문에 버그 발생
  1차적으로는 지속적인 통합을 권장
  2차적으로는 UnitTest 를 통해서 다른 팀원들에게
  지켜야 할 가이드라인을 제시
Working Effectively
         with Legacy Code
Seams
Sprout Method / Class
Breaking Dependencies
Interception Points
Pinch Point Traps
Targeted Testing
Sensing Variable
Construction Test
Hack Points
테스트 방법
리턴값
CHECK_CLOSE(10.5248, CAttacker::GetCritical(p1,
 p2, ...), 0.001);
객체 상태
pPlayer->GetSkill(1, 1);
CHECK_EQUAL(1, pPlayer->GetSkillsNum());
객체 상호작용
  Mock 객체 사용.
TDD Tips 1
가장 쉽게 만들 수 있는 것부터 테스트에 추가한다.
Multithread 테스트는 포기한다.
#if defined(UnitTestDefined) && defined(_DEBUG)
  팀원들을 안심시켜라.
  Release 빌드에서는
  file 에서 오른쪽 버튼 -> general 탭 에서 exclude file
  from build
테스트를 빠르게 유지
  Disk I/O 를 최소화한다.
    스크립트, Database dependency 를 최소화 할 수 있다.
TDD Tips 2
기존 코드에 테스트 추가하기
 test 없는 private 보다 test 있는 public 이 안전
 멤버변수도 parameter 로 넘기면 test 만들기 쉬
 워진다.
  마찬가지로 전역변수도 parameter 로 넘겨주자.
 이제 아예 static 멤버함수로 만들자.
  좀 더 쉽게 테스트를 만들 수 있다.
TDD Tips 3
breakpoint -> trace 는 대신
 필요한 곳에 CHECK 테스트를 추가한다.
임의성 테스트
Windows 프로그램에서 콘솔 띄우기
TDD 돌릴 것인지 여부를 설정파일로 결정
주의!
 직접 테스트도 병행해야 한다.
임의성 테스트
타격 크리티컬 같이 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(...));
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() 이용
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;}
}
Mock 시연 – FPS? ☺
Mock 시연 – FPS? ☺
테스트 - 일반원칙
망가질 가능성이 있는 모든 것을 테스트한다.
망가지는 모든 것을 테스트한다.
새 코드는 무죄가 증명되기 전까지는 유죄.
적어도 제품 코드만큼 테스트 코드를 작성한
다.
컴파일을 할 때마다 지역 테스트를 실행한다.
저장소에 체크인하기 전에 모든 테스트를 실
행해 본다.
자문해 봐야 할 사항
 이 코드가 옳게 동작한다면, 어떻게 그것을
알 수 있는가?
이것을 어떻게 테스트할 것인가?
'그밖에' 어떤 것이 잘못될 수 있는가?
이와 똑같은 종류의 문제가 다른 곳에서도 일
어날 수 있을까?
무엇을 테스트해야 하는가
       RIGHT-BICEP
Right : 결과가 옳은가?
Boundary : 모든 경계 조건이 CORRECT한가?
Inverse : 역관계를 확인할 수 있는가?
Cross-check : 다른 수단을 사용해서 결과를 교차
확인 할 수 있는가?
Error condition : 에러 조건을 강제로 만들어낼 수
있는가?
Performance : 성능 특성이 한도내에 있는가?
좋은 테스트는 A-TRIP해야 한다.
Automatic(자동적)
Through(철저함)
Repeatable(반복 가능)
Independent(독립적)
Professional(전문적)
CORRECT 경계 조건
Conformance(형식 일치) : 값의 형식이 예상한 형식과 일치하는가?
Ordering(순서) : 적절히 순서대로 되어 있거나 그렇지 않은 값인가?
Range(범위) : 적당한 최소값과 최대값 사이에 있는 값인가?
Reference(참조) : 코드가 자기가 직접 제어하지 않는 외부 코드를
참조하는가?
Existence(존재성) : 값이 존재하는가?
Cardinality(개체 수) : 확실히 충분한 값이 존재하는가?
Time(시간) : 모든 것이 순서대로 일어나는가? 제시간에? 때맞추어?

출처 : 실용주의 프로그래머를 위한 단위 테스트 with JUnit
테스트 기피를 위한 변명
시간이 오래 걸린다.
개발 초기에는 기획 변경이 잦아서 테스트를
만들어 봐야 소용없다.
시간이 오래 걸린다 -> 맞습니다
2개월에서 1년까지는 시간이 더 걸립니다.
모 게임사의 XP 실패담.
  테스트 코드가 2 만 라인이 안 되는 Product Code 보다 8배 정도 많음.
  사람들이 #ifdef 로 테스트 코드를 무시하기 시작함.
  다른 사람이 망가뜨린 테스트를 대신 고치는 일이 계속되면서 짜증 증가


       CODE
그러나!
서비스를 오래 하려면?
기획 경화 현상
 이거 고쳤다가
 잘못 되면 어쩔려고 그래요?
예전 구현을 손 대지
않으려고
땜빵식 구현/기획을
추가하면서
점점 더 고치기 힘들어짐.
버그/핵 에 대처 능력이
떨어지게 된다.
기획이 자주 변경된다.
Fragile Test
지금 아니면 할 수 없습니다.
 초반부터
 테스트 코드를
 추가하면,
 더욱 더 단단한
 코드를
 얻을 수 있고,
 신뢰할 수 있는
 테스트 집합을
 구축할 수 있다.
TDD 적용하기
스스로 먼저 확신을 가질 수 있도록 먼저 해
보기
UnitTest 의 어려운 점
팀원들에게 같이 하자고 꼬시는 게 가장 어려움
  왜 일을 더 해야 하는지(테스트 코딩)를 설득하기가 어려움
일부만 UnitTest 를 한다면
  다른 팀원이 수정한 내용이 Test 를 실패시키는 바람에 갈등 유발
Mock 객체를 부주의하게 사용해서
  UnitTest define 을 끈 채로 빌드하면 에러 발생!
  비정상적인 로직이 동작하게 할 수 있음
기존 가정을 깨는 Seam Code 를 추가하는 도중에
  없던 문제를 발생시킬 수 있음
테스트 코드는 제품코드가 아니라는 생각 때문에 막 코딩해 버림
  테스트 코드 자체가 주체할 수 없게 됨
그럼에도 불구하고 지켜봐 주고 도와준 팀원들에게 감사!!!
결론
테스트 프레임워크 구축은 쉽지 않다.
그러나 노력한 만큼 복리로 돌려받을 수 있다.
테스터의 입장에서 코드를 바라보게 된다.
(코드 품질이 향상되고, 좋은 버릇이 생긴다.)
모든 방법을 동원해서 테스트하라.
 상상력이 필요합니다.
 TDD 는 도구이지 목표가 아니다.
 1900 년 초부터 UnitTest 는 시작되었습니다.
참고자료
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 및 노트
책
테스트 주도 개발
단위 테스트 with JUnit
책
Working Effectively with Legacy Code
xUnit Test Patterns
Q&A

More Related Content

Viewers also liked

Io t에서의 소프트웨어단위테스트_접근사례
Io t에서의 소프트웨어단위테스트_접근사례Io t에서의 소프트웨어단위테스트_접근사례
Io t에서의 소프트웨어단위테스트_접근사례SangIn Choung
 
Istqb 2-소프트웨어수명주기와테스팅-2015
Istqb 2-소프트웨어수명주기와테스팅-2015Istqb 2-소프트웨어수명주기와테스팅-2015
Istqb 2-소프트웨어수명주기와테스팅-2015Jongwon Lee
 
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)Jeongho Shin
 
KGC2010 - 낡은 코드에 단위테스트 넣기
KGC2010 - 낡은 코드에 단위테스트 넣기KGC2010 - 낡은 코드에 단위테스트 넣기
KGC2010 - 낡은 코드에 단위테스트 넣기Ryan Park
 
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Testbeom kyun choi
 
시작하자 단위테스트
시작하자 단위테스트시작하자 단위테스트
시작하자 단위테스트YongEun Choi
 

Viewers also liked (6)

Io t에서의 소프트웨어단위테스트_접근사례
Io t에서의 소프트웨어단위테스트_접근사례Io t에서의 소프트웨어단위테스트_접근사례
Io t에서의 소프트웨어단위테스트_접근사례
 
Istqb 2-소프트웨어수명주기와테스팅-2015
Istqb 2-소프트웨어수명주기와테스팅-2015Istqb 2-소프트웨어수명주기와테스팅-2015
Istqb 2-소프트웨어수명주기와테스팅-2015
 
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)
더 나은 S/W를 만드는 것에 관하여 (OKKY 세미나)
 
KGC2010 - 낡은 코드에 단위테스트 넣기
KGC2010 - 낡은 코드에 단위테스트 넣기KGC2010 - 낡은 코드에 단위테스트 넣기
KGC2010 - 낡은 코드에 단위테스트 넣기
 
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 TestOkjsp 13주년 발표자료: 생존 프로그래밍 Test
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
 
시작하자 단위테스트
시작하자 단위테스트시작하자 단위테스트
시작하자 단위테스트
 

More from Ryan Park

위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점Ryan Park
 
Domain Driven Design Ch7
Domain Driven Design Ch7Domain Driven Design Ch7
Domain Driven Design Ch7Ryan Park
 
즉흥연기와프로그래밍
즉흥연기와프로그래밍즉흥연기와프로그래밍
즉흥연기와프로그래밍Ryan Park
 
Oop design principle SOLID
Oop design principle SOLIDOop design principle SOLID
Oop design principle SOLIDRyan Park
 
OOP 설계 원칙 S.O.L.I.D.
OOP 설계 원칙 S.O.L.I.D.OOP 설계 원칙 S.O.L.I.D.
OOP 설계 원칙 S.O.L.I.D.Ryan Park
 
Unicode 이해하기
Unicode 이해하기Unicode 이해하기
Unicode 이해하기Ryan Park
 
Oop design principle
Oop design principleOop design principle
Oop design principleRyan Park
 
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10Ryan Park
 
나도기술서번역한번해볼까 in NDC10
나도기술서번역한번해볼까 in NDC10나도기술서번역한번해볼까 in NDC10
나도기술서번역한번해볼까 in NDC10Ryan Park
 
나도(기술서)번역한번해볼까
나도(기술서)번역한번해볼까나도(기술서)번역한번해볼까
나도(기술서)번역한번해볼까Ryan Park
 
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010Ryan Park
 
Programming Game AI by Example. Ch7. Raven
Programming Game AI by Example. Ch7. RavenProgramming Game AI by Example. Ch7. Raven
Programming Game AI by Example. Ch7. RavenRyan Park
 
AIbyExample - Ch7 raven. version 0.8
AIbyExample - Ch7 raven. version 0.8AIbyExample - Ch7 raven. version 0.8
AIbyExample - Ch7 raven. version 0.8Ryan Park
 
카사 공개세미나1회 W.E.L.C.
카사 공개세미나1회  W.E.L.C.카사 공개세미나1회  W.E.L.C.
카사 공개세미나1회 W.E.L.C.Ryan Park
 
온라인 게임에서 사례로 살펴보는 디버깅
온라인 게임에서 사례로 살펴보는 디버깅온라인 게임에서 사례로 살펴보는 디버깅
온라인 게임에서 사례로 살펴보는 디버깅Ryan Park
 

More from Ryan Park (20)

위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점위대한 게임개발팀의 공통점
위대한 게임개발팀의 공통점
 
Domain Driven Design Ch7
Domain Driven Design Ch7Domain Driven Design Ch7
Domain Driven Design Ch7
 
Taocp1 2 4
Taocp1 2 4Taocp1 2 4
Taocp1 2 4
 
즉흥연기와프로그래밍
즉흥연기와프로그래밍즉흥연기와프로그래밍
즉흥연기와프로그래밍
 
Oop design principle SOLID
Oop design principle SOLIDOop design principle SOLID
Oop design principle SOLID
 
OOP 설계 원칙 S.O.L.I.D.
OOP 설계 원칙 S.O.L.I.D.OOP 설계 원칙 S.O.L.I.D.
OOP 설계 원칙 S.O.L.I.D.
 
Unicode 이해하기
Unicode 이해하기Unicode 이해하기
Unicode 이해하기
 
Unicode100
Unicode100Unicode100
Unicode100
 
Unicode
UnicodeUnicode
Unicode
 
Unicode
UnicodeUnicode
Unicode
 
Unicode
UnicodeUnicode
Unicode
 
Oop design principle
Oop design principleOop design principle
Oop design principle
 
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
 
나도기술서번역한번해볼까 in NDC10
나도기술서번역한번해볼까 in NDC10나도기술서번역한번해볼까 in NDC10
나도기술서번역한번해볼까 in NDC10
 
나도(기술서)번역한번해볼까
나도(기술서)번역한번해볼까나도(기술서)번역한번해볼까
나도(기술서)번역한번해볼까
 
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
 
Programming Game AI by Example. Ch7. Raven
Programming Game AI by Example. Ch7. RavenProgramming Game AI by Example. Ch7. Raven
Programming Game AI by Example. Ch7. Raven
 
AIbyExample - Ch7 raven. version 0.8
AIbyExample - Ch7 raven. version 0.8AIbyExample - Ch7 raven. version 0.8
AIbyExample - Ch7 raven. version 0.8
 
카사 공개세미나1회 W.E.L.C.
카사 공개세미나1회  W.E.L.C.카사 공개세미나1회  W.E.L.C.
카사 공개세미나1회 W.E.L.C.
 
온라인 게임에서 사례로 살펴보는 디버깅
온라인 게임에서 사례로 살펴보는 디버깅온라인 게임에서 사례로 살펴보는 디버깅
온라인 게임에서 사례로 살펴보는 디버깅
 

UnitTest, Tdd For Games Kgc2007 ParkPD

  • 1. TDD, UnitTest for games 박일(NcSoft. Lineage II) http://ParkPD.egloos.com
  • 3. TDD 란? Test Driven Development 테스트가 개발을 운전(Driven)한다. Programmer Test 프로그래머가 직접 설치하는 자동화된 테스트 White Box Test QA 팀의 테스트는 Black Box Test
  • 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. UnitTest++ 개발자 Noel Llopis Senior Architect High Moon Studios
  • 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. 피보나치 수열 1 fn = fn-1 + fn-2 (단, f0 = 0, f1 = 1, n = 2, 3, 4, ....) 재귀호출을 이용
  • 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. 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. UnitTest++ 기능 2/3 TimeConstraint 실행 시간이 일정 이상 지나면 테스트 fail 로 간주. TestResult r; TimeConstraint t(10, result, TestDetails(“”, “”, “”, 0); TimeHelpers::SleepMs(20); CHECK_EQUAL(1, result.GetFailureCount()); Crash 검사
  • 12. UnitTest++ 기능 3/3 Suite Two Stage Test 1단계 리소스 로딩 이전에 로직 테스트, 순수한 의미의 UnitTest 2단계 리소스 로딩 후에 월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트 지형의 이동 가능 여부 등 성능 테스트 같은 함수를 100만번 부를 때 0.01초 내에 리턴되는지 검사 매번 검사하기 부담스러우므로 command 명령으로 가끔씩 수동 으로 테스트하기.
  • 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. 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. 예시: 캐릭터의 행동 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. Working Effectively with Legacy Code 필요한 이유 Debugging Regression Test
  • 17. 리니지2 리니지2 업데이트 일지 CHRONICLE 01 - 전란을 부르는 자들 CHRONICLE 02 - 풍요의 시대 CHRONICLE 03 - 눈뜨는 어둠 CHRONICLE 04 - 운명의 계승자들 CHRONICLE 05 - Death of Blood 혼돈의 왕좌 Interlude - 그 시작을 말하다 혼돈의 왕좌 - The kamael (2007) 계속되는 업데이트 & 변경되는 기획
  • 18. 왜 개발자가 Test 까지? QA 팀이 있으신가요? 없는 회사가 대부분 QA 팀이 있어도 최고의 QA 팀이 있어도 버그는 막을 수 없다. Lineage2 팀의 QA 팀은 최고입니다. 마감직전에 발견되는 버그가 가장 큰 문제를 일으킨다. 결국 욕은 프로그래머가 먹고, 야근도 해야 한다. 미리 Test를 이용, 버그를 막아보자. 버그가 생기면 수익 감소 악플뿐 아니라 웹진기사가 뜨는 경우까지!
  • 19. QA 팀은 역시 필요합니다. 수동 자동 사용성 테스팅 스토리 테스트 비즈니스 의도 탐색적 테스팅 (제품 설계) 단위 테스트 특성 테스팅 개발자 의도 보안 테스팅 부하 테스팅 (코드 설계) 조합 테스팅 … 자동 도구
  • 20. Test Driven Debugging? 일반적인 디버깅 방법은? 1. 버그 리포트 시스템에 새로운 버그 추가 2. 게임 스크립트 데이타 받아서 컴파일 3. 서버들 빌드 후 loading 1. 여기까지 5~10분은 걸림. 4. 클라이언트 1개~3개 실행 1. 역시나 3분 이상 소모됨 5. 재현 1. 재현하기 힘든 경우라면? 2. 혈맹 전쟁을 테스트하려면? 혈원 15명 이상이 접속 해야 테스트 가능 6. 코드 수정 7. 3번으로 돌아가서 확인
  • 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. Regression Test 변경되지 않은 기능은 ‘예전과 동일하게 동 작함’을 보장하는 테스트 Characterization Test 현재 상태를 그대로 테스트로 추가 CPlayer* pMe = ...; CHECK_EQUAL(0, pMe->GetLife()); // Test Failed CHECK_EQUAL(644, pMe->GetLife()); // Test 성공 리펙토링을 하기 전 필수적인 작업 일종의 TLP(Test Last Programming)
  • 23. Regression Test 2년 전의 전투 관련 서버 코드가 어떻게 돌아 가는지 보고 싶다면 2년 전 Server 소스 snapshot 받아서 빌드 같은 날의 Client 소스 snapshot 받아서 빌드 같은 날의 게임 스크립트 데이타 로딩 DB 스키마 셋팅 등등등...
  • 24. Regression Test in TDD 2년 전에 전투 관련 서버 코드가 어떻게 돌아 가는지 보고 싶다면 2년 전 Server 소스 snapshot 받아서 빌드 같은 날의 Client 소스 snapshot 받아서 빌드 같은 날의 게임 스크립트 데이타 로딩 DB 스키마 셋팅 등등등... 심지어 예전 코드가 어떻게 실행되는지를 직 접 Break Point 잡고 Trace 할 수 있다.
  • 25. Branch & Merge Branch 후 Merge 작업 Merge 하면서 다른 팀원이 바꾸어 놓은 코드 때문에 버그 발생 1차적으로는 지속적인 통합을 권장 2차적으로는 UnitTest 를 통해서 다른 팀원들에게 지켜야 할 가이드라인을 제시
  • 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. 테스트 방법 리턴값 CHECK_CLOSE(10.5248, CAttacker::GetCritical(p1, p2, ...), 0.001); 객체 상태 pPlayer->GetSkill(1, 1); CHECK_EQUAL(1, pPlayer->GetSkillsNum()); 객체 상호작용 Mock 객체 사용.
  • 28. TDD Tips 1 가장 쉽게 만들 수 있는 것부터 테스트에 추가한다. Multithread 테스트는 포기한다. #if defined(UnitTestDefined) && defined(_DEBUG) 팀원들을 안심시켜라. Release 빌드에서는 file 에서 오른쪽 버튼 -> general 탭 에서 exclude file from build 테스트를 빠르게 유지 Disk I/O 를 최소화한다. 스크립트, Database dependency 를 최소화 할 수 있다.
  • 29. TDD Tips 2 기존 코드에 테스트 추가하기 test 없는 private 보다 test 있는 public 이 안전 멤버변수도 parameter 로 넘기면 test 만들기 쉬 워진다. 마찬가지로 전역변수도 parameter 로 넘겨주자. 이제 아예 static 멤버함수로 만들자. 좀 더 쉽게 테스트를 만들 수 있다.
  • 30. TDD Tips 3 breakpoint -> trace 는 대신 필요한 곳에 CHECK 테스트를 추가한다. 임의성 테스트 Windows 프로그램에서 콘솔 띄우기 TDD 돌릴 것인지 여부를 설정파일로 결정 주의! 직접 테스트도 병행해야 한다.
  • 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. 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. Mock 객체 소켓 통신을 어떻게 테스트할 것인가? 파일 시스템이 꽉 차 있는 경우는 어떻게 테 스트 할 것인가? 진짜 하드를 꽉 채운 후 테스트? DB 관련 원하는 환경을 가짜로 돌아가는 것처럼 만들 어 주는 객체를 이용하자.
  • 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. 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. Mock 시연 – FPS? ☺
  • 37. Mock 시연 – FPS? ☺
  • 38. 테스트 - 일반원칙 망가질 가능성이 있는 모든 것을 테스트한다. 망가지는 모든 것을 테스트한다. 새 코드는 무죄가 증명되기 전까지는 유죄. 적어도 제품 코드만큼 테스트 코드를 작성한 다. 컴파일을 할 때마다 지역 테스트를 실행한다. 저장소에 체크인하기 전에 모든 테스트를 실 행해 본다.
  • 39. 자문해 봐야 할 사항 이 코드가 옳게 동작한다면, 어떻게 그것을 알 수 있는가? 이것을 어떻게 테스트할 것인가? '그밖에' 어떤 것이 잘못될 수 있는가? 이와 똑같은 종류의 문제가 다른 곳에서도 일 어날 수 있을까?
  • 40. 무엇을 테스트해야 하는가 RIGHT-BICEP Right : 결과가 옳은가? Boundary : 모든 경계 조건이 CORRECT한가? Inverse : 역관계를 확인할 수 있는가? Cross-check : 다른 수단을 사용해서 결과를 교차 확인 할 수 있는가? Error condition : 에러 조건을 강제로 만들어낼 수 있는가? Performance : 성능 특성이 한도내에 있는가?
  • 41. 좋은 테스트는 A-TRIP해야 한다. Automatic(자동적) Through(철저함) Repeatable(반복 가능) Independent(독립적) Professional(전문적)
  • 42. CORRECT 경계 조건 Conformance(형식 일치) : 값의 형식이 예상한 형식과 일치하는가? Ordering(순서) : 적절히 순서대로 되어 있거나 그렇지 않은 값인가? Range(범위) : 적당한 최소값과 최대값 사이에 있는 값인가? Reference(참조) : 코드가 자기가 직접 제어하지 않는 외부 코드를 참조하는가? Existence(존재성) : 값이 존재하는가? Cardinality(개체 수) : 확실히 충분한 값이 존재하는가? Time(시간) : 모든 것이 순서대로 일어나는가? 제시간에? 때맞추어? 출처 : 실용주의 프로그래머를 위한 단위 테스트 with JUnit
  • 43. 테스트 기피를 위한 변명 시간이 오래 걸린다. 개발 초기에는 기획 변경이 잦아서 테스트를 만들어 봐야 소용없다.
  • 44. 시간이 오래 걸린다 -> 맞습니다 2개월에서 1년까지는 시간이 더 걸립니다. 모 게임사의 XP 실패담. 테스트 코드가 2 만 라인이 안 되는 Product Code 보다 8배 정도 많음. 사람들이 #ifdef 로 테스트 코드를 무시하기 시작함. 다른 사람이 망가뜨린 테스트를 대신 고치는 일이 계속되면서 짜증 증가 CODE
  • 45. 그러나! 서비스를 오래 하려면? 기획 경화 현상 이거 고쳤다가 잘못 되면 어쩔려고 그래요? 예전 구현을 손 대지 않으려고 땜빵식 구현/기획을 추가하면서 점점 더 고치기 힘들어짐. 버그/핵 에 대처 능력이 떨어지게 된다.
  • 46. 기획이 자주 변경된다. Fragile Test 지금 아니면 할 수 없습니다. 초반부터 테스트 코드를 추가하면, 더욱 더 단단한 코드를 얻을 수 있고, 신뢰할 수 있는 테스트 집합을 구축할 수 있다.
  • 47. TDD 적용하기 스스로 먼저 확신을 가질 수 있도록 먼저 해 보기
  • 48. UnitTest 의 어려운 점 팀원들에게 같이 하자고 꼬시는 게 가장 어려움 왜 일을 더 해야 하는지(테스트 코딩)를 설득하기가 어려움 일부만 UnitTest 를 한다면 다른 팀원이 수정한 내용이 Test 를 실패시키는 바람에 갈등 유발 Mock 객체를 부주의하게 사용해서 UnitTest define 을 끈 채로 빌드하면 에러 발생! 비정상적인 로직이 동작하게 할 수 있음 기존 가정을 깨는 Seam Code 를 추가하는 도중에 없던 문제를 발생시킬 수 있음 테스트 코드는 제품코드가 아니라는 생각 때문에 막 코딩해 버림 테스트 코드 자체가 주체할 수 없게 됨 그럼에도 불구하고 지켜봐 주고 도와준 팀원들에게 감사!!!
  • 49. 결론 테스트 프레임워크 구축은 쉽지 않다. 그러나 노력한 만큼 복리로 돌려받을 수 있다. 테스터의 입장에서 코드를 바라보게 된다. (코드 품질이 향상되고, 좋은 버릇이 생긴다.) 모든 방법을 동원해서 테스트하라. 상상력이 필요합니다. TDD 는 도구이지 목표가 아니다. 1900 년 초부터 UnitTest 는 시작되었습니다.
  • 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. 책 테스트 주도 개발 단위 테스트 with JUnit
  • 52. 책 Working Effectively with Legacy Code xUnit Test Patterns
  • 53. Q&A