UE4自动化测试框架UnrealAutomator:从设计到实战的完整搭建指南

📅 2026/8/7 12:07:17
UE4自动化测试框架UnrealAutomator:从设计到实战的完整搭建指南
1. 项目概述与核心价值如果你是一名UE4开发者无论是独立制作人还是团队中的一员肯定都经历过这样的场景项目迭代到后期每次修改一个看似无关紧要的材质参数或者蓝图逻辑都得手动把整个游戏流程跑一遍生怕哪里又冒出一个诡异的Bug。更头疼的是UI交互、物理效果、动画状态这些内容靠人工测试不仅效率低下还容易遗漏。这就是为什么我们需要一套可靠的自动化测试框架而不仅仅是引擎自带的简单单元测试。今天要聊的就是如何从零开始为你的UE4项目搭建一个名为“UnrealAutomator”的自动化测试插件框架。这个名字听起来可能有点唬人但它的核心思想很直接利用UE4内置的Automation系统作为骨架通过插件的形式注入我们自定义的、更强大的测试能力让它能模拟玩家操作、验证游戏状态、甚至进行压力测试。这不仅仅是写几个测试用例而是构建一个可扩展、易维护的测试基础设施。对于中大型项目、需要频繁回归测试的团队或者追求交付质量的独立开发者来说这套框架能省下大量重复劳动的时间把精力真正投入到创造性的开发中。简单来说这个“UnrealAutomator”框架的目标是将那些繁琐、重复、易出错的游戏测试任务转化为可自动执行、可报告结果、可持续集成的代码。接下来我会结合我实际搭建和使用的经验拆解每一个步骤和背后的思考。2. 框架设计思路与核心组件选型在动手写代码之前我们必须想清楚框架要解决什么问题以及如何与UE4现有的生态结合。盲目开始很容易陷入“造轮子”或与引擎冲突的困境。2.1 为什么选择插件形式而非独立工具首先为什么是“插件”而不是一个独立的.exe程序这基于几个核心考量深度集成自动化测试需要直接访问游戏运行时Runtime的数据和接口。例如你需要获取一个Actor的世界坐标、检查一个Widget是否可见、或者触发一个特定的动画蒙太奇。这些操作通过插件内嵌在引擎中可以直接调用UObject接口和引擎模块拥有最高的权限和灵活性。独立外部工具通常只能通过有限的进程间通信IPC或网络接口功能受限且延迟高。生命周期管理测试框架的生命周期需要与游戏进程同步。插件可以方便地在游戏模块启动时初始化在关卡加载前后执行准备和清理工作在游戏结束时生成报告。这种紧密的绑定关系是外部工具难以实现的。资源访问测试用例可能需要引用项目内的蓝图、数据表、材质等资产。插件运行在引擎进程内可以无缝使用FSoftObjectPath或直接加载资源而外部工具处理资产路径会非常麻烦。部署简便插件可以打包成.uplugin文件随项目工程一起分发。对于团队协作开发者只需启用该插件即可在编辑器内或打包版本中运行测试无需额外安装配置复杂的测试环境。2.2 核心依赖UE4 Automation系统我们的框架不会完全另起炉灶而是建立在UE4自带的Automation系统之上。这是引擎提供的一个用于编写和运行自动化测试的框架它本身已经解决了测试发现、执行、报告等基础问题。我们的“UnrealAutomator”本质上是这个系统的一个功能增强插件。 Automation系统的核心是FAutomationTestBase类。我们编写的每一个测试用例比如“测试玩家跳跃功能”、“测试主菜单UI流程”都是一个继承自它的类。系统会自动发现这些类并提供命令行和编辑器UI来运行它们。它的优点是稳定、官方支持、与引擎构建流程集成好可以设置WithLowLevelTests等构建参数。但它的缺点是“偏底层”对于模拟鼠标点击、键盘输入、等待特定UI状态等高级游戏交互支持起来比较繁琐。这正是我们需要扩展的地方。2.3 UnrealAutomator的核心扩展点设计基于以上分析UnrealAutomator插件主要围绕以下几个核心模块进行构建交互模拟层这是框架的“手”和“眼睛”。我们需要封装一套API让测试代码能够模拟输入触发键盘按键、鼠标移动/点击、手柄输入。不能直接用FSlateApplication的底层接口那太脆弱。我们需要构建一个更稳定的输入模拟器能正确处理输入设备的上下文和游戏视口的坐标转换。查找与操作对象在庞大的World Outliner中精准找到我们要测试的Actor或UI Widget。这需要类似“选择器”的功能支持按名称、标签、类型、甚至是屏幕坐标来定位对象。状态查询与断言读取游戏对象的状态如血量值、动画状态机、Widget的可见性并与期望值进行比较断言。这是判断测试通过与否的依据。测试用例管理层在FAutomationTestBase基础上进行封装提供更友好的测试基类。比如提供SetUp/TearDown方法用于每个测试用例的初始化和清理提供通用的等待函数等待关卡加载完成、等待某个蓝图接口返回、等待特定事件发生避免测试代码里写满FPlatformProcess::Sleep这种不精确的休眠。测试流与数据驱动层支持将测试步骤和数据外部化。例如我们可以用JSON或CSV文件定义一系列的UI点击坐标和验证点测试框架读取文件并执行。这对于需要大量不同数据组合的测试如不同语言下的文本显示、不同分辨率下的UI布局非常有用。报告与可视化层增强Automation系统的原生报告。除了简单的“通过/失败”我们还可以截取失败时的游戏屏幕、记录错误发生前的操作日志、甚至录制一小段视频。这些丰富的诊断信息对于快速定位问题至关重要。同时可以在编辑器中提供一个简单的UI面板用于浏览测试用例、选择运行、并查看美观的测试报告。明确了这些设计目标我们就可以开始动手搭建了。3. 插件工程创建与基础结构搭建3.1 创建插件项目首先在你的UE4项目根目录下的Plugins文件夹里如果没有就创建一个新建一个文件夹命名为UnrealAutomator。然后在该文件夹内创建关键的插件描述文件UnrealAutomator.uplugin。{ FileVersion: 3, Version: 1, VersionName: 1.0, FriendlyName: UnrealAutomator, Description: 一个功能强大的UE4自动化测试框架插件用于模拟交互和验证游戏状态。, Category: Testing, CreatedBy: YourName, CreatedByURL: , DocsURL: , MarketplaceURL: , SupportURL: , EnabledByDefault: true, CanContainContent: false, IsBetaVersion: false, Installed: false, Modules: [ { Name: UnrealAutomator, Type: Runtime, LoadingPhase: Default }, { Name: UnrealAutomatorEditor, Type: Editor, LoadingPhase: PostEngineInit } ] }这里定义了两个模块UnrealAutomator运行时模块打包后仍可用和UnrealAutomatorEditor仅编辑器模块用于提供测试UI等工具。LoadingPhase设置为PostEngineInit确保引擎核心系统初始化完毕后再加载我们的编辑器模块避免依赖问题。3.2 构建模块文件接着在UnrealAutomator/Source目录下创建对应的模块目录和.Build.cs文件。UnrealAutomator.Build.cs(运行时模块):using UnrealBuildTool; public class UnrealAutomator : ModuleRules { public UnrealAutomator(ReadOnlyTargetRules Target) : base(Target) { PCHUsage ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange( new string[] { Core, CoreUObject, Engine, Slate, SlateCore, InputCore, // 输入模拟依赖 AutomationController, // 核心自动化测试模块 AutomationWorker } ); if (Target.bBuildEditor true) { // 如果需要在编辑器环境下运行运行时模块的代码可以添加编辑器模块依赖 PrivateDependencyModuleNames.Add(UnrealEd); } } }UnrealAutomatorEditor.Build.cs(编辑器模块):using UnrealBuildTool; public class UnrealAutomatorEditor : ModuleRules { public UnrealAutomatorEditor(ReadOnlyTargetRules Target) : base(Target) { PCHUsage ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange( new string[] { Core, CoreUObject, Engine, Slate, SlateCore, UnrealAutomator, // 依赖我们自己的运行时模块 AutomationController, EditorStyle, // 编辑器UI样式 WorkspaceMenuStructure // 用于将UI挂载到编辑器菜单 } ); } }注意模块依赖关系要清晰。编辑器模块依赖运行时模块这样编辑器UI可以调用运行时提供的测试执行功能。AutomationController是关键它提供了管理自动化测试的核心接口。3.3 实现基础模块类在两个模块的Private文件夹下分别创建UnrealAutomatorModule.cpp和UnrealAutomatorEditorModule.cpp实现StartupModule和ShutdownModule。目前可以先留空或只写日志确保插件能正确加载。// UnrealAutomatorModule.cpp #include Modules/ModuleManager.h IMPLEMENT_MODULE(FDefaultModuleImpl, UnrealAutomator);编辑器模块类似。完成这些后在UE4编辑器中打开“插件”窗口你应该能看到“UnrealAutomator”插件启用它并重启编辑器。4. 核心功能层实现交互模拟与对象查找这是框架最核心、也最容易踩坑的部分。我们的目标是让测试脚本能像真人一样“操作”游戏。4.1 输入模拟器的封装直接调用FSlateApplication::Get().ProcessKeyDownEvent()这类函数存在风险因为它绕过了游戏视图端GameViewport的输入路由可能导致输入状态不一致。更稳健的做法是向游戏视图端发送模拟输入事件。我们创建一个FAutomatorInputSimulator类// UnrealAutomator/Private/AutomatorInputSimulator.h #pragma once #include CoreMinimal.h class UGameViewportClient; class UNREALAUTOMATOR_API FAutomatorInputSimulator { public: FAutomatorInputSimulator(UGameViewportClient* InViewport); // 模拟键盘按键 void SimulateKeyPress(FKey Key, bool bRepeat false); void SimulateKeyRelease(FKey Key); // 模拟鼠标动作 void SimulateMouseMove(float X, float Y); void SimulateMouseButtonDown(FKey MouseButton); void SimulateMouseButtonUp(FKey MouseButton); void SimulateMouseWheel(float Delta); // 模拟手柄输入略 private: UGameViewportClient* TargetViewport; FVector2D CurrentMousePos; bool SendInputKey(FKey Key, bool bPressed); };实现的关键在于SendInputKey函数它需要构造一个FInputKeyEventArgs并传递给TargetViewport-ProcessInputKey。对于鼠标移动则需要调用ProcessInputMouseMove。这里有一个细节坐标需要是相对于游戏视图窗口的归一化坐标0到1之间而不是屏幕像素坐标。我们需要一个转换函数。// UnrealAutomator/Private/AutomatorInputSimulator.cpp #include AutomatorInputSimulator.h #include Engine/GameViewportClient.h #include Framework/Application/SlateApplication.h FAutomatorInputSimulator::FAutomatorInputSimulator(UGameViewportClient* InViewport) : TargetViewport(InViewport), CurrentMousePos(FVector2D::ZeroVector) { check(TargetViewport); } void FAutomatorInputSimulator::SimulateMouseMove(float X, float Y) { if (!TargetViewport) return; // 假设X, Y是0-1的归一化坐标 FVector2D NewPos(X, Y); FVector2D Delta NewPos - CurrentMousePos; CurrentMousePos NewPos; // 转换为视口坐标像素 FViewport* Viewport TargetViewport-Viewport; if (Viewport) { const FIntPoint ViewportSize Viewport-GetSizeXY(); const FIntPoint PixelDelta(Delta.X * ViewportSize.X, Delta.Y * ViewportSize.Y); const FIntPoint PixelPos(CurrentMousePos.X * ViewportSize.X, CurrentMousePos.Y * ViewportSize.Y); // 发送移动事件 TargetViewport-MouseMove(Viewport, PixelPos.X, PixelPos.Y); } } bool FAutomatorInputSimulator::SendInputKey(FKey Key, bool bPressed) { if (!TargetViewport || !TargetViewport-Viewport) return false; FInputKeyEventArgs Args( TargetViewport-Viewport, Key, bPressed ? IE_Pressed : IE_Released, 1.0f, // 模拟的按键力度对于非模拟键通常为1.0 false // 是否重复 ); return TargetViewport-ProcessInputKey(Args); }实操心得输入模拟的时机很重要。最好在游戏线程GameThread中执行这些操作因为ProcessInputKey不是线程安全的。我们可以在测试用例中利用AsyncTask或FFunctionGraphTask将输入模拟任务派发到游戏线程执行避免竞态条件。4.2 游戏对象查找器在庞大的游戏场景中精准定位一个测试目标是自动化测试的另一个挑战。我们实现一个FAutomatorObjectFinder工具类。// UnrealAutomator/Private/AutomatorObjectFinder.h #pragma once #include CoreMinimal.h #include UObject/Object.h #include Engine/World.h class UNREALAUTOMATOR_API FAutomatorObjectFinder { public: // 通过名称和类型查找Actor支持模糊匹配 templatetypename T static T* FindActorByName(UWorld* World, const FString NameSubstring, bool bExactMatch false); // 通过标签查找Actor templatetypename T static TArrayT* FindActorsByTag(UWorld* World, const FName Tag); // 查找UI Widget通过WidgetTree的路径或名称 static UWidget* FindWidgetByName(UUserWidget* RootWidget, const FString WidgetName); // 更高级的通过屏幕坐标拾取用于点击测试 static AActor* GetActorUnderCursor(UWorld* World, const FVector2D ScreenPos); // 等待某个Actor出现带超时 templatetypename T static T* WaitForActor(UWorld* World, const FString Name, float TimeoutSeconds 10.0f); };查找Actor相对简单遍历TActorIterator即可。查找UI Widget则复杂一些需要递归遍历WidgetTree。GetActorUnderCursor可以通过玩家控制器的GetHitResultAtScreenPosition方法实现射线检测这对测试3D场景中的交互物体非常有用。WaitForActor的实现体现了自动化测试的一个重要模式等待与轮询。游戏是异步的对象不会立即出现。我们的测试代码必须能够“等待”某个条件成立。templatetypename T T* FAutomatorObjectFinder::WaitForActor(UWorld* World, const FString Name, float TimeoutSeconds) { const float StartTime FPlatformTime::Seconds(); while (FPlatformTime::Seconds() - StartTime TimeoutSeconds) { T* FoundActor FindActorByNameT(World, Name, false); if (FoundActor) { return FoundActor; } // 重要让出当前线程时间片避免忙等待卡死引擎 FPlatformProcess::Sleep(0.1f); // 同时需要推进游戏世界的Tick在编辑器测试中可能需要手动Tick if (World-IsGameWorld()) { World-Tick(LEVELTICK_All, 0.1f); } } return nullptr; // 超时未找到 }注意事项在编辑器模式下运行测试时游戏世界可能不会自动Tick。上述代码中的World-Tick调用是一种简化处理。更健壮的做法是我们的测试框架提供一个“等待”工具函数它内部会处理编辑器与运行时模式下的Tick差异或者利用FTicker或定时器在游戏线程中安全地等待。5. 测试用例基类与实用工具封装有了底层工具我们需要为测试编写者提供一个友好、高效的API。我们将创建自己的测试基类FAutomatorTestBase它继承自FAutomationTestBase并封装常用操作。5.1 定义增强型测试基类// UnrealAutomator/Public/AutomatorTestBase.h #pragma once #include AutomationTest.h class UWorld; class UGameViewportClient; class FAutomatorInputSimulator; /** * UnrealAutomator框架的测试用例基类。 * 提供了对象查找、输入模拟、断言等待等便捷方法。 */ class UNREALAUTOMATOR_API FAutomatorTestBase : public FAutomationTestBase { public: FAutomatorTestBase(const FString InName, const bool bInComplexTask); // 重写以获取测试所需的上下文World, Viewport等 virtual void GetTests(TArrayFString OutBeautifiedNames, TArrayFString OutTestCommands) override; virtual bool RunTest(const FString Parameters) override; protected: // 工具方法 UWorld* GetTestWorld() const; UGameViewportClient* GetGameViewport() const; FAutomatorInputSimulator GetInputSimulator(); // 增强型断言带等待 bool TestEqual_Wait(const FString What, const auto Actual, const auto Expected, float WaitTime 5.0f, float CheckInterval 0.2f); // 等待条件成立 bool WaitForCondition(TFunctionbool() Condition, float TimeoutSeconds, float CheckInterval 0.1f, const FString TimeoutMessage TEXT()); // 执行一个操作并等待其完成例如点击按钮后等待界面跳转 void DoActionAndWait(TFunctionvoid() Action, TFunctionbool() WaitCondition, float TimeoutSeconds); // 截图功能用于失败报告 void TakeScreenshot(const FString ScreenshotName); private: TSharedPtrFAutomatorInputSimulator InputSimulator; };TestEqual_Wait是一个非常有用的模式。在游戏测试中很多状态变化不是瞬时的比如血量减少有一个数值滚动动画。普通的TestEqual断言会立即失败而TestEqual_Wait会在指定时间内周期性检查直到条件满足或超时。templatetypename T bool FAutomatorTestBase::TestEqual_Wait(const FString What, const T Actual, const T Expected, float WaitTime, float CheckInterval) { const float StartTime FPlatformTime::Seconds(); T CurrentValue Actual; while (FPlatformTime::Seconds() - StartTime WaitTime) { if (CurrentValue Expected) { AddInfo(FString::Printf(TEXT([等待断言成功] %s: 符合预期), *What)); return true; } FPlatformProcess::Sleep(CheckInterval); // 这里需要一种机制来更新CurrentValue通常需要传入一个Getter函数而非值。 // 更通用的实现是使用WaitForCondition。 } AddError(FString::Printf(TEXT([等待断言超时] %s: 实际值(%s) 不等于 期望值(%s)), *What, *LexToString(CurrentValue), *LexToString(Expected))); return false; }更通用的WaitForCondition实现如下bool FAutomatorTestBase::WaitForCondition(TFunctionbool() Condition, float TimeoutSeconds, float CheckInterval, const FString TimeoutMessage) { const float StartTime FPlatformTime::Seconds(); while (FPlatformTime::Seconds() - StartTime TimeoutSeconds) { if (Condition()) { return true; } // 在等待期间确保世界继续运转 if (UWorld* World GetTestWorld()) { if (World-IsGameWorld()) { World-Tick(LEVELTICK_All, CheckInterval); } } FPlatformProcess::Sleep(CheckInterval); } if (!TimeoutMessage.IsEmpty()) { AddError(FString::Printf(TEXT(等待条件超时: %s), *TimeoutMessage)); } return false; }5.2 编写第一个真正的测试用例现在我们可以用这个基类来编写一个具体的测试了。假设我们要测试游戏主菜单的“开始游戏”按钮。// 在项目的某个测试模块中例如 MyGame.Tests 模块 #include AutomatorTestBase.h IMPLEMENT_COMPLEX_AUTOMATION_TEST(FMainMenuStartButtonTest, MyGame.Feature.MainMenu.StartButton, EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) void FMainMenuStartButtonTest::GetTests(TArrayFString OutBeautifiedNames, TArrayFString OutTestCommands) { // 可以参数化测试这里我们只运行一个场景 OutBeautifiedNames.Add(TEXT(点击开始按钮应加载第一关)); OutTestCommands.Add(TEXT()); // 参数为空 } bool FMainMenuStartButtonTest::RunTest(const FString Parameters) { // 1. 获取世界和视口 UWorld* World GetTestWorld(); if (!World) { AddError(TEXT(无法获取测试世界)); return false; } // 2. 等待主菜单关卡加载完成假设关卡名称为“MainMenu” // 这里需要实现一个等待关卡加载的函数依赖于项目逻辑此处简化 AddInfo(TEXT(等待主菜单加载...)); if (!WaitForCondition([World](){ // 检查是否有一个代表主菜单的特定Actor或Widget存在 return FAutomatorObjectFinder::FindActorByNameAActor(World, TEXT(MainMenuRoot)) ! nullptr; }, 15.0f, TEXT(主菜单加载超时))) { return false; // 超时测试失败 } // 3. 查找“开始游戏”按钮Widget // 假设我们通过某种方式获取到了主菜单的根UserWidget UUserWidget* MainMenuWidget ...; UButton* StartButton CastUButton(FAutomatorObjectFinder::FindWidgetByName(MainMenuWidget, TEXT(StartButton))); if (!StartButton) { AddError(TEXT(未找到开始按钮)); return false; } // 4. 获取按钮在屏幕上的位置并模拟点击 FVector2D ButtonScreenPos; if (GetWidgetScreenPosition(StartButton, ButtonScreenPos)) // 需要实现此函数 { FAutomatorInputSimulator InputSim GetInputSimulator(); InputSim.SimulateMouseMove(ButtonScreenPos.X, ButtonScreenPos.Y); InputSim.SimulateMouseButtonDown(EKeys::LeftMouseButton); InputSim.SimulateMouseButtonUp(EKeys::LeftMouseButton); AddInfo(TEXT(已模拟点击开始按钮)); } // 5. 等待关卡切换例如通过检查当前关卡名称是否变为“Level01” AddInfo(TEXT(等待加载第一关...)); bool bLevelLoaded WaitForCondition([World](){ return World-GetMapName().Contains(TEXT(Level01)); }, 20.0f, TEXT(加载第一关超时)); if (bLevelLoaded) { AddInfo(TEXT(测试通过成功点击开始按钮并进入第一关)); return true; } else { // 失败时截图 TakeScreenshot(TEXT(MainMenuStartButton_Failure)); return false; } }这个例子展示了测试用例的基本结构准备环境、执行操作、验证结果。WaitForCondition的使用是关键它使测试代码能够适应游戏的异步特性。6. 编辑器集成与测试管理界面为了让测试更容易被团队使用我们提供一个编辑器内的UI面板。这将在UnrealAutomatorEditor模块中实现。6.1 创建编辑器工具栏按钮和窗口首先我们创建一个Slate Widget来作为我们的测试管理器窗口。// UnrealAutomatorEditor/Private/SAutomatorTestPanel.h #pragma once #include CoreMinimal.h #include Widgets/SCompoundWidget.h class SAutomatorTestPanel : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SAutomatorTestPanel) {} SLATE_END_ARGS() void Construct(const FArguments InArgs); // 刷新测试列表 void RefreshTestList(); private: // 测试列表视图相关 TSharedPtrSListViewTSharedPtrFString TestListView; TArrayTSharedPtrFString AvailableTests; // 按钮回调 FReply OnRunSelectedTestsClicked(); FReply OnRunAllTestsClicked(); // 从AutomationController获取所有测试 void QueryAvailableTests(); };然后在模块启动时我们将这个面板添加到编辑器菜单中。// UnrealAutomatorEditor/Private/UnrealAutomatorEditorModule.cpp #include Framework/MultiBox/MultiBoxBuilder.h #include SAutomatorTestPanel.h #include WorkspaceMenuStructure.h #include WorkspaceMenuStructureModule.h void FUnrealAutomatorEditorModule::StartupModule() { // 注册到Window菜单 FGlobalTabmanager::Get()-RegisterNomadTabSpawner( TEXT(UnrealAutomatorPanel), FOnSpawnTab::CreateRaw(this, FUnrealAutomatorEditorModule::SpawnAutomatorTab)) .SetDisplayName(FText::FromString(TEXT(UnrealAutomator))) .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsMiscCategory()) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), LevelEditor.Tabs.Details)); } TSharedRefSDockTab FUnrealAutomatorEditorModule::SpawnAutomatorTab(const FSpawnTabArgs Args) { return SNew(SDockTab) .TabRole(ETabRole::NomadTab) [ SNew(SAutomatorTestPanel) ]; }QueryAvailableTests函数需要调用AutomationController模块的接口来获取所有已注册的测试包括我们通过FAutomatorTestBase编写的那些。6.2 与AutomationController集成AutomationController是编辑器内管理自动化测试的核心模块。我们的面板需要与其交互来运行测试并获取结果。void SAutomatorTestPanel::QueryAvailableTests() { AvailableTests.Empty(); IAutomationControllerModule AutomationControllerModule FModuleManager::Get().LoadModuleCheckedIAutomationControllerModule(AutomationController); IAutomationControllerManagerPtr Controller AutomationControllerModule.GetAutomationController(); TArrayFAutomationTestInfo TestInfos; Controller-GetTestInfo(TestInfos); for (const FAutomationTestInfo Info : TestInfos) { // 可以在这里过滤测试比如只显示属于我们项目的测试 if (Info.GetTestName().StartsWith(TEXT(MyGame.))) { AvailableTests.Add(MakeSharedFString(Info.GetTestName())); } } if (TestListView.IsValid()) { TestListView-RequestListRefresh(); } } FReply SAutomatorTestPanel::OnRunSelectedTestsClicked() { TArrayTSharedPtrFString SelectedItems TestListView-GetSelectedItems(); if (SelectedItems.Num() 0) return FReply::Handled(); IAutomationControllerModule AutomationControllerModule FModuleManager::Get().LoadModuleCheckedIAutomationControllerModule(AutomationController); IAutomationControllerManagerPtr Controller AutomationControllerModule.GetAutomationController(); // 停止当前可能正在运行的测试 Controller-StopTests(); // 设置要运行的测试 TArrayFAutomationTestInfo TestInfos; Controller-GetTestInfo(TestInfos); TArrayFString TestsToRun; for (const auto Selected : SelectedItems) { TestsToRun.Add(*Selected); } Controller-SetTestsToRun(TestsToRun); // 开始运行测试通常会在PIE中运行 Controller-RunTests(); return FReply::Handled(); }运行测试后AutomationController会通过委托广播结果。我们可以监听这些委托在UI上更新进度和最终结果。7. 高级功能与实战技巧基础框架搭建完成后我们可以考虑一些增强功能让测试更强大、更易用。7.1 数据驱动测试将测试逻辑与测试数据分离是提高测试可维护性的好方法。我们可以支持从外部文件如JSON读取测试步骤。// TestData/CombatTest.json [ { TestName: PlayerAttack_Melee, Steps: [ { Action: PressKey, Params: { Key: LeftMouseButton, Duration: 0.1 } }, { Action: Wait, Params: { Time: 0.5 } }, { Action: AssertActorProperty, Params: { ActorName: TrainingDummy, PropertyPath: HealthComponent.CurrentHealth, ExpectedValue: 90, Comparison: LessThan } } ] } ]在测试用例中我们可以解析这个JSON然后在一个循环中执行每一步。FAutomatorTestBase可以提供一个ExecuteStep的虚函数由具体的测试类实现每个Action的操作。7.2 网络与多人游戏测试对于多人游戏自动化测试更加复杂。一个思路是启动多个客户端可以通过-game命令行参数启动独立进程和一个服务器进程然后使用我们的插件运行在其中一个客户端或服务器上作为“主控”通过RPC或网络消息来协调其他客户端的操作和验证状态。这需要更精细的进程间通信和同步控制。7.3 性能与压力测试集成Automation系统本身就支持性能捕捉如stat命令。我们可以扩展框架在特定的测试场景如复杂战斗、大量NPC中自动执行一系列操作同时记录帧时间、内存、DrawCall等数据并与基线进行比较自动判断是否有性能回归。8. 常见问题与排查技巧实录在实际开发和团队推广UnrealAutomator的过程中我遇到了不少典型问题这里分享一些排查思路。问题1输入模拟无效点击没反应。可能原因A坐标错误。模拟点击的屏幕坐标不对没有落在真正的UI按钮碰撞区域内。UI的屏幕坐标计算涉及视口缩放、DPI缩放等。务必使用FSlateApplication::Get().GetCursorPos()或Widget的几何信息来获取精确坐标。可能原因B输入被其他系统吞噬。某些特殊的UI如UMG的模态弹窗、Slate的复杂嵌套可能会拦截输入。尝试在点击前调用FSlateApplication::Get().SetAllUserFocusToGameViewport()将焦点强制设置到游戏视口。排查技巧在模拟点击前后用TakeScreenshot截图并在测试日志中输出你尝试点击的坐标和当前鼠标位置进行对比。也可以临时在游戏代码中加日志确认点击事件是否被触发。问题2测试在编辑器PIE中通过但打包后失败。可能原因A资源引用问题。测试代码中使用了编辑器路径如/Game/...硬编码查找资源打包后路径或加载方式可能不同。应使用FSoftObjectPath或通过GameInstance等持久化对象间接获取。可能原因B模块加载顺序。测试插件或相关模块在打包版本中的加载时机可能与编辑器不同。确保你的运行时模块在.uproject文件的LoadingPhase设置正确或者使用ModuleManager动态加载。排查技巧在打包版本中运行测试时确保将测试日志输出到文件或控制台。对比编辑器模式和打包模式的日志差异尤其是初始化阶段和资源加载阶段的日志。问题3测试不稳定时而通过时而失败。可能原因A时机问题Race Condition。这是自动化测试最常见的问题。比如点击按钮后立即检查界面状态但界面切换有一个动画过渡。解决方案就是充分使用等待Wait并且等待条件要具体如“等待某个特定的Widget可见”而不是傻等固定时间。可能原因B随机元素。游戏中有随机数如暴击、怪物刷新点。测试需要能够控制或重置随机种子确保每次运行结果可预测。可以在测试的SetUp阶段设置固定的随机种子。排查技巧为不稳定的测试增加更详细的日志记录每个关键步骤前后的游戏状态。考虑引入重试机制Retry对于非核心的偶发失败可以自动重试几次。问题4测试运行速度太慢。可能原因不必要的等待和休眠。滥用FPlatformProcess::Sleep会极大拖慢测试速度。尽量使用基于条件的等待WaitForCondition条件满足就立刻继续。优化技巧并行化测试如果测试用例之间没有依赖可以利用Automation系统支持的Parallel测试标志同时运行多个测试需要注意它们不能操作同一个游戏实例。减少关卡加载使用-game模式并配合Open控制台命令切换子关卡比反复重启PIE要快得多。使用最小化资源为自动化测试专门准备一个轻量级的测试关卡移除高清纹理、复杂粒子等非必要资源。问题5如何测试复杂的游戏逻辑如AI行为、物理交互策略对于非常复杂或难以通过外部输入模拟验证的逻辑可以考虑“白盒测试”。即在你的游戏代码中暴露一些内部状态查询接口例如给AI控制器添加一个GetCurrentGoal()方法专门用于测试。虽然这在一定程度上破坏了封装但对于保证核心逻辑的正确性是值得的。这些接口可以通过编译开关如#if WITH_AUTOMATION_TESTS来控制只在测试版本中启用。搭建UnrealAutomator这样的框架初期投入确实不小。但一旦成型它将成为项目质量的稳定器。我的建议是从一个小而具体的测试用例开始比如“按ESC键能否调出暂停菜单”逐步完善框架功能并让团队看到它节省的时间和避免的线上问题自然就能获得支持。记住好的自动化测试不是一蹴而就的而是随着项目迭代不断积累和优化的资产。