unittest, tdd for games kgc2007 parkpd

Post on 19-May-2015

3.578 Views

Category:

Documents

3 Downloads

Preview:

Click to see full reader

DESCRIPTION

KGC2007 presentation file about UnitTest, TDD applying in game programming

TRANSCRIPT

TDD, UnitTest for games

박일(NcSoft. Lineage II)

http://http://ParkPD.egloos.comParkPD.egloos.com

버그?

TDD 란?

Test Driven Development

� 테스트가 개발을 운전(Driven)한다.

Programmer Test

� 프로그래머가 직접 설치하는 자동화된 테스트

White Box Test

� QA 팀의 테스트는 Black Box Test

불과불과 몇몇 분밖에분밖에 걸걸리지리지 않는다않는다..

테스트테스트 실패실패

테스트테스트 통과통과테스트테스트 통과통과

체크 인

체크 인

TDD의 순환과정

TEST (ShieldLevelStartsFull){Shield shield;CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel());

}

TEST (ShieldLevelStartsFull){Shield shield;CHECK_EQUAL (Shield::kMaxLevel, shield.GetLevel());

}

Shield::Shield() : m_level (Shield::kMaxLevel){}

Shield::Shield() : m_level (Shield::kMaxLevel){}

테스트작성 코드 작성

리팩토링

UnitTest++

개발자개발자개발자개발자 Noel Noel Noel Noel LlopisLlopisLlopisLlopis

� Senior ArchitectSenior ArchitectSenior ArchitectSenior Architect

� High Moon StudiosHigh Moon StudiosHigh Moon StudiosHigh 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 테스트

