유닛 테스트 개요
언리얼 엔진 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");
}
- 테스트 파일을 위한 디렉토리 생성 (예 :
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;
}
게임플레이 요소 유닛 테스트 작성 전략
게임플레이 요소 테스트 예시
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;
}
목(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;
}
비동기 코드 테스트 방법
비동기 코드 테스트 예시
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;
}
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
효과적인 테스트 커버리지 관리 전략
- 코드 커버리지 도구 사용 (예 : 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;
}
성능에 민감한 코드의 테스트 전략
- 프로파일링 도구와 테스트 통합
- 성능 벤치마크 테스트 작성
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;
}
테스트 가능한 코드 설계 원칙
- 단일 책임 원칙 (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;
}
유닛 테스트는 언리얼 엔진 C++ 프로젝트의 품질을 향상시키는 핵심 도구입니다.
자동화된 테스트 프레임워크를 활용하여 기본적인 테스트 케이스부터 복잡한 게임플레이 요소의 테스트까지 다양한 수준의 테스트를 구현할 수 있습니다.
목 객체와 스텁을 활용하면 복잡한 의존성을 가진 코드도 효과적으로 테스트할 수 있으며 비동기 코드 테스트를 위한 특별한 기법도 제공됩니다.
TDD 접근법을 적용하면 더 견고하고 유지보수가 쉬운 코드를 작성할 수 있습니다.