Smoke Tests Guide
March 11, 2026 ยท View on GitHub
Quick health checks and validation for critical systems
This guide covers smoke tests - rapid validation that critical systems are operational.
See also:
- Overview - Testing philosophy and architecture
- Unit Tests - Plugin-internal testing
- Integration Tests - Cross-plugin testing
- Automation - Running tests and CI/CD
Smoke Test Characteristics
Purpose: Rapid validation that critical systems are operational
Characteristics:
- Very fast (< 5 seconds total)
- Test only critical paths
- Run on every build
- Fail fast if something is broken
- Verify cross-plugin dependencies are satisfied
Examples:
- All subsystems initialize without crashing
- Plugins load and dependencies resolve
- ViewModels can be instantiated
- Critical assets can be loaded
- Menu system displays without errors
- Loading system can start a load
Where to Use:
- Pre-commit hooks (block commits if fails)
- CI/CD entry point (run before full test suite)
- Post-deployment validation
- Developer quick checks
Smoke Test Examples
Subsystem Health Check
/**
* Smoke test: Core subsystems initialize
* Quick check that all critical subsystems exist and initialize
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FSubsystemsSmokeTest,
"Project.Smoke.Subsystems",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FSubsystemsSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
// Verify all critical subsystems exist
TestNotNull(TEXT("ProjectLoadingSubsystem"),
GameInstance->GetSubsystem<UProjectLoadingSubsystem>());
TestNotNull(TEXT("ProjectSessionSubsystem"),
GameInstance->GetSubsystem<UProjectSessionSubsystem>());
TestNotNull(TEXT("ProjectUIFrameworkThemeManager"),
GameInstance->GetSubsystem<UProjectUIFrameworkThemeManager>());
return true;
}
ViewModels Instantiation
/**
* Smoke test: ViewModels instantiate
* Quick check that all ViewModels can be created
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FViewModelsSmokeTest,
"ProjectMenuMain.Smoke.ViewModels",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FViewModelsSmokeTest::RunTest(const FString& Parameters)
{
// Create all ViewModels
UProjectMenuViewModel* MenuVM = NewObject<UProjectMenuViewModel>();
UProjectMapListViewModel* MapListVM = NewObject<UProjectMapListViewModel>();
UProjectLoadingViewModel* LoadingVM = NewObject<UProjectLoadingViewModel>();
UProjectSettingsViewModel* SettingsVM = NewObject<UProjectSettingsViewModel>();
// Verify creation succeeded
TestNotNull(TEXT("MenuViewModel"), MenuVM);
TestNotNull(TEXT("MapListViewModel"), MapListVM);
TestNotNull(TEXT("LoadingViewModel"), LoadingVM);
TestNotNull(TEXT("SettingsViewModel"), SettingsVM);
// Initialize (should not crash)
MenuVM->Initialize(nullptr);
MapListVM->Initialize(nullptr);
LoadingVM->Initialize(nullptr);
SettingsVM->Initialize(nullptr);
// Cleanup
MenuVM->Shutdown();
MapListVM->Shutdown();
LoadingVM->Shutdown();
SettingsVM->Shutdown();
return true;
}
Screen Controllers Instantiation
/**
* Smoke test: Screen controllers instantiate
* Quick check that all screen controllers can be created
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FScreenControllersSmokeTest,
"ProjectMenuMain.Smoke.ScreenControllers",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FScreenControllersSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
// Create all screen controllers
UProjectMenuScreen* MenuScreen = CreateWidget<UProjectMenuScreen>(
GameInstance, UProjectMenuScreen::StaticClass());
UProjectMapBrowserScreen* MapBrowserScreen = CreateWidget<UProjectMapBrowserScreen>(
GameInstance, UProjectMapBrowserScreen::StaticClass());
UProjectLoadingScreen* LoadingScreen = CreateWidget<UProjectLoadingScreen>(
GameInstance, UProjectLoadingScreen::StaticClass());
// Verify creation succeeded
TestNotNull(TEXT("MenuScreen"), MenuScreen);
TestNotNull(TEXT("MapBrowserScreen"), MapBrowserScreen);
TestNotNull(TEXT("LoadingScreen"), LoadingScreen);
return true;
}
Plugin Pipeline Connectivity
/**
* Smoke test: Plugin pipeline connections
* Quick check that cross-plugin dependencies work
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FPluginPipelineSmokeTest,
"Project.Smoke.PluginPipeline",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FPluginPipelineSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
// Test ProjectMenuMain ??? ProjectLoading connection
UProjectMapListViewModel* MapListVM = NewObject<UProjectMapListViewModel>();
MapListVM->Initialize(GameInstance);
UProjectLoadingSubsystem* LoadingSubsystem =
GameInstance->GetSubsystem<UProjectLoadingSubsystem>();
TestNotNull(TEXT("MapListVM should access LoadingSubsystem"), LoadingSubsystem);
// Test ProjectMenuMain ??? ProjectData connection
MapListVM->RefreshMapList(); // Should query Asset Manager
// Should not crash (even if no manifests exist)
// Test ProjectUI ??? ProjectMenuMain connection
UProjectUIFrameworkThemeManager* ThemeManager =
GameInstance->GetSubsystem<UProjectUIFrameworkThemeManager>();
TestNotNull(TEXT("MenuScreen should access ThemeManager"), ThemeManager);
MapListVM->Shutdown();
return true;
}
Boot Flow Smoke Tests
Boot Subsystem Initialization
/**
* Smoke test: Boot subsystem initializes
* Verifies ProjectBootSubsystem can initialize without crashing
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootSubsystemSmokeTest,
"ProjectBoot.Smoke.SubsystemInit",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootSubsystemSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
UProjectBootSubsystem* BootSubsystem =
GameInstance->GetSubsystem<UProjectBootSubsystem>();
TestNotNull(TEXT("BootSubsystem should exist"), BootSubsystem);
// Should not crash during basic operations
bool bIsBootComplete = BootSubsystem->IsBootComplete();
TestFalse(TEXT("Boot should not be complete yet"), bIsBootComplete);
return true;
}
Boot Flow Controller Spawn
/**
* Smoke test: Boot flow controller spawns
* Verifies AProjectBootFlowController can be created
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootFlowControllerSmokeTest,
"ProjectBoot.Smoke.FlowController",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootFlowControllerSmokeTest::RunTest(const FString& Parameters)
{
UWorld* TestWorld = CreateTestWorld();
TestNotNull(TEXT("Test world should exist"), TestWorld);
AProjectBootFlowController* FlowController =
TestWorld->SpawnActor<AProjectBootFlowController>();
TestNotNull(TEXT("FlowController should spawn"), FlowController);
return true;
}
Feature Activation Smoke Tests
Feature Subsystem Initialization
/**
* Smoke test: Feature subsystems initialize
* Verifies ProjectFeatureActivationSubsystem and Registry exist
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FFeatureSubsystemsSmokeTest,
"Features.Smoke.Subsystems",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FFeatureSubsystemsSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
UProjectFeatureActivationSubsystem* ActivationSubsystem =
GameInstance->GetSubsystem<UProjectFeatureActivationSubsystem>();
TestNotNull(TEXT("FeatureActivationSubsystem should exist"), ActivationSubsystem);
UProjectFeatureRegistrySubsystem* RegistrySubsystem =
GameInstance->GetSubsystem<UProjectFeatureRegistrySubsystem>();
TestNotNull(TEXT("FeatureRegistrySubsystem should exist"), RegistrySubsystem);
return true;
}
Feature Manifest Loading
/**
* Smoke test: Feature manifests load
* Verifies feature manifests can be discovered via Asset Manager
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FFeatureManifestSmokeTest,
"Features.Smoke.Manifests",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FFeatureManifestSmokeTest::RunTest(const FString& Parameters)
{
UGameInstance* GameInstance = NewObject<UGameInstance>();
GameInstance->Init();
UProjectFeatureRegistrySubsystem* Registry =
GameInstance->GetSubsystem<UProjectFeatureRegistrySubsystem>();
TestNotNull(TEXT("Registry should exist"), Registry);
// Attempt to discover manifests (should not crash)
Registry->DiscoverFeatures();
// Should return >= 0 features (0 is valid if no manifests exist yet)
int32 FeatureCount = Registry->GetFeatureCount();
TestTrue(TEXT("Feature count should be >= 0"), FeatureCount >= 0);
return true;
}
Immutable Bootloader Smoke Tests
Manifest JSON Parse
/**
* Smoke test: Bootloader manifest parses
* Verifies manifest JSON can be parsed without errors
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootloaderManifestParseSmokeTest,
"ProjectBootloader.Smoke.ManifestParse",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootloaderManifestParseSmokeTest::RunTest(const FString& Parameters)
{
// Sample manifest JSON
FString ManifestJson = TEXT(R"({
"plugins": [
{
"uuid": "550e8400-e29b-41d4-a716-446655440001",
"name": "ProjectCore",
"version": "1.4.0",
"module": "ProjectCore",
"platform": "Windows",
"code": {
"url": "https://cdn.example.com/ProjectCore_1.4.0_code.zip",
"hash": "abc123",
"size": 1024
},
"assets": [
{
"url": "https://cdn.example.com/ProjectCore_1.4.0_content.utoc",
"hash": "def456",
"size": 2048,
"role": "utoc"
},
{
"url": "https://cdn.example.com/ProjectCore_1.4.0_content.ucas",
"hash": "789abc",
"size": 4096,
"role": "ucas"
}
],
"engine_build_id": "UE5.5.0",
"depends_on": [],
"signature_thumbprint": "1234567890ABCDEF"
}
]
})");
// Attempt to parse (replace with actual parsing logic)
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ManifestJson);
bool bSuccess = FJsonSerializer::Deserialize(Reader, JsonObject);
TestTrue(TEXT("Manifest should parse successfully"), bSuccess);
TestTrue(TEXT("JsonObject should be valid"), JsonObject.IsValid());
return true;
}
Decision Rule Behavior
/**
* Smoke test: Decision rule behavior
* Verifies code vs asset hash decision logic
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootloaderDecisionRuleSmokeTest,
"ProjectBootloader.Smoke.DecisionRule",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootloaderDecisionRuleSmokeTest::RunTest(const FString& Parameters)
{
// Sample test data
FString CurrentCodeHash = TEXT("abc123");
FString CurrentContentHash = TEXT("def456");
// Test 1: No changes -> up-to-date
FString NewCodeHash = TEXT("abc123");
FString NewContentHash = TEXT("def456");
TestTrue(TEXT("Should be up-to-date"),
CurrentCodeHash == NewCodeHash && CurrentContentHash == NewContentHash);
// Test 2: Code hash changed -> install-before-load (no restart in Shipping)
NewCodeHash = TEXT("xyz789");
NewContentHash = TEXT("def456");
TestTrue(TEXT("Code change should trigger restart"),
CurrentCodeHash != NewCodeHash);
// Test 3: Content hash changed -> hot-mount
NewCodeHash = TEXT("abc123");
NewContentHash = TEXT("uvw999");
TestTrue(TEXT("Content change should trigger hot-mount"),
CurrentCodeHash == NewCodeHash && CurrentContentHash != NewContentHash);
return true;
}
engine_build_id Mismatch Handling
/**
* Smoke test: engine_build_id mismatch aborts
* Verifies update is refused when engine build IDs don't match
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootloaderEngineBuildIdSmokeTest,
"ProjectBootloader.Smoke.EngineBuildId",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootloaderEngineBuildIdSmokeTest::RunTest(const FString& Parameters)
{
FString CurrentEngineBuildId = TEXT("UE5.5.0");
FString ManifestEngineBuildId = TEXT("UE5.6.0");
// Verify mismatch is detected
bool bMismatch = (CurrentEngineBuildId != ManifestEngineBuildId);
TestTrue(TEXT("Mismatch should be detected"), bMismatch);
// In real implementation, this should refuse the update
// (Orchestrator would log error and abort)
return true;
}
(Dev/Editor) pending_updates.json on Code Change
/**
* Smoke test (Dev/Editor): pending_updates.json written when simulating relaunch flow
* Verifies pending updates file is created when code changes
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FBootloaderPendingUpdatesSmokeTest,
"ProjectBootloader.Smoke.PendingUpdates",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FBootloaderPendingUpdatesSmokeTest::RunTest(const FString& Parameters)
{
// Simulate code change detection
bool bCodeChanged = true;
if (bCodeChanged)
{
// Dev/Editor-only: mock writing pending_updates.json
// In real implementation, Orchestrator would write (dev/editor):
// <local-app-data>/Alis/State/pending_updates.json
FString PendingUpdatesPath = TEXT("<local-app-data>/Alis/State/pending_updates.json");
// Verify path is constructed correctly
TestTrue(TEXT("Pending updates path should be valid"),
!PendingUpdatesPath.IsEmpty());
}
return true;
}
Details: boot_chain.md
Validation Tests
Purpose: Verify data integrity and asset correctness
Characteristics:
- Fast to medium speed
- Test data assets and manifests
- Verify references and dependencies
- Run on data changes
Examples:
- Manifest fields are valid
- Soft references resolve
- Required features are declared
- Asset naming conventions
Manifest Validation
/**
* Validation test: World manifests are valid
* Verifies all WorldManifest assets have required fields
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FWorldManifestValidationTest,
"ProjectData.Validation.WorldManifests",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FWorldManifestValidationTest::RunTest(const FString& Parameters)
{
UAssetManager& AssetManager = UAssetManager::Get();
// Query all WorldManifest assets
TArray<FAssetData> ManifestAssets;
AssetManager.GetPrimaryAssetDataList(FPrimaryAssetType("WorldManifest"), ManifestAssets);
int32 ValidCount = 0;
int32 InvalidCount = 0;
for (const FAssetData& AssetData : ManifestAssets)
{
UProjectWorldManifest* Manifest =
Cast<UProjectWorldManifest>(AssetData.GetAsset());
if (Manifest)
{
TArray<FString> Errors;
if (Manifest->Validate(Errors))
{
ValidCount++;
}
else
{
InvalidCount++;
for (const FString& Error : Errors)
{
AddError(FString::Printf(TEXT("%s: %s"),
*AssetData.AssetName.ToString(), *Error));
}
}
}
}
TestTrue(TEXT("Should have at least 1 manifest"), ValidCount > 0);
TestEqual(TEXT("All manifests should be valid"), InvalidCount, 0);
return true;
}
Asset Reference Validation
/**
* Validation test: Soft references resolve
* Verifies all soft object paths in manifests are valid
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FAssetReferenceValidationTest,
"ProjectData.Validation.AssetReferences",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
)
bool FAssetReferenceValidationTest::RunTest(const FString& Parameters)
{
UAssetManager& AssetManager = UAssetManager::Get();
TArray<FAssetData> ManifestAssets;
AssetManager.GetPrimaryAssetDataList(FPrimaryAssetType("WorldManifest"), ManifestAssets);
int32 BrokenReferences = 0;
for (const FAssetData& AssetData : ManifestAssets)
{
UProjectWorldManifest* Manifest =
Cast<UProjectWorldManifest>(AssetData.GetAsset());
if (Manifest && !Manifest->WorldPath.IsNull())
{
// Check if soft reference resolves
if (!Manifest->WorldPath.IsValid())
{
BrokenReferences++;
AddError(FString::Printf(TEXT("Broken reference in %s: %s"),
*AssetData.AssetName.ToString(),
*Manifest->WorldPath.ToString()));
}
}
}
TestEqual(TEXT("No broken references"), BrokenReferences, 0);
return true;
}
Running Smoke Tests
Automated
# Run only smoke tests (fastest)
make test-quick
Manual
UnrealEditor-Cmd.exe "Alis.uproject" \
-ExecCmds="Automation RunTests Project.Smoke;ProjectMenuMain.Smoke;ProjectBoot.Smoke" \
-unattended -nopause -nosplash -NullRHI \
-testexit="Automation Test Queue Empty" \
-log
Pre-Commit Hook
Add smoke tests to .git/hooks/pre-commit:
#!/bin/bash
echo "Running smoke tests..."
make test-quick
if [ $? -ne 0 ]; then
echo "??? Smoke tests failed! Commit aborted."
exit 1
fi
echo "??? Smoke tests passed!"
CI/CD Integration
Build Pipeline Entry Point
Smoke tests should be the first step in CI/CD:
stages:
- smoke # Fast health check (< 5s)
- test # Full test suite (30-60s)
- build # Compile game
- deploy # Package and deploy
smoke:
stage: smoke
script:
- make test-quick
timeout: 1m
retry: 0 # Fail fast, no retries
Benefits:
- Fails fast if critical systems are broken
- Saves CI time (don't run full suite if smoke fails)
- Early feedback to developers
Resources
See Also:
- Unit Tests - Plugin-internal testing
- Integration Tests - Cross-plugin testing
- Automation - Running tests and troubleshooting
Project Documentation:
License
Copyright ALIS. All Rights Reserved.