� struct FixtureSQL {FixtureSQL() { sql.connect(); }~FixtureSQL() { sql.close() }SQL sql;

};TEST_FIXTURE (FixtureSQL, DBTest) {

// sql.xxx 실제 테스트

TEST(DBTest) {SQL sql;sql.connect();// 실제 테스트 코드sql.close();

}

TEST(DBTest) {SQL sql;sql.connect();// 실제 테스트 코드sql.close();

}

TEST(DBTest1) {SQL sql;sql.connect();// 실제 테스트 코드sql.close();

}

TEST(DBTest1) {SQL sql;sql.connect();// 실제 테스트 코드sql.close();

}

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

SuiteTwo Stage Test � 1단계

� 리소스 로딩 이전에� 로직 테스트, 순수한 의미의 UnitTest

� 2단계� 리소스 로딩 후에� 월드 지형 버그, 스킬, 퀘스트 등 데이터 로딩이 필요한 테스트� 지형의 이동 가능 여부 등

성능 테스트� 같은 함수를 100만번 부를 때 0.01초 내에 리턴되는지 검사� 매번 검사하기 부담스러우므로 command 명령으로 가끔씩 수동

으로 테스트하기.

Unit Test 예제World World World World worldworldworldworld;;;;const const const const initialHealthinitialHealthinitialHealthinitialHealth = 60;= 60;= 60;= 60;Player Player Player Player player(initialHealthplayer(initialHealthplayer(initialHealthplayer(initialHealth););););world.Add(&playerworld.Add(&playerworld.Add(&playerworld.Add(&player, , , , Transform(AxisYTransform(AxisYTransform(AxisYTransform(AxisY, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));HealthPowerupHealthPowerupHealthPowerupHealthPowerup poweruppoweruppoweruppowerup;;;;world.Add(&powerupworld.Add(&powerupworld.Add(&powerupworld.Add(&powerup, , , , Transform(AxisYTransform(AxisYTransform(AxisYTransform(AxisY, 0, Vector3(, 0, Vector3(, 0, Vector3(, 0, Vector3(----10,0,20);10,0,20);10,0,20);10,0,20);world.Update(0.1f);world.Update(0.1f);world.Update(0.1f);world.Update(0.1f);CHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealth, , , , player.GetHealthplayer.GetHealthplayer.GetHealthplayer.GetHealth());());());());

TEST (TEST (TEST (TEST (PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerupPlayersHealtDoesNotIncreaseWhileFarFromHealthPowerupPlayersHealtDoesNotIncreaseWhileFarFromHealthPowerupPlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup) {) {) {) {World World World World worldworldworldworld;;;;const const const const initialHealthinitialHealthinitialHealthinitialHealth = 60;= 60;= 60;= 60;Player Player Player Player player(initialHealthplayer(initialHealthplayer(initialHealthplayer(initialHealth););););world.Add(&playerworld.Add(&playerworld.Add(&playerworld.Add(&player, , , , Transform(AxisYTransform(AxisYTransform(AxisYTransform(AxisY, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));, 0, Vector3(10,0,10));HealthPowerupHealthPowerupHealthPowerupHealthPowerup poweruppoweruppoweruppowerup;;;;world.Add(&powerupworld.Add(&powerupworld.Add(&powerupworld.Add(&powerup, , , , Transform(AxisYTransform(AxisYTransform(AxisYTransform(AxisY, 0, Vector3(, 0, Vector3(, 0, Vector3(, 0, Vector3(----10,0,20);10,0,20);10,0,20);10,0,20);world.Update(0.1f);world.Update(0.1f);world.Update(0.1f);world.Update(0.1f);CHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealthCHECK_EQUAL(initialHealth, , , , player.GetHealthplayer.GetHealthplayer.GetHealthplayer.GetHealth());());());());

}}}}

최상의 관행: 간결한 검사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(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_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

리니지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. 게임 스크립트 데이타 받아서 컴파일3. 서버들 빌드 후 loading

1. 여기까지 5~10분은 걸림.2. 스크립트 없이 테스트 할 수 있는 경우가 많음.

4. 클라이언트 1개~3개 실행1. 역시나 3분 이상 소모됨

2. 클라이언트 없이 실행 가능.5. 재현

1. 재현하기 힘든 경우라면?2. 혈맹 전쟁을 테스트하려면? 혈원 15명 이상이 접속해야 테스트 가능

3. 직접 확률을 지정하거나, 코드에서 loop 돌릴 수 있다.6. 코드 수정7. 3번으로 돌아가서 확인

8. 한 번 만들어진 테스트는 계속 남는다.

Regression Test

변경되지 않은 기능은 ‘예전과 동일하게 동작함’을 보장하는 테스트� Characterization Test� 현재 상태를 그대로 테스트로 추가CPlayer* pMe = ...;CHECK_EQUAL(0, pMe->GetLife()); // Test FailedCHECK_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.htmstatic 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 consolelStdHandle = (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() 이용

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 로 테스트 코드를 무시하기 시작함.

� 다른 사람이 망가뜨린 테스트를 대신 고치는 일이 계속되면서 짜증 증가

CODECODECODECODE

그러나!서비스를 오래 하려면?

기획 경화 현상� 이거 고쳤다가

잘못 되면 어쩔려고 그래요?

예전 구현을 손 대지않으려고땜빵식 구현/기획을추가하면서점점 더 고치기 힘들어짐.

버그/핵 에 대처 능력이떨어지게 된다.

기획이 자주 변경된다.

Fragile Test

지금 아니면 할 수 없습니다.

� 초반부터테스트 코드를추가하면,더욱 더 단단한코드를얻을 수 있고,신뢰할 수 있는테스트 집합을구축할 수 있다.

TDD 적용하기

스스로 먼저 확신을 가질 수 있도록 먼저 해보기

UnitTest 의 어려운 점

팀원들에게 같이 하자고 꼬시는 게 가장 어려움

� 왜 일을 더 해야 하는지(테스트 코딩)를 설득하기가 어려움

일부만 UnitTest 를 한다면

� 다른 팀원이 수정한 내용이 Test 를 실패시키는 바람에 갈등 유발

Mock 객체를 부주의하게 사용해서

� UnitTest define 을 끈 채로 빌드하면 에러 발생!

� 비정상적인 로직이 동작하게 할 수 있음

기존 가정을 깨는 Seam Code 를 추가하는 도중에� 없던 문제를 발생시킬 수 있음 �

테스트 코드는 제품코드가 아니라는 생각 때문에 막 코딩해 버림

� 테스트 코드 자체가 주체할 수 없게 됨

그럼에도 불구하고 지켜봐 주고 도와준 팀원들에게 감사!!!

결론

테스트 프레임워크 구축은 쉽지 않다.

그러나 노력한 만큼 복리로 돌려받을 수 있다.

테스터의 입장에서 코드를 바라보게 된다.(코드 품질이 향상되고, 좋은 버릇이 생긴다.)

모든 방법을 동원해서 테스트하라.

� 상상력이 필요합니다.

� TDD 는 도구이지 목표가 아니다.

� 1900 년 초부터 UnitTest 는 시작되었습니다.

참고자료

http://unittesthttp://unittest--cpp.sourceforge.net/cpp.sourceforge.net/�� UnitTestUnitTest++ ++ 소스소스 받는받는 곳곳

http://www.gamesfromwithin.comhttp://www.gamesfromwithin.com�� Noel Noel LlopisLlopis -- llopis@convexhull.comllopis@convexhull.com

�� GDC2006 GDC2006 발표자료발표자료

http://andstudy.com/andwiki/wiki.php/BackwardsIsFohttp://andstudy.com/andwiki/wiki.php/BackwardsIsForwardrward

�� 위위 자료를자료를 번역해번역해 놓은놓은 PPT PPT 및및 노트노트

테스트 주도 개발

단위 테스트 with JUnit

Working Effectively with Legacy Code

xUnit Test Patterns

Q & A

top related