언리얼 엔진 C++ 프로젝트에서 유닛 테스트는 코드의 품질과 안정성을 보장하는 중요한 도구입니다.
이 절에서는 언리얼 엔진에서 유닛 테스트를 구현하고 실행하는 방법을 살펴보겠습니다.
언리얼 엔진의 자동화된 테스트 프레임워크 소개
언리얼 엔진은 Automation System
이라는 내장 테스트 프레임워크를 제공합니다. 이 프레임워크를 사용하여 유닛 테스트, 기능 테스트, 그리고 복잡한 시나리오 테스트를 작성할 수 있습니다.
테스트 모듈 설정 방법
프로젝트의 Build.cs
파일에 테스트 모듈 추가
PublicDependencyModuleNames . AddRange ( new string [] { "Core" , "CoreUObject" , "Engine" , "InputCore" , "HeadMountedDisplay" });
PrivateDependencyModuleNames . AddRange ( new string [] { "Slate" , "SlateCore" });
// 테스트 모듈 추가
if ( Target . Configuration != UnrealTargetConfiguration . Shipping )
{
PrivateDependencyModuleNames . Add ( "AutomationController" );
}
Copy
테스트 파일을 위한 디렉토리 생성 (예 : Source/MyProject/Tests/
)
기본적인 테스트 케이스 작성법
테스트 파일 예시 (MyMathTest.cpp
)
##include "CoreMinimal.h"
##include "Misc/AutomationTest.h"
##include "MyMath.h" // 테스트할 클래스 헤더
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FMyMathAddTest, "MyProject.Math.Addition" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::ProductFilter)
bool FMyMathAddTest :: RunTest ( const FString & Parameters )
{
// 테스트 로직
const int32 Result = UMyMath :: Add ( 2 , 3 );
TestEqual ( "2 + 3 should equal 5" , Result, 5 );
return true ;
}
Copy
게임플레이 요소에 대한 유닛 테스트 작성 전략
게임플레이 요소 테스트 예시
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FPlayerHealthTest, "MyProject.Gameplay.PlayerHealth" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::ProductFilter)
bool FPlayerHealthTest :: RunTest ( const FString & Parameters )
{
// 테스트 환경 설정
UWorld* World = UWorld :: CreateWorld ( EWorldType ::Game, false );
FWorldContext& WorldContext = GEngine -> CreateNewWorldContext ( EWorldType ::Game);
WorldContext . SetCurrentWorld (World);
// 플레이어 생성 및 초기화
AMyPlayerCharacter* Player = World -> SpawnActor <AMyPlayerCharacter>();
TestNotNull ( "Player should be spawned" , Player);
// 체력 테스트
const float InitialHealth = Player -> GetHealth ();
TestEqual ( "Initial health should be 100" , InitialHealth, 100.0f );
Player -> TakeDamage ( 20.0f );
TestEqual ( "Health after taking 20 damage should be 80" , Player -> GetHealth (), 80.0f );
// 정리
GEngine -> DestroyWorldContext (World);
World -> DestroyWorld ( false );
return true ;
}
Copy
목(mock) 객체 및 스텁(stub) 사용법
목 객체를 사용한 테스트 예시
class MockWeapon : public IWeapon
{
public:
MOCK_METHOD0 ( Fire , void ());
MOCK_METHOD0 ( Reload , void ());
};
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FPlayerWeaponTest, "MyProject.Gameplay.PlayerWeapon" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::ProductFilter)
bool FPlayerWeaponTest :: RunTest ( const FString & Parameters )
{
MockWeapon MockWeapon;
AMyPlayerCharacter* Player = World -> SpawnActor <AMyPlayerCharacter>();
EXPECT_CALL (MockWeapon, Fire ()). Times ( 1 );
Player -> SetWeapon (&MockWeapon);
Player -> FireWeapon ();
return true ;
}
Copy
비동기 코드 테스트 방법
비동기 코드 테스트 예시
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FAsyncLoadTest, "MyProject.Async.AssetLoading" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::ProductFilter)
bool FAsyncLoadTest :: RunTest ( const FString & Parameters )
{
ADD_LATENT_AUTOMATION_COMMAND ( FEngineWaitLatentCommand ( 5.0f ));
ADD_LATENT_AUTOMATION_COMMAND ( FLoadAssetLatentCommand ( this ));
ADD_LATENT_AUTOMATION_COMMAND ( FCheckAssetLoadedLatentCommand ( this ));
return true ;
}
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER (FLoadAssetLatentCommand, FAsyncLoadTest*, Test);
bool FLoadAssetLatentCommand :: Update ()
{
// 에셋 비동기 로드 시작
Test -> AssetLoader . RequestAsyncLoad ( "Path/To/Asset" );
return true ;
}
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER (FCheckAssetLoadedLatentCommand, FAsyncLoadTest*, Test);
bool FCheckAssetLoadedLatentCommand :: Update ()
{
if ( Test -> AssetLoader . IsAssetLoaded ())
{
Test -> TestTrue ( "Asset should be loaded" , Test -> AssetLoader . GetLoadedAsset () != nullptr );
return true ;
}
return false ;
}
Copy
테스트 주도 개발(TDD) 접근법의 언리얼 엔진 적용
TDD를 언리얼 엔진 프로젝트에 적용하는 단계
실패하는 테스트 작성
최소한의 코드로 테스트 통과
리팩토링
반복
지속적 통합(CI) 파이프라인에 유닛 테스트 통합 방법
Jenkins나 GitLab CI를 사용한 CI 파이프라인 설정 예시
stages :
- build
- test
build_job :
stage : build
script :
- path/to/UnrealBuildTool.exe MyProject Development Win64 -Project=path/to/MyProject.uproject -TargetType=Editor
test_job :
stage : test
script :
- path/to/UnrealEditor.exe path/to/MyProject.uproject -ExecCmds="Automation RunTests MyProject" -Unattended -NullRHI -NoSound -NoSplash -Log
Copy
효과적인 테스트 커버리지 관리 전략
코드 커버리지 도구 사용 (예 : OpenCppCoverage)
중요 기능에 대한 테스트 우선 작성
정기적인 커버리지 리포트 검토
대규모 프로젝트에서의 유닛 테스트 관리 방법
테스트 카테고리 및 태그 활용
테스트 실행 자동화
병렬 테스트 실행 구현
IMPLEMENT_COMPLEX_AUTOMATION_TEST (FPerformanceCriticalTest, "MyProject.Performance" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::HighPriority | EAutomationTestFlags ::ProductFilter)
void FPerformanceCriticalTest :: GetTests ( TArray < FString > & OutBeautifiedNames , TArray < FString > & OutTestCommands ) const
{
OutBeautifiedNames . Add ( "CriticalFunction1" );
OutTestCommands . Add ( "CriticalFunction1" );
OutBeautifiedNames . Add ( "CriticalFunction2" );
OutTestCommands . Add ( "CriticalFunction2" );
}
bool FPerformanceCriticalTest :: RunTest ( const FString & Parameters )
{
if (Parameters == "CriticalFunction1" )
{
// CriticalFunction1 테스트 로직
}
else if (Parameters == "CriticalFunction2" )
{
// CriticalFunction2 테스트 로직
}
return true ;
}
Copy
성능에 민감한 코드의 테스트 전략
프로파일링 도구와 테스트 통합
성능 벤치마크 테스트 작성
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FPerformanceBenchmarkTest, "MyProject.Performance.Benchmark" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::PerformanceFilter)
bool FPerformanceBenchmarkTest :: RunTest ( const FString & Parameters )
{
const double StartTime = FPlatformTime :: Seconds ();
// 성능 테스트할 함수 호출
PerformanceIntensiveFunction ();
const double EndTime = FPlatformTime :: Seconds ();
const double ElapsedTime = EndTime - StartTime;
TestTrue ( "Function should complete within 100ms" , ElapsedTime < 0.1 );
return true ;
}
Copy
테스트 가능한 코드 설계 원칙
단일 책임 원칙 (SRP) 준수
의존성 주입 활용
인터페이스 기반 프로그래밍
테스트 가능한 코드 예시
class IWeaponSystem
{
public:
virtual void Fire () = 0 ;
virtual int32 GetAmmo () const = 0 ;
};
class AMyCharacter : public ACharacter
{
UPROPERTY ()
TScriptInterface<IWeaponSystem> WeaponSystem;
public:
void SetWeaponSystem ( TScriptInterface < IWeaponSystem > NewWeaponSystem )
{
WeaponSystem = NewWeaponSystem;
}
void FireWeapon ()
{
if (WeaponSystem)
{
WeaponSystem -> Fire ();
}
}
};
// 테스트 코드
IMPLEMENT_SIMPLE_AUTOMATION_TEST (FCharacterWeaponTest, "MyProject.Character.Weapon" , EAutomationTestFlags ::ApplicationContextMask | EAutomationTestFlags ::ProductFilter)
bool FCharacterWeaponTest :: RunTest ( const FString & Parameters )
{
AMyCharacter* Character = World -> SpawnActor <AMyCharacter>();
// 목 객체 생성
TScriptInterface<IWeaponSystem> MockWeapon = NewObject < UMockWeaponSystem >();
Character -> SetWeaponSystem (MockWeapon);
Character -> FireWeapon ();
// MockWeaponSystem에서 Fire() 메서드가 호출되었는지 확인
UMockWeaponSystem* MockWeaponPtr = Cast < UMockWeaponSystem >( MockWeapon . GetObject ());
TestTrue ( "Fire method should be called" , MockWeaponPtr -> WasFireCalled ());
return true ;
}
Copy
유닛 테스트는 언리얼 엔진 C++ 프로젝트의 품질을 향상시키는 핵심 도구입니다. 자동화된 테스트 프레임워크를 활용하여 기본적인 테스트 케이스부터 복잡한 게임플레이 요소의 테스트까지 다양한 수준의 테스트를 구현할 수 있습니다.
목 객체와 스텁을 활용하면 복잡한 의존성을 가진 코드도 효과적으로 테스트할 수 있으며, 비동기 코드 테스트를 위한 특별한 기법도 제공됩니다. TDD 접근법을 적용하면 더 견고하고 유지보수가 쉬운 코드를 작성할 수 있습니다.
CI 파이프라인에 유닛 테스트를 통합하면 지속적으로 코드의 품질을 모니터링하고 유지할 수 있습니다. 대규모 프로젝트에서는 테스트의 체계적인 관리와 실행이 중요하며, 성능에 민감한 코드의 경우 특별한 테스트 전략이 필요합니다.
마지막으로, 테스트 가능한 코드를 설계하는 것이 중요합니다. 단일 책임 원칙을 준수하고, 의존성 주입을 활용하며, 인터페이스 기반으로 프로그래밍하면 더 쉽게 테스트할 수 있는 코드를 작성할 수 있습니다. 이러한 원칙들을 따르면 장기적으로 유지보수가 용이하고 안정적인 게임 코드를 개발할 수 있습니다.