UnitTest, Tdd For Games Kgc2007 ParkPD
description
Transcript of UnitTest, Tdd For Games Kgc2007 ParkPD
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 -- [email protected]@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