mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 06:58:05 +02:00
feat: security overhaul (proxy ssrf fix, streaming sanitization, docker optimization)
This commit is contained in:
1 parent
6d87ada2b8
commit
8df9a37baa
810 files changed
+131090
-151
No files matched your search
@@ -0,0 +1,220 @@
|
||||
# Balance Testing for Games
|
||||
|
||||
## Overview
|
||||
|
||||
Balance testing validates that your game's systems create fair, engaging, and appropriately challenging experiences. It covers difficulty, economy, progression, and competitive balance.
|
||||
|
||||
## Types of Balance
|
||||
|
||||
### Difficulty Balance
|
||||
|
||||
- Is the game appropriately challenging?
|
||||
- Does difficulty progress smoothly?
|
||||
- Are difficulty spikes intentional?
|
||||
|
||||
### Economy Balance
|
||||
|
||||
- Is currency earned at the right rate?
|
||||
- Are prices fair for items/upgrades?
|
||||
- Can the economy be exploited?
|
||||
|
||||
### Progression Balance
|
||||
|
||||
- Does power growth feel satisfying?
|
||||
- Are unlocks paced well?
|
||||
- Is there meaningful choice in builds?
|
||||
|
||||
### Competitive Balance
|
||||
|
||||
- Are all options viable?
|
||||
- Is there a dominant strategy?
|
||||
- Do counters exist for strong options?
|
||||
|
||||
## Balance Testing Methods
|
||||
|
||||
### Spreadsheet Modeling
|
||||
|
||||
Before implementation, model systems mathematically:
|
||||
|
||||
- DPS calculations
|
||||
- Time-to-kill analysis
|
||||
- Economy simulations
|
||||
- Progression curves
|
||||
|
||||
### Automated Simulation
|
||||
|
||||
Run thousands of simulated games:
|
||||
|
||||
- AI vs AI battles
|
||||
- Economy simulations
|
||||
- Progression modeling
|
||||
- Monte Carlo analysis
|
||||
|
||||
### Telemetry Analysis
|
||||
|
||||
Gather data from real players:
|
||||
|
||||
- Win rates by character/weapon/strategy
|
||||
- Currency flow analysis
|
||||
- Completion rates by level
|
||||
- Time to reach milestones
|
||||
|
||||
### Expert Testing
|
||||
|
||||
High-skill players identify issues:
|
||||
|
||||
- Exploits and degenerate strategies
|
||||
- Underpowered options
|
||||
- Skill ceiling concerns
|
||||
- Meta predictions
|
||||
|
||||
## Key Balance Metrics
|
||||
|
||||
### Combat Balance
|
||||
|
||||
| Metric | Target | Red Flag |
|
||||
| ------------------------- | ------------------- | ------------------------- |
|
||||
| Win rate (symmetric) | 50% | <45% or >55% |
|
||||
| Win rate (asymmetric) | Varies by design | Outliers by >10% |
|
||||
| Time-to-kill | Design dependent | Too fast = no counterplay |
|
||||
| Damage dealt distribution | Even across options | One option dominates |
|
||||
|
||||
### Economy Balance
|
||||
|
||||
| Metric | Target | Red Flag |
|
||||
| -------------------- | -------------------- | ------------------------------- |
|
||||
| Currency earned/hour | Design dependent | Too fast = trivializes content |
|
||||
| Item purchase rate | Healthy distribution | Nothing bought = bad prices |
|
||||
| Currency on hand | Healthy churn | Hoarding = nothing worth buying |
|
||||
| Premium currency | Reasonable value | Pay-to-win concerns |
|
||||
|
||||
### Progression Balance
|
||||
|
||||
| Metric | Target | Red Flag |
|
||||
| ------------------ | ---------------------- | ---------------------- |
|
||||
| Time to max level | Design dependent | Too fast = no journey |
|
||||
| Power growth curve | Smooth, satisfying | Flat periods = boring |
|
||||
| Build diversity | Multiple viable builds | One "best" build |
|
||||
| Content completion | Healthy progression | Walls or trivial skips |
|
||||
|
||||
## Balance Testing Process
|
||||
|
||||
### 1. Define Design Intent
|
||||
|
||||
- What experience are you creating?
|
||||
- What should feel powerful?
|
||||
- What trade-offs should exist?
|
||||
|
||||
### 2. Model Before Building
|
||||
|
||||
- Spreadsheet the math
|
||||
- Simulate outcomes
|
||||
- Identify potential issues
|
||||
|
||||
### 3. Test Incrementally
|
||||
|
||||
- Test each system in isolation
|
||||
- Then test systems together
|
||||
- Then test at scale
|
||||
|
||||
### 4. Gather Data
|
||||
|
||||
- Internal playtesting
|
||||
- Telemetry from beta
|
||||
- Expert feedback
|
||||
|
||||
### 5. Iterate
|
||||
|
||||
- Adjust based on data
|
||||
- Re-test changes
|
||||
- Document rationale
|
||||
|
||||
## Common Balance Issues
|
||||
|
||||
### Power Creep
|
||||
|
||||
- **Symptom:** New content is always stronger
|
||||
- **Cause:** Fear of releasing weak content
|
||||
- **Fix:** Sidegrades over upgrades, periodic rebalancing
|
||||
|
||||
### Dominant Strategy
|
||||
|
||||
- **Symptom:** One approach beats all others
|
||||
- **Cause:** Insufficient counters, math oversight
|
||||
- **Fix:** Add counters, nerf dominant option, buff alternatives
|
||||
|
||||
### Feast or Famine
|
||||
|
||||
- **Symptom:** Players either crush or get crushed
|
||||
- **Cause:** Snowball mechanics, high variance
|
||||
- **Fix:** Comeback mechanics, reduce variance
|
||||
|
||||
### Analysis Paralysis
|
||||
|
||||
- **Symptom:** Too many options, players can't choose
|
||||
- **Cause:** Over-complicated systems
|
||||
- **Fix:** Simplify, provide recommendations
|
||||
|
||||
## Balance Tools
|
||||
|
||||
### Spreadsheets
|
||||
|
||||
- Model DPS, TTK, economy
|
||||
- Simulate progression
|
||||
- Compare options side-by-side
|
||||
|
||||
### Simulation Frameworks
|
||||
|
||||
- Monte Carlo for variance
|
||||
- AI bots for combat testing
|
||||
- Economy simulations
|
||||
|
||||
### Telemetry Systems
|
||||
|
||||
- Track player choices
|
||||
- Measure outcomes
|
||||
- A/B test changes
|
||||
|
||||
### Visualization
|
||||
|
||||
- Graphs of win rates over time
|
||||
- Heat maps of player deaths
|
||||
- Flow charts of progression
|
||||
|
||||
## Balance Testing Checklist
|
||||
|
||||
### Pre-Launch
|
||||
|
||||
- [ ] Core systems modeled in spreadsheets
|
||||
- [ ] Internal playtesting complete
|
||||
- [ ] No obvious dominant strategies
|
||||
- [ ] Difficulty curve feels right
|
||||
- [ ] Economy tested for exploits
|
||||
- [ ] Progression pacing validated
|
||||
|
||||
### Live Service
|
||||
|
||||
- [ ] Telemetry tracking key metrics
|
||||
- [ ] Regular balance reviews scheduled
|
||||
- [ ] Player feedback channels monitored
|
||||
- [ ] Hotfix process for critical issues
|
||||
- [ ] Communication plan for changes
|
||||
|
||||
## Communicating Balance Changes
|
||||
|
||||
### Patch Notes Best Practices
|
||||
|
||||
- Explain the "why" not just the "what"
|
||||
- Use concrete numbers when possible
|
||||
- Acknowledge player concerns
|
||||
- Set expectations for future changes
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
**Sword of Valor - Damage reduced from 100 to 85**
|
||||
Win rate for Sword users was 58%, indicating it was
|
||||
overperforming. This brings it in line with other weapons
|
||||
while maintaining its identity as a high-damage option.
|
||||
We'll continue monitoring and adjust if needed.
|
||||
```
|
||||
@@ -0,0 +1,319 @@
|
||||
# Platform Certification Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Certification testing ensures games meet platform holder requirements (Sony TRC, Microsoft XR, Nintendo Guidelines). Failing certification delays launch and costs money—test thoroughly before submission.
|
||||
|
||||
## Platform Requirements Overview
|
||||
|
||||
### Major Platforms
|
||||
|
||||
| Platform | Requirements Doc | Submission Portal |
|
||||
| --------------- | -------------------------------------- | ------------------------- |
|
||||
| PlayStation | TRC (Technical Requirements Checklist) | PlayStation Partners |
|
||||
| Xbox | XR (Xbox Requirements) | Xbox Partner Center |
|
||||
| Nintendo Switch | Guidelines | Nintendo Developer Portal |
|
||||
| Steam | Guidelines (less strict) | Steamworks |
|
||||
| iOS | App Store Guidelines | App Store Connect |
|
||||
| Android | Play Store Policies | Google Play Console |
|
||||
|
||||
## Common Certification Categories
|
||||
|
||||
### Account and User Management
|
||||
|
||||
```
|
||||
REQUIREMENT: User Switching
|
||||
GIVEN user is playing game
|
||||
WHEN system-level user switch occurs
|
||||
THEN game handles transition gracefully
|
||||
AND no data corruption
|
||||
AND correct user data loads
|
||||
|
||||
REQUIREMENT: Guest Accounts
|
||||
GIVEN guest user plays game
|
||||
WHEN guest makes progress
|
||||
THEN progress is not saved to other accounts
|
||||
AND appropriate warnings displayed
|
||||
|
||||
REQUIREMENT: Parental Controls
|
||||
GIVEN parental controls restrict content
|
||||
WHEN restricted content is accessed
|
||||
THEN content is blocked or modified
|
||||
AND appropriate messaging shown
|
||||
```
|
||||
|
||||
### System Events
|
||||
|
||||
```
|
||||
REQUIREMENT: Suspend/Resume (PS4/PS5)
|
||||
GIVEN game is running
|
||||
WHEN console enters rest mode
|
||||
AND console wakes from rest mode
|
||||
THEN game resumes correctly
|
||||
AND network reconnects if needed
|
||||
AND no audio/visual glitches
|
||||
|
||||
REQUIREMENT: Controller Disconnect
|
||||
GIVEN player is in gameplay
|
||||
WHEN controller battery dies
|
||||
THEN game pauses immediately
|
||||
AND reconnect prompt appears
|
||||
AND gameplay resumes when connected
|
||||
|
||||
REQUIREMENT: Storage Full
|
||||
GIVEN storage is nearly full
|
||||
WHEN game attempts save
|
||||
THEN graceful error handling
|
||||
AND user informed of issue
|
||||
AND no data corruption
|
||||
```
|
||||
|
||||
### Network Requirements
|
||||
|
||||
```
|
||||
REQUIREMENT: PSN/Xbox Live Unavailable
|
||||
GIVEN online features
|
||||
WHEN platform network is unavailable
|
||||
THEN offline features still work
|
||||
AND appropriate error messages
|
||||
AND no crashes
|
||||
|
||||
REQUIREMENT: Network Transition
|
||||
GIVEN active online session
|
||||
WHEN network connection lost
|
||||
THEN graceful handling
|
||||
AND reconnection attempted
|
||||
AND user informed of status
|
||||
|
||||
REQUIREMENT: NAT Type Handling
|
||||
GIVEN various NAT configurations
|
||||
WHEN multiplayer is attempted
|
||||
THEN appropriate feedback on connectivity
|
||||
AND fallback options offered
|
||||
```
|
||||
|
||||
### Save Data
|
||||
|
||||
```
|
||||
REQUIREMENT: Save Data Integrity
|
||||
GIVEN save data exists
|
||||
WHEN save is loaded
|
||||
THEN data is validated
|
||||
AND corrupted data handled gracefully
|
||||
AND no crashes on invalid data
|
||||
|
||||
REQUIREMENT: Cloud Save Sync
|
||||
GIVEN cloud saves enabled
|
||||
WHEN save conflict occurs
|
||||
THEN user chooses which to keep
|
||||
AND no silent data loss
|
||||
|
||||
REQUIREMENT: Save Data Portability (PS4→PS5)
|
||||
GIVEN save from previous generation
|
||||
WHEN loaded on current generation
|
||||
THEN data migrates correctly
|
||||
AND no features lost
|
||||
```
|
||||
|
||||
## Platform-Specific Requirements
|
||||
|
||||
### PlayStation (TRC)
|
||||
|
||||
| Requirement | Description | Priority |
|
||||
| ----------- | --------------------------- | -------- |
|
||||
| TRC R4010 | Suspend/resume handling | Critical |
|
||||
| TRC R4037 | User switching | Critical |
|
||||
| TRC R4062 | Parental controls | Critical |
|
||||
| TRC R4103 | PS VR comfort ratings | VR only |
|
||||
| TRC R4120 | DualSense haptics standards | PS5 |
|
||||
| TRC R5102 | PSN sign-in requirements | Online |
|
||||
|
||||
### Xbox (XR)
|
||||
|
||||
| Requirement | Description | Priority |
|
||||
| ----------- | ----------------------------- | ----------- |
|
||||
| XR-015 | Title timeout handling | Critical |
|
||||
| XR-045 | User sign-out handling | Critical |
|
||||
| XR-067 | Active user requirement | Critical |
|
||||
| XR-074 | Quick Resume support | Series X/S |
|
||||
| XR-115 | Xbox Accessibility Guidelines | Recommended |
|
||||
|
||||
### Nintendo Switch
|
||||
|
||||
| Requirement | Description | Priority |
|
||||
| ------------------ | ------------------- | -------- |
|
||||
| Docked/Handheld | Seamless transition | Critical |
|
||||
| Joy-Con detachment | Controller handling | Critical |
|
||||
| Home button | Immediate response | Critical |
|
||||
| Screenshots/Video | Proper support | Required |
|
||||
| Sleep mode | Resume correctly | Critical |
|
||||
|
||||
## Automated Test Examples
|
||||
|
||||
### System Event Testing
|
||||
|
||||
```cpp
|
||||
// Unreal - Suspend/Resume Test
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FSuspendResumeTest,
|
||||
"Certification.System.SuspendResume",
|
||||
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter
|
||||
)
|
||||
|
||||
bool FSuspendResumeTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Get game state before suspend
|
||||
FGameState StateBefore = GetCurrentGameState();
|
||||
|
||||
// Simulate suspend
|
||||
FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Broadcast();
|
||||
|
||||
// Simulate resume
|
||||
FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Broadcast();
|
||||
|
||||
// Verify state matches
|
||||
FGameState StateAfter = GetCurrentGameState();
|
||||
|
||||
TestEqual("Player position preserved",
|
||||
StateAfter.PlayerPosition, StateBefore.PlayerPosition);
|
||||
TestEqual("Game progress preserved",
|
||||
StateAfter.Progress, StateBefore.Progress);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Unity - Controller Disconnect Test
|
||||
[UnityTest]
|
||||
public IEnumerator ControllerDisconnect_ShowsPauseMenu()
|
||||
{
|
||||
// Simulate gameplay
|
||||
GameManager.Instance.StartGame();
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Simulate controller disconnect
|
||||
InputSystem.DisconnectDevice(Gamepad.current);
|
||||
yield return null;
|
||||
|
||||
// Verify pause menu shown
|
||||
Assert.IsTrue(PauseMenu.IsVisible, "Pause menu should appear");
|
||||
Assert.IsTrue(Time.timeScale == 0, "Game should be paused");
|
||||
|
||||
// Simulate reconnect
|
||||
InputSystem.ReconnectDevice(Gamepad.current);
|
||||
yield return null;
|
||||
|
||||
// Verify prompt appears
|
||||
Assert.IsTrue(ReconnectPrompt.IsVisible);
|
||||
}
|
||||
```
|
||||
|
||||
```gdscript
|
||||
# Godot - Save Corruption Test
|
||||
func test_corrupted_save_handling():
|
||||
# Create corrupted save file
|
||||
var file = FileAccess.open("user://save_corrupt.dat", FileAccess.WRITE)
|
||||
file.store_string("CORRUPTED_GARBAGE_DATA")
|
||||
file.close()
|
||||
|
||||
# Attempt to load
|
||||
var result = SaveManager.load("save_corrupt")
|
||||
|
||||
# Should handle gracefully
|
||||
assert_null(result, "Should return null for corrupted save")
|
||||
assert_false(OS.has_feature("crashed"), "Should not crash")
|
||||
|
||||
# Should show user message
|
||||
var message_shown = ErrorDisplay.current_message != ""
|
||||
assert_true(message_shown, "Should inform user of corruption")
|
||||
```
|
||||
|
||||
## Pre-Submission Checklist
|
||||
|
||||
### General Requirements
|
||||
|
||||
- [ ] Game boots to interactive state within platform time limit
|
||||
- [ ] Controller disconnect pauses game
|
||||
- [ ] User sign-out handled correctly
|
||||
- [ ] Save data validates on load
|
||||
- [ ] No crashes in 8+ hours of automated testing
|
||||
- [ ] Memory usage within platform limits
|
||||
- [ ] Load times meet requirements
|
||||
|
||||
### Platform Services
|
||||
|
||||
- [ ] Achievements/Trophies work correctly
|
||||
- [ ] Friends list integration works
|
||||
- [ ] Invite system functions
|
||||
- [ ] Store/DLC integration validated
|
||||
- [ ] Cloud saves sync properly
|
||||
|
||||
### Accessibility (Increasingly Required)
|
||||
|
||||
- [ ] Text size options
|
||||
- [ ] Colorblind modes
|
||||
- [ ] Subtitle options
|
||||
- [ ] Controller remapping
|
||||
- [ ] Screen reader support (where applicable)
|
||||
|
||||
### Content Compliance
|
||||
|
||||
- [ ] Age rating displayed correctly
|
||||
- [ ] Parental controls respected
|
||||
- [ ] No prohibited content
|
||||
- [ ] Required legal text present
|
||||
|
||||
## Common Certification Failures
|
||||
|
||||
| Issue | Platform | Fix |
|
||||
| --------------------- | ------------ | ----------------------------------- |
|
||||
| Home button delay | All consoles | Respond within required time |
|
||||
| Controller timeout | PlayStation | Handle reactivation properly |
|
||||
| Save on suspend | PlayStation | Don't save during suspend |
|
||||
| User context loss | Xbox | Track active user correctly |
|
||||
| Joy-Con drift | Switch | Proper deadzone handling |
|
||||
| Background memory | Mobile | Release resources when backgrounded |
|
||||
| Crash on corrupt data | All | Validate all loaded data |
|
||||
|
||||
## Testing Matrix
|
||||
|
||||
### Build Configurations to Test
|
||||
|
||||
| Configuration | Scenarios |
|
||||
| --------------- | ----------------------- |
|
||||
| First boot | No save data exists |
|
||||
| Return user | Save data present |
|
||||
| Upgrade path | Previous version save |
|
||||
| Fresh install | After uninstall |
|
||||
| Low storage | Minimum space available |
|
||||
| Network offline | No connectivity |
|
||||
|
||||
### Hardware Variants
|
||||
|
||||
| Platform | Variants to Test |
|
||||
| ----------- | ------------------------------- |
|
||||
| PlayStation | PS4, PS4 Pro, PS5 |
|
||||
| Xbox | One, One X, Series S, Series X |
|
||||
| Switch | Docked, Handheld, Lite |
|
||||
| PC | Min spec, recommended, high-end |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Read platform requirements document thoroughly
|
||||
- Test on actual hardware, not just dev kits
|
||||
- Automate certification test scenarios
|
||||
- Submit with extra time for re-submission
|
||||
- Document all edge case handling
|
||||
- Test with real user accounts
|
||||
|
||||
### DON'T
|
||||
|
||||
- Assume debug builds behave like retail
|
||||
- Skip testing on oldest supported hardware
|
||||
- Ignore platform-specific features
|
||||
- Wait until last minute to test certification items
|
||||
- Use placeholder content in submission build
|
||||
- Skip testing with real platform services
|
||||
@@ -0,0 +1,228 @@
|
||||
# Compatibility Testing for Games
|
||||
|
||||
## Overview
|
||||
|
||||
Compatibility testing ensures your game works correctly across different hardware, operating systems, and configurations that players use.
|
||||
|
||||
## Types of Compatibility Testing
|
||||
|
||||
### Hardware Compatibility
|
||||
|
||||
- Graphics cards (NVIDIA, AMD, Intel)
|
||||
- CPUs (Intel, AMD, Apple Silicon)
|
||||
- Memory configurations
|
||||
- Storage types (HDD, SSD, NVMe)
|
||||
- Input devices (controllers, keyboards, mice)
|
||||
|
||||
### Software Compatibility
|
||||
|
||||
- Operating system versions
|
||||
- Driver versions
|
||||
- Background software conflicts
|
||||
- Antivirus interference
|
||||
|
||||
### Platform Compatibility
|
||||
|
||||
- Console SKUs (PS5, Xbox Series X|S)
|
||||
- PC storefronts (Steam, Epic, GOG)
|
||||
- Mobile devices (iOS, Android)
|
||||
- Cloud gaming services
|
||||
|
||||
### Configuration Compatibility
|
||||
|
||||
- Graphics settings combinations
|
||||
- Resolution and aspect ratios
|
||||
- Refresh rates (60Hz, 144Hz, etc.)
|
||||
- HDR and color profiles
|
||||
|
||||
## Testing Matrix
|
||||
|
||||
### Minimum Hardware Matrix
|
||||
|
||||
| Component | Budget | Mid-Range | High-End |
|
||||
| --------- | -------- | --------- | -------- |
|
||||
| GPU | GTX 1050 | RTX 3060 | RTX 4080 |
|
||||
| CPU | i5-6400 | i7-10700 | i9-13900 |
|
||||
| RAM | 8GB | 16GB | 32GB |
|
||||
| Storage | HDD | SATA SSD | NVMe |
|
||||
|
||||
### OS Matrix
|
||||
|
||||
- Windows 10 (21H2, 22H2)
|
||||
- Windows 11 (22H2, 23H2)
|
||||
- macOS (Ventura, Sonoma)
|
||||
- Linux (Ubuntu LTS, SteamOS)
|
||||
|
||||
### Controller Matrix
|
||||
|
||||
- Xbox Controller (wired, wireless, Elite)
|
||||
- PlayStation DualSense
|
||||
- Nintendo Pro Controller
|
||||
- Generic XInput controllers
|
||||
- Keyboard + Mouse
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### 1. Define Supported Configurations
|
||||
|
||||
- Minimum specifications
|
||||
- Recommended specifications
|
||||
- Officially supported platforms
|
||||
- Known unsupported configurations
|
||||
|
||||
### 2. Create Test Matrix
|
||||
|
||||
- Prioritize common configurations
|
||||
- Include edge cases
|
||||
- Balance coverage vs. effort
|
||||
|
||||
### 3. Execute Systematic Testing
|
||||
|
||||
- Full playthrough on key configs
|
||||
- Spot checks on edge cases
|
||||
- Automated smoke tests where possible
|
||||
|
||||
### 4. Document Issues
|
||||
|
||||
- Repro steps with exact configuration
|
||||
- Severity and frequency
|
||||
- Workarounds if available
|
||||
|
||||
## Common Compatibility Issues
|
||||
|
||||
### Graphics Issues
|
||||
|
||||
| Issue | Cause | Detection |
|
||||
| -------------------- | ---------------------- | -------------------------------- |
|
||||
| Crashes on launch | Driver incompatibility | Test on multiple GPUs |
|
||||
| Rendering artifacts | Shader issues | Visual inspection across configs |
|
||||
| Performance variance | Optimization gaps | Profile on multiple GPUs |
|
||||
| Resolution bugs | Aspect ratio handling | Test non-standard resolutions |
|
||||
|
||||
### Input Issues
|
||||
|
||||
| Issue | Cause | Detection |
|
||||
| ----------------------- | ------------------ | ------------------------------ |
|
||||
| Controller not detected | Missing driver/API | Test all supported controllers |
|
||||
| Wrong button prompts | Platform detection | Swap controllers mid-game |
|
||||
| Stick drift handling | Deadzone issues | Test worn controllers |
|
||||
| Mouse acceleration | Raw input issues | Test at different DPIs |
|
||||
|
||||
### Audio Issues
|
||||
|
||||
| Issue | Cause | Detection |
|
||||
| -------------- | ---------------- | --------------------------- |
|
||||
| No sound | Device selection | Test multiple audio devices |
|
||||
| Crackling | Buffer issues | Test under CPU load |
|
||||
| Wrong channels | Surround setup | Test stereo vs 5.1 vs 7.1 |
|
||||
|
||||
## Platform-Specific Considerations
|
||||
|
||||
### PC
|
||||
|
||||
- **Steam:** Verify Steam Input, Steamworks features
|
||||
- **Epic:** Test EOS features if used
|
||||
- **GOG:** Test offline/DRM-free functionality
|
||||
- **Game Pass:** Test Xbox services integration
|
||||
|
||||
### Console
|
||||
|
||||
- **Certification Requirements:** Study TRCs/XRs early
|
||||
- **SKU Differences:** Test on all variants (S vs X)
|
||||
- **External Storage:** Test on USB drives
|
||||
- **Quick Resume:** Test suspend/resume cycles
|
||||
|
||||
### Mobile
|
||||
|
||||
- **Device Fragmentation:** Test across screen sizes
|
||||
- **OS Versions:** Test min supported to latest
|
||||
- **Permissions:** Test permission flows
|
||||
- **App Lifecycle:** Test background/foreground
|
||||
|
||||
## Automated Compatibility Testing
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
```yaml
|
||||
# Run on matrix of configurations
|
||||
compatibility_test:
|
||||
matrix:
|
||||
os: [windows-10, windows-11, ubuntu-22]
|
||||
gpu: [nvidia, amd, intel]
|
||||
script:
|
||||
- launch_game --headless
|
||||
- verify_main_menu_reached
|
||||
- check_no_errors
|
||||
```
|
||||
|
||||
### Screenshot Comparison
|
||||
|
||||
- Capture screenshots on different GPUs
|
||||
- Compare for rendering differences
|
||||
- Flag significant deviations
|
||||
|
||||
### Cloud Testing Services
|
||||
|
||||
- AWS Device Farm
|
||||
- BrowserStack (web games)
|
||||
- LambdaTest
|
||||
- Sauce Labs
|
||||
|
||||
## Compatibility Checklist
|
||||
|
||||
### Pre-Alpha
|
||||
|
||||
- [ ] Minimum specs defined
|
||||
- [ ] Key platforms identified
|
||||
- [ ] Test matrix created
|
||||
- [ ] Test hardware acquired/rented
|
||||
|
||||
### Alpha
|
||||
|
||||
- [ ] Full playthrough on min spec
|
||||
- [ ] Controller support verified
|
||||
- [ ] Major graphics issues found
|
||||
- [ ] Platform SDK integrated
|
||||
|
||||
### Beta
|
||||
|
||||
- [ ] All matrix configurations tested
|
||||
- [ ] Edge cases explored
|
||||
- [ ] Certification pre-check done
|
||||
- [ ] Store page requirements met
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Final certification passed
|
||||
- [ ] Known issues documented
|
||||
- [ ] Workarounds communicated
|
||||
- [ ] Support matrix published
|
||||
|
||||
## Documenting Compatibility
|
||||
|
||||
### System Requirements
|
||||
|
||||
```
|
||||
MINIMUM:
|
||||
- OS: Windows 10 64-bit
|
||||
- Processor: Intel Core i5-6400 or AMD equivalent
|
||||
- Memory: 8 GB RAM
|
||||
- Graphics: NVIDIA GTX 1050 or AMD RX 560
|
||||
- Storage: 50 GB available space
|
||||
|
||||
RECOMMENDED:
|
||||
- OS: Windows 11 64-bit
|
||||
- Processor: Intel Core i7-10700 or AMD equivalent
|
||||
- Memory: 16 GB RAM
|
||||
- Graphics: NVIDIA RTX 3060 or AMD RX 6700 XT
|
||||
- Storage: 50 GB SSD
|
||||
```
|
||||
|
||||
### Known Issues
|
||||
|
||||
Maintain a public-facing list of known compatibility issues with:
|
||||
|
||||
- Affected configurations
|
||||
- Symptoms
|
||||
- Workarounds
|
||||
- Fix status
|
||||
@@ -0,0 +1,376 @@
|
||||
# Godot GUT Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
GUT (Godot Unit Test) is the standard unit testing framework for Godot. It provides a full-featured testing framework with assertions, mocking, and CI integration.
|
||||
|
||||
## Installation
|
||||
|
||||
### Via Asset Library
|
||||
|
||||
1. Open AssetLib in Godot
|
||||
2. Search for "GUT"
|
||||
3. Download and install
|
||||
4. Enable the plugin in Project Settings
|
||||
|
||||
### Via Git Submodule
|
||||
|
||||
```bash
|
||||
git submodule add https://github.com/bitwes/Gut.git addons/gut
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── addons/
|
||||
│ └── gut/
|
||||
├── src/
|
||||
│ ├── player/
|
||||
│ │ └── player.gd
|
||||
│ └── combat/
|
||||
│ └── damage_calculator.gd
|
||||
└── tests/
|
||||
├── unit/
|
||||
│ └── test_damage_calculator.gd
|
||||
└── integration/
|
||||
└── test_player_combat.gd
|
||||
```
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
### Simple Test Class
|
||||
|
||||
```gdscript
|
||||
# tests/unit/test_damage_calculator.gd
|
||||
extends GutTest
|
||||
|
||||
var calculator: DamageCalculator
|
||||
|
||||
func before_each():
|
||||
calculator = DamageCalculator.new()
|
||||
|
||||
func after_each():
|
||||
calculator.free()
|
||||
|
||||
func test_calculate_base_damage():
|
||||
var result = calculator.calculate(100.0, 1.0)
|
||||
assert_eq(result, 100.0, "Base damage should equal input")
|
||||
|
||||
func test_calculate_critical_hit():
|
||||
var result = calculator.calculate(100.0, 2.0)
|
||||
assert_eq(result, 200.0, "Critical hit should double damage")
|
||||
|
||||
func test_calculate_with_zero_multiplier():
|
||||
var result = calculator.calculate(100.0, 0.0)
|
||||
assert_eq(result, 0.0, "Zero multiplier should result in zero damage")
|
||||
```
|
||||
|
||||
### Parameterized Tests
|
||||
|
||||
```gdscript
|
||||
func test_damage_scenarios():
|
||||
var scenarios = [
|
||||
{"base": 100.0, "mult": 1.0, "expected": 100.0},
|
||||
{"base": 100.0, "mult": 2.0, "expected": 200.0},
|
||||
{"base": 50.0, "mult": 1.5, "expected": 75.0},
|
||||
{"base": 0.0, "mult": 2.0, "expected": 0.0},
|
||||
]
|
||||
|
||||
for scenario in scenarios:
|
||||
var result = calculator.calculate(scenario.base, scenario.mult)
|
||||
assert_eq(
|
||||
result,
|
||||
scenario.expected,
|
||||
"Base %s * %s should equal %s" % [
|
||||
scenario.base, scenario.mult, scenario.expected
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Testing Nodes
|
||||
|
||||
### Scene Testing
|
||||
|
||||
```gdscript
|
||||
# tests/integration/test_player.gd
|
||||
extends GutTest
|
||||
|
||||
var player: Player
|
||||
var player_scene = preload("res://src/player/player.tscn")
|
||||
|
||||
func before_each():
|
||||
player = player_scene.instantiate()
|
||||
add_child(player)
|
||||
|
||||
func after_each():
|
||||
player.queue_free()
|
||||
|
||||
func test_player_initial_health():
|
||||
assert_eq(player.health, 100, "Player should start with 100 health")
|
||||
|
||||
func test_player_takes_damage():
|
||||
player.take_damage(30)
|
||||
assert_eq(player.health, 70, "Health should be reduced by damage")
|
||||
|
||||
func test_player_dies_at_zero_health():
|
||||
player.take_damage(100)
|
||||
assert_true(player.is_dead, "Player should be dead at 0 health")
|
||||
```
|
||||
|
||||
### Testing with Signals
|
||||
|
||||
```gdscript
|
||||
func test_damage_emits_signal():
|
||||
watch_signals(player)
|
||||
|
||||
player.take_damage(10)
|
||||
|
||||
assert_signal_emitted(player, "health_changed")
|
||||
assert_signal_emit_count(player, "health_changed", 1)
|
||||
|
||||
func test_death_emits_signal():
|
||||
watch_signals(player)
|
||||
|
||||
player.take_damage(100)
|
||||
|
||||
assert_signal_emitted(player, "died")
|
||||
```
|
||||
|
||||
### Testing with Await
|
||||
|
||||
```gdscript
|
||||
func test_attack_cooldown():
|
||||
player.attack()
|
||||
assert_true(player.is_attacking)
|
||||
|
||||
# Wait for cooldown
|
||||
await get_tree().create_timer(player.attack_cooldown).timeout
|
||||
|
||||
assert_false(player.is_attacking)
|
||||
assert_true(player.can_attack)
|
||||
```
|
||||
|
||||
## Mocking and Doubles
|
||||
|
||||
### Creating Doubles
|
||||
|
||||
```gdscript
|
||||
func test_enemy_uses_pathfinding():
|
||||
var mock_pathfinding = double(Pathfinding).new()
|
||||
stub(mock_pathfinding, "find_path").to_return([Vector2(0, 0), Vector2(10, 10)])
|
||||
|
||||
var enemy = Enemy.new()
|
||||
enemy.pathfinding = mock_pathfinding
|
||||
|
||||
enemy.move_to(Vector2(10, 10))
|
||||
|
||||
assert_called(mock_pathfinding, "find_path")
|
||||
```
|
||||
|
||||
### Partial Doubles
|
||||
|
||||
```gdscript
|
||||
func test_player_inventory():
|
||||
var player_double = partial_double(Player).new()
|
||||
stub(player_double, "save_to_disk").to_do_nothing()
|
||||
|
||||
player_double.add_item("sword")
|
||||
|
||||
assert_eq(player_double.inventory.size(), 1)
|
||||
assert_called(player_double, "save_to_disk")
|
||||
```
|
||||
|
||||
## Physics Testing
|
||||
|
||||
### Testing Collision
|
||||
|
||||
```gdscript
|
||||
func test_projectile_hits_enemy():
|
||||
var projectile = Projectile.new()
|
||||
var enemy = Enemy.new()
|
||||
|
||||
add_child(projectile)
|
||||
add_child(enemy)
|
||||
|
||||
projectile.global_position = Vector2(0, 0)
|
||||
enemy.global_position = Vector2(100, 0)
|
||||
|
||||
projectile.velocity = Vector2(200, 0)
|
||||
|
||||
# Simulate physics frames
|
||||
for i in range(60):
|
||||
await get_tree().physics_frame
|
||||
|
||||
assert_true(enemy.was_hit, "Enemy should be hit by projectile")
|
||||
|
||||
projectile.queue_free()
|
||||
enemy.queue_free()
|
||||
```
|
||||
|
||||
### Testing Area2D
|
||||
|
||||
```gdscript
|
||||
func test_pickup_collected():
|
||||
var pickup = Pickup.new()
|
||||
var player = player_scene.instantiate()
|
||||
|
||||
add_child(pickup)
|
||||
add_child(player)
|
||||
|
||||
pickup.global_position = Vector2(50, 50)
|
||||
player.global_position = Vector2(50, 50)
|
||||
|
||||
# Wait for physics to process overlap
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
|
||||
assert_true(pickup.is_queued_for_deletion(), "Pickup should be collected")
|
||||
|
||||
player.queue_free()
|
||||
```
|
||||
|
||||
## Input Testing
|
||||
|
||||
### Simulating Input
|
||||
|
||||
```gdscript
|
||||
func test_jump_on_input():
|
||||
var input_event = InputEventKey.new()
|
||||
input_event.keycode = KEY_SPACE
|
||||
input_event.pressed = true
|
||||
|
||||
Input.parse_input_event(input_event)
|
||||
await get_tree().process_frame
|
||||
|
||||
player._unhandled_input(input_event)
|
||||
|
||||
assert_true(player.is_jumping, "Player should jump on space press")
|
||||
```
|
||||
|
||||
### Testing Input Actions
|
||||
|
||||
```gdscript
|
||||
func test_attack_action():
|
||||
# Simulate action press
|
||||
Input.action_press("attack")
|
||||
await get_tree().process_frame
|
||||
|
||||
player._process(0.016)
|
||||
|
||||
assert_true(player.is_attacking)
|
||||
|
||||
Input.action_release("attack")
|
||||
```
|
||||
|
||||
## Resource Testing
|
||||
|
||||
### Testing Custom Resources
|
||||
|
||||
```gdscript
|
||||
func test_weapon_stats_resource():
|
||||
var weapon = WeaponStats.new()
|
||||
weapon.base_damage = 10.0
|
||||
weapon.attack_speed = 2.0
|
||||
|
||||
assert_eq(weapon.dps, 20.0, "DPS should be damage * speed")
|
||||
|
||||
func test_save_load_resource():
|
||||
var original = PlayerData.new()
|
||||
original.level = 5
|
||||
original.gold = 1000
|
||||
|
||||
ResourceSaver.save(original, "user://test_save.tres")
|
||||
var loaded = ResourceLoader.load("user://test_save.tres")
|
||||
|
||||
assert_eq(loaded.level, 5)
|
||||
assert_eq(loaded.gold, 1000)
|
||||
|
||||
DirAccess.remove_absolute("user://test_save.tres")
|
||||
```
|
||||
|
||||
## GUT Configuration
|
||||
|
||||
### gut_config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"dirs": ["res://tests/"],
|
||||
"include_subdirs": true,
|
||||
"prefix": "test_",
|
||||
"suffix": ".gd",
|
||||
"should_exit": true,
|
||||
"should_exit_on_success": true,
|
||||
"log_level": 1,
|
||||
"junit_xml_file": "results.xml",
|
||||
"font_size": 16
|
||||
}
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
### Command Line Execution
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
godot --headless -s addons/gut/gut_cmdln.gd
|
||||
|
||||
# Run specific tests
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gdir=res://tests/unit \
|
||||
-gprefix=test_
|
||||
|
||||
# With JUnit output
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gjunit_xml_file=results.xml
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: barichello/godot-ci:4.2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gjunit_xml_file=results.xml
|
||||
|
||||
- name: Publish Results
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: results.xml
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use `before_each`/`after_each` for setup/teardown
|
||||
- Free nodes after tests to prevent leaks
|
||||
- Use meaningful assertion messages
|
||||
- Group related tests in the same file
|
||||
- Use `watch_signals` for signal testing
|
||||
- Await physics frames when testing physics
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't test Godot's built-in functionality
|
||||
- Don't rely on execution order between test files
|
||||
- Don't leave orphan nodes
|
||||
- Don't use `yield` (use `await` in Godot 4)
|
||||
- Don't test private methods directly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
| -------------------- | ------------------ | ------------------------------------ |
|
||||
| Tests not found | Wrong prefix/path | Check gut_config.json |
|
||||
| Orphan nodes warning | Missing cleanup | Add `queue_free()` in `after_each` |
|
||||
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
||||
| Physics not working | Missing frames | Await `physics_frame` |
|
||||
| Flaky tests | Timing issues | Use proper await/signals |
|
||||
@@ -0,0 +1,315 @@
|
||||
# Input Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Input testing validates that all supported input devices work correctly across platforms. Poor input handling frustrates players instantly—responsive, accurate input is foundational to game feel.
|
||||
|
||||
## Input Categories
|
||||
|
||||
### Device Types
|
||||
|
||||
| Device | Platforms | Key Concerns |
|
||||
| ----------------- | -------------- | ----------------------------------- |
|
||||
| Keyboard + Mouse | PC | Key conflicts, DPI sensitivity |
|
||||
| Gamepad (Xbox/PS) | PC, Console | Deadzone, vibration, button prompts |
|
||||
| Touch | Mobile, Switch | Multi-touch, gesture recognition |
|
||||
| Motion Controls | Switch, VR | Calibration, drift, fatigue |
|
||||
| Specialty | Various | Flight sticks, wheels, fight sticks |
|
||||
|
||||
### Input Characteristics
|
||||
|
||||
| Characteristic | Description | Test Focus |
|
||||
| -------------- | ---------------------------- | -------------------------------- |
|
||||
| Responsiveness | Input-to-action delay | Should feel instant (< 100ms) |
|
||||
| Accuracy | Input maps to correct action | No ghost inputs or missed inputs |
|
||||
| Consistency | Same input = same result | Deterministic behavior |
|
||||
| Accessibility | Alternative input support | Remapping, assist options |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Keyboard and Mouse
|
||||
|
||||
```
|
||||
SCENARIO: All Keybinds Functional
|
||||
GIVEN default keyboard bindings
|
||||
WHEN each bound key is pressed
|
||||
THEN corresponding action triggers
|
||||
AND no key conflicts exist
|
||||
|
||||
SCENARIO: Key Remapping
|
||||
GIVEN player remaps "Jump" from Space to F
|
||||
WHEN F is pressed
|
||||
THEN jump action triggers
|
||||
AND Space no longer triggers jump
|
||||
AND remapping persists after restart
|
||||
|
||||
SCENARIO: Mouse Sensitivity
|
||||
GIVEN sensitivity set to 5 (mid-range)
|
||||
WHEN mouse moves 10cm
|
||||
THEN camera rotation matches expected degrees
|
||||
AND movement feels consistent at different frame rates
|
||||
|
||||
SCENARIO: Mouse Button Support
|
||||
GIVEN mouse with 5+ buttons
|
||||
WHEN side buttons are pressed
|
||||
THEN they can be bound to actions
|
||||
AND they function correctly in gameplay
|
||||
```
|
||||
|
||||
### Gamepad
|
||||
|
||||
```
|
||||
SCENARIO: Analog Stick Deadzone
|
||||
GIVEN controller with slight stick drift
|
||||
WHEN stick is in neutral position
|
||||
THEN no movement occurs (deadzone filters drift)
|
||||
AND intentional small movements still register
|
||||
|
||||
SCENARIO: Trigger Pressure
|
||||
GIVEN analog triggers
|
||||
WHEN trigger is partially pressed
|
||||
THEN partial values are read (e.g., 0.5 for half-press)
|
||||
AND full press reaches 1.0
|
||||
|
||||
SCENARIO: Controller Hot-Swap
|
||||
GIVEN game running with keyboard
|
||||
WHEN gamepad is connected
|
||||
THEN input prompts switch to gamepad icons
|
||||
AND gamepad input works immediately
|
||||
AND keyboard still works if used
|
||||
|
||||
SCENARIO: Vibration Feedback
|
||||
GIVEN rumble-enabled controller
|
||||
WHEN damage is taken
|
||||
THEN controller vibrates appropriately
|
||||
AND vibration intensity matches damage severity
|
||||
```
|
||||
|
||||
### Touch Input
|
||||
|
||||
```
|
||||
SCENARIO: Multi-Touch Accuracy
|
||||
GIVEN virtual joystick and buttons
|
||||
WHEN left thumb on joystick AND right thumb on button
|
||||
THEN both inputs register simultaneously
|
||||
AND no interference between touch points
|
||||
|
||||
SCENARIO: Gesture Recognition
|
||||
GIVEN swipe-to-attack mechanic
|
||||
WHEN player swipes right
|
||||
THEN attack direction matches swipe
|
||||
AND swipe is distinguished from tap
|
||||
|
||||
SCENARIO: Touch Target Size
|
||||
GIVEN minimum touch target of 44x44 points
|
||||
WHEN buttons are placed
|
||||
THEN all interactive elements meet minimum size
|
||||
AND elements have adequate spacing
|
||||
```
|
||||
|
||||
## Platform-Specific Testing
|
||||
|
||||
### PC
|
||||
|
||||
- Multiple keyboard layouts (QWERTY, AZERTY, QWERTZ)
|
||||
- Different mouse DPI settings (400-3200+)
|
||||
- Multiple monitors (cursor confinement)
|
||||
- Background application conflicts
|
||||
- Steam Input API integration
|
||||
|
||||
### Console
|
||||
|
||||
| Platform | Specific Tests |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| PlayStation | Touchpad, adaptive triggers, haptics |
|
||||
| Xbox | Impulse triggers, Elite controller paddles |
|
||||
| Switch | Joy-Con detachment, gyro, HD rumble |
|
||||
|
||||
### Mobile
|
||||
|
||||
- Different screen sizes and aspect ratios
|
||||
- Notch/cutout avoidance
|
||||
- External controller support
|
||||
- Apple MFi / Android gamepad compatibility
|
||||
|
||||
## Automated Test Examples
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Movement_WithGamepad_RespondsToStick()
|
||||
{
|
||||
var gamepad = InputSystem.AddDevice<Gamepad>();
|
||||
|
||||
yield return null;
|
||||
|
||||
// Simulate stick input
|
||||
Set(gamepad.leftStick, new Vector2(1, 0));
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
Assert.Greater(player.transform.position.x, 0f,
|
||||
"Player should move right");
|
||||
|
||||
InputSystem.RemoveDevice(gamepad);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator InputLatency_UnderLoad_StaysAcceptable()
|
||||
{
|
||||
float inputTime = Time.realtimeSinceStartup;
|
||||
bool actionTriggered = false;
|
||||
|
||||
player.OnJump += () => {
|
||||
float latency = (Time.realtimeSinceStartup - inputTime) * 1000;
|
||||
Assert.Less(latency, 100f, "Input latency should be under 100ms");
|
||||
actionTriggered = true;
|
||||
};
|
||||
|
||||
var keyboard = InputSystem.AddDevice<Keyboard>();
|
||||
Press(keyboard.spaceKey);
|
||||
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
|
||||
Assert.IsTrue(actionTriggered, "Jump should have triggered");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deadzone_FiltersSmallInputs()
|
||||
{
|
||||
var settings = new InputSettings { stickDeadzone = 0.2f };
|
||||
|
||||
// Input below deadzone
|
||||
var filtered = InputProcessor.ApplyDeadzone(new Vector2(0.1f, 0.1f), settings);
|
||||
Assert.AreEqual(Vector2.zero, filtered);
|
||||
|
||||
// Input above deadzone
|
||||
filtered = InputProcessor.ApplyDeadzone(new Vector2(0.5f, 0.5f), settings);
|
||||
Assert.AreNotEqual(Vector2.zero, filtered);
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal
|
||||
|
||||
```cpp
|
||||
bool FInputTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Test gamepad input mapping
|
||||
APlayerController* PC = GetWorld()->GetFirstPlayerController();
|
||||
|
||||
// Simulate gamepad stick input
|
||||
FInputKeyParams Params;
|
||||
Params.Key = EKeys::Gamepad_LeftX;
|
||||
Params.Delta = FVector(1.0f, 0, 0);
|
||||
PC->InputKey(Params);
|
||||
|
||||
// Verify movement
|
||||
APawn* Pawn = PC->GetPawn();
|
||||
FVector Velocity = Pawn->GetVelocity();
|
||||
|
||||
TestTrue("Pawn should be moving", Velocity.SizeSquared() > 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
func test_input_action_mapping():
|
||||
# Verify action exists
|
||||
assert_true(InputMap.has_action("jump"))
|
||||
|
||||
# Simulate input
|
||||
var event = InputEventKey.new()
|
||||
event.keycode = KEY_SPACE
|
||||
event.pressed = true
|
||||
|
||||
Input.parse_input_event(event)
|
||||
await get_tree().process_frame
|
||||
|
||||
assert_true(Input.is_action_just_pressed("jump"))
|
||||
|
||||
func test_gamepad_deadzone():
|
||||
var input = Vector2(0.15, 0.1)
|
||||
var deadzone = 0.2
|
||||
|
||||
var processed = input_processor.apply_deadzone(input, deadzone)
|
||||
|
||||
assert_eq(processed, Vector2.ZERO, "Small input should be filtered")
|
||||
|
||||
func test_controller_hotswap():
|
||||
# Simulate controller connect
|
||||
Input.joy_connection_changed(0, true)
|
||||
await get_tree().process_frame
|
||||
|
||||
var prompt_icon = ui.get_action_prompt("jump")
|
||||
|
||||
assert_true(prompt_icon.texture.resource_path.contains("gamepad"),
|
||||
"Should show gamepad prompts after controller connect")
|
||||
```
|
||||
|
||||
## Accessibility Testing
|
||||
|
||||
### Requirements Checklist
|
||||
|
||||
- [ ] Full keyboard navigation (no mouse required)
|
||||
- [ ] Remappable controls for all actions
|
||||
- [ ] Button hold alternatives to rapid press
|
||||
- [ ] Toggle options for hold actions
|
||||
- [ ] One-handed control schemes
|
||||
- [ ] Colorblind-friendly UI indicators
|
||||
- [ ] Screen reader support for menus
|
||||
|
||||
### Accessibility Test Scenarios
|
||||
|
||||
```
|
||||
SCENARIO: Keyboard-Only Navigation
|
||||
GIVEN mouse is disconnected
|
||||
WHEN navigating through all menus
|
||||
THEN all menu items are reachable via keyboard
|
||||
AND focus indicators are clearly visible
|
||||
|
||||
SCENARIO: Button Hold Toggle
|
||||
GIVEN "sprint requires hold" is toggled OFF
|
||||
WHEN sprint button is tapped once
|
||||
THEN sprint activates
|
||||
AND sprint stays active until tapped again
|
||||
|
||||
SCENARIO: Reduced Button Mashing
|
||||
GIVEN QTE assist mode enabled
|
||||
WHEN QTE sequence appears
|
||||
THEN single press advances sequence
|
||||
AND no rapid input required
|
||||
```
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
| Metric | Target | Maximum Acceptable |
|
||||
| ----------------------- | --------------- | ------------------ |
|
||||
| Input-to-render latency | < 50ms | 100ms |
|
||||
| Polling rate match | 1:1 with device | No input loss |
|
||||
| Deadzone processing | < 1ms | 5ms |
|
||||
| Rebind save/load | < 100ms | 500ms |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Test with actual hardware, not just simulated input
|
||||
- Support simultaneous keyboard + gamepad
|
||||
- Provide sensible default deadzones
|
||||
- Show device-appropriate button prompts
|
||||
- Allow complete control remapping
|
||||
- Test at different frame rates
|
||||
|
||||
### DON'T
|
||||
|
||||
- Assume controller layout (Xbox vs PlayStation)
|
||||
- Hard-code input mappings
|
||||
- Ignore analog input precision
|
||||
- Skip accessibility considerations
|
||||
- Forget about input during loading/cutscenes
|
||||
- Neglect testing with worn/drifting controllers
|
||||
@@ -0,0 +1,304 @@
|
||||
# Localization Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Localization testing ensures games work correctly across languages, regions, and cultures. Beyond translation, it validates text display, cultural appropriateness, and regional compliance.
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Linguistic Testing
|
||||
|
||||
| Category | Focus | Examples |
|
||||
| -------------------- | ----------------------- | ------------------------------ |
|
||||
| Translation accuracy | Meaning preserved | Idioms, game terminology |
|
||||
| Grammar/spelling | Language correctness | Verb tense, punctuation |
|
||||
| Consistency | Same terms throughout | "Health" vs "HP" vs "Life" |
|
||||
| Context | Meaning in game context | Item names, skill descriptions |
|
||||
|
||||
### Functional Testing
|
||||
|
||||
| Category | Focus | Examples |
|
||||
| -------------- | ----------------------- | --------------------------- |
|
||||
| Text display | Fits in UI | Button labels, dialog boxes |
|
||||
| Font support | Characters render | CJK, Cyrillic, Arabic |
|
||||
| Text expansion | Longer translations | German is ~30% longer |
|
||||
| RTL support | Right-to-left languages | Arabic, Hebrew layouts |
|
||||
|
||||
### Cultural Testing
|
||||
|
||||
| Category | Focus | Examples |
|
||||
| -------------------- | ------------------ | ------------------------- |
|
||||
| Cultural sensitivity | Offensive content | Gestures, symbols, colors |
|
||||
| Regional compliance | Legal requirements | Ratings, gambling laws |
|
||||
| Date/time formats | Local conventions | DD/MM/YYYY vs MM/DD/YYYY |
|
||||
| Number formats | Decimal separators | 1,000.00 vs 1.000,00 |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Text Display
|
||||
|
||||
```
|
||||
SCENARIO: Text Fits UI Elements
|
||||
GIVEN all localized strings
|
||||
WHEN displayed in target language
|
||||
THEN text fits within UI boundaries
|
||||
AND no truncation or overflow occurs
|
||||
AND text remains readable
|
||||
|
||||
SCENARIO: Dynamic Text Insertion
|
||||
GIVEN template "Player {name} scored {points} points"
|
||||
WHEN name="Alexander" and points=1000
|
||||
THEN German: "Spieler Alexander hat 1.000 Punkte erzielt"
|
||||
AND text fits UI element
|
||||
AND variables are correctly formatted for locale
|
||||
|
||||
SCENARIO: Plural Forms
|
||||
GIVEN English "1 coin" / "5 coins"
|
||||
WHEN displaying in Polish (4 plural forms)
|
||||
THEN correct plural form is used
|
||||
AND all plural forms are translated
|
||||
```
|
||||
|
||||
### Character Support
|
||||
|
||||
```
|
||||
SCENARIO: CJK Character Rendering
|
||||
GIVEN Japanese localization
|
||||
WHEN displaying text with kanji/hiragana/katakana
|
||||
THEN all characters render correctly
|
||||
AND no missing glyphs (tofu boxes)
|
||||
AND line breaks respect CJK rules
|
||||
|
||||
SCENARIO: Special Characters
|
||||
GIVEN text with accented characters (é, ñ, ü)
|
||||
WHEN displayed in-game
|
||||
THEN all characters render correctly
|
||||
AND sorting works correctly
|
||||
|
||||
SCENARIO: User-Generated Content
|
||||
GIVEN player can name character
|
||||
WHEN name includes non-Latin characters
|
||||
THEN name displays correctly
|
||||
AND name saves/loads correctly
|
||||
AND name appears correctly to other players
|
||||
```
|
||||
|
||||
### Layout and Direction
|
||||
|
||||
```
|
||||
SCENARIO: Right-to-Left Layout
|
||||
GIVEN Arabic localization
|
||||
WHEN viewing UI
|
||||
THEN text reads right-to-left
|
||||
AND UI elements mirror appropriately
|
||||
AND numbers remain left-to-right
|
||||
AND mixed content (Arabic + English) displays correctly
|
||||
|
||||
SCENARIO: Text Expansion Accommodation
|
||||
GIVEN English UI "OK" / "Cancel" buttons
|
||||
WHEN localized to German "OK" / "Abbrechen"
|
||||
THEN button expands or text size adjusts
|
||||
AND button remains clickable
|
||||
AND layout doesn't break
|
||||
```
|
||||
|
||||
## Locale-Specific Formatting
|
||||
|
||||
### Date and Time
|
||||
|
||||
| Locale | Date Format | Time Format |
|
||||
| ------ | -------------- | ----------- |
|
||||
| en-US | 12/25/2024 | 3:30 PM |
|
||||
| en-GB | 25/12/2024 | 15:30 |
|
||||
| de-DE | 25.12.2024 | 15:30 Uhr |
|
||||
| ja-JP | 2024年12月25日 | 15時30分 |
|
||||
|
||||
### Numbers and Currency
|
||||
|
||||
| Locale | Number | Currency |
|
||||
| ------ | -------- | ---------- |
|
||||
| en-US | 1,234.56 | $1,234.56 |
|
||||
| de-DE | 1.234,56 | 1.234,56 € |
|
||||
| fr-FR | 1 234,56 | 1 234,56 € |
|
||||
| ja-JP | 1,234.56 | ¥1,235 |
|
||||
|
||||
## Automated Test Examples
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
using UnityEngine.Localization;
|
||||
|
||||
[Test]
|
||||
public void Localization_AllKeysHaveTranslations([Values("en", "de", "ja", "zh-CN")] string locale)
|
||||
{
|
||||
var stringTable = LocalizationSettings.StringDatabase
|
||||
.GetTable("GameStrings", new Locale(locale));
|
||||
|
||||
foreach (var entry in stringTable)
|
||||
{
|
||||
Assert.IsFalse(string.IsNullOrEmpty(entry.Value.LocalizedValue),
|
||||
$"Missing translation for '{entry.Key}' in {locale}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TextFits_AllUIElements()
|
||||
{
|
||||
var languages = new[] { "en", "de", "fr", "ja" };
|
||||
|
||||
foreach (var lang in languages)
|
||||
{
|
||||
LocalizationSettings.SelectedLocale = new Locale(lang);
|
||||
|
||||
foreach (var textElement in FindObjectsOfType<LocalizedText>())
|
||||
{
|
||||
var rectTransform = textElement.GetComponent<RectTransform>();
|
||||
var textComponent = textElement.GetComponent<Text>();
|
||||
|
||||
Assert.LessOrEqual(
|
||||
textComponent.preferredWidth,
|
||||
rectTransform.rect.width,
|
||||
$"Text overflows in {lang}: {textElement.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("en", 1, "1 coin")]
|
||||
[TestCase("en", 5, "5 coins")]
|
||||
[TestCase("ru", 1, "1 монета")]
|
||||
[TestCase("ru", 2, "2 монеты")]
|
||||
[TestCase("ru", 5, "5 монет")]
|
||||
public void Pluralization_ReturnsCorrectForm(string locale, int count, string expected)
|
||||
{
|
||||
var result = Localization.GetPlural("coin", count, locale);
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal
|
||||
|
||||
```cpp
|
||||
bool FLocalizationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TArray<FString> Cultures = {"en", "de", "ja", "ko"};
|
||||
|
||||
for (const FString& Culture : Cultures)
|
||||
{
|
||||
FInternationalization::Get().SetCurrentCulture(Culture);
|
||||
|
||||
// Test critical strings exist
|
||||
FText LocalizedText = NSLOCTEXT("Game", "StartButton", "Start");
|
||||
TestFalse(
|
||||
FString::Printf(TEXT("Missing StartButton in %s"), *Culture),
|
||||
LocalizedText.IsEmpty());
|
||||
|
||||
// Test number formatting
|
||||
FText NumberText = FText::AsNumber(1234567);
|
||||
TestTrue(
|
||||
TEXT("Number should be formatted"),
|
||||
NumberText.ToString().Len() > 7); // Has separators
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
func test_all_translations_complete():
|
||||
var locales = ["en", "de", "ja", "es"]
|
||||
var keys = TranslationServer.get_all_keys()
|
||||
|
||||
for locale in locales:
|
||||
TranslationServer.set_locale(locale)
|
||||
for key in keys:
|
||||
var translated = tr(key)
|
||||
assert_ne(translated, key,
|
||||
"Missing translation for '%s' in %s" % [key, locale])
|
||||
|
||||
func test_plural_forms():
|
||||
TranslationServer.set_locale("ru")
|
||||
|
||||
assert_eq(tr_n("coin", "coins", 1), "1 монета")
|
||||
assert_eq(tr_n("coin", "coins", 2), "2 монеты")
|
||||
assert_eq(tr_n("coin", "coins", 5), "5 монет")
|
||||
assert_eq(tr_n("coin", "coins", 21), "21 монета")
|
||||
|
||||
func test_text_fits_buttons():
|
||||
var locales = ["en", "de", "fr"]
|
||||
|
||||
for locale in locales:
|
||||
TranslationServer.set_locale(locale)
|
||||
await get_tree().process_frame # Allow UI update
|
||||
|
||||
for button in get_tree().get_nodes_in_group("localized_buttons"):
|
||||
var label = button.get_node("Label")
|
||||
assert_lt(label.size.x, button.size.x,
|
||||
"Button text overflows in %s: %s" % [locale, button.name])
|
||||
```
|
||||
|
||||
## Visual Verification Checklist
|
||||
|
||||
### Text Display
|
||||
|
||||
- [ ] No truncation in any language
|
||||
- [ ] Consistent font sizing
|
||||
- [ ] Proper line breaks
|
||||
- [ ] No overlapping text
|
||||
|
||||
### UI Layout
|
||||
|
||||
- [ ] Buttons accommodate longer text
|
||||
- [ ] Dialog boxes resize appropriately
|
||||
- [ ] Menu items align correctly
|
||||
- [ ] Scrollbars appear when needed
|
||||
|
||||
### Cultural Elements
|
||||
|
||||
- [ ] Icons are culturally appropriate
|
||||
- [ ] Colors don't have negative connotations
|
||||
- [ ] Gestures are region-appropriate
|
||||
- [ ] No unintended political references
|
||||
|
||||
## Regional Compliance
|
||||
|
||||
### Ratings Requirements
|
||||
|
||||
| Region | Rating Board | Special Requirements |
|
||||
| ------------- | ------------ | ------------------------- |
|
||||
| North America | ESRB | Content descriptors |
|
||||
| Europe | PEGI | Age-appropriate icons |
|
||||
| Japan | CERO | Strict content guidelines |
|
||||
| Germany | USK | Violence restrictions |
|
||||
| China | GRAC | Approval process |
|
||||
|
||||
### Common Regional Issues
|
||||
|
||||
| Issue | Regions Affected | Solution |
|
||||
| ---------------- | ---------------- | ------------------------ |
|
||||
| Blood color | Japan, Germany | Option for green/disable |
|
||||
| Gambling imagery | Many regions | Remove or modify |
|
||||
| Skulls/bones | China | Alternative designs |
|
||||
| Nazi imagery | Germany | Remove entirely |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Test with native speakers
|
||||
- Plan for text expansion (reserve 30% extra space)
|
||||
- Use placeholder text during development (Lorem ipsum-style)
|
||||
- Support multiple input methods (IME for CJK)
|
||||
- Test all language combinations (UI language + audio language)
|
||||
- Validate string format parameters
|
||||
|
||||
### DON'T
|
||||
|
||||
- Hard-code strings in source code
|
||||
- Assume left-to-right layout
|
||||
- Concatenate translated strings
|
||||
- Use machine translation without review
|
||||
- Forget about date/time/number formatting
|
||||
- Ignore cultural context of images and icons
|
||||
@@ -0,0 +1,322 @@
|
||||
# Multiplayer Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Multiplayer testing validates network code, synchronization, and the player experience under real-world conditions. Network bugs are notoriously hard to reproduce—systematic testing is essential.
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Synchronization Testing
|
||||
|
||||
| Test Type | Description | Priority |
|
||||
| ------------------- | ---------------------------------------- | -------- |
|
||||
| State sync | All clients see consistent game state | P0 |
|
||||
| Position sync | Character positions match across clients | P0 |
|
||||
| Event ordering | Actions occur in correct sequence | P0 |
|
||||
| Conflict resolution | Simultaneous actions handled correctly | P1 |
|
||||
| Late join | New players sync correctly mid-game | P1 |
|
||||
|
||||
### Network Conditions
|
||||
|
||||
| Condition | Simulation Method | Test Focus |
|
||||
| --------------- | ----------------- | ------------------------ |
|
||||
| High latency | 200-500ms delay | Input responsiveness |
|
||||
| Packet loss | 5-20% drop rate | State recovery |
|
||||
| Jitter | Variable delay | Interpolation smoothness |
|
||||
| Bandwidth limit | Throttle to 1Mbps | Data prioritization |
|
||||
| Disconnection | Kill connection | Reconnection handling |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Basic Multiplayer
|
||||
|
||||
```
|
||||
SCENARIO: Player Join/Leave
|
||||
GIVEN host has started multiplayer session
|
||||
WHEN Player 2 joins
|
||||
THEN Player 2 appears in host's game
|
||||
AND Player 1 appears in Player 2's game
|
||||
AND player counts sync across all clients
|
||||
|
||||
SCENARIO: State Synchronization
|
||||
GIVEN 4 players in match
|
||||
WHEN Player 1 picks up item at position (10, 5)
|
||||
THEN item disappears for all players
|
||||
AND Player 1's inventory updates for all players
|
||||
AND no duplicate pickups possible
|
||||
|
||||
SCENARIO: Combat Synchronization
|
||||
GIVEN Player 1 attacks Player 2
|
||||
WHEN attack hits
|
||||
THEN damage is consistent on all clients
|
||||
AND hit effects play for all players
|
||||
AND health updates sync within 100ms
|
||||
```
|
||||
|
||||
### Network Degradation
|
||||
|
||||
```
|
||||
SCENARIO: High Latency Gameplay
|
||||
GIVEN 200ms latency between players
|
||||
WHEN Player 1 moves forward
|
||||
THEN movement is smooth on Player 1's screen
|
||||
AND other players see interpolated movement
|
||||
AND position converges within 500ms
|
||||
|
||||
SCENARIO: Packet Loss Recovery
|
||||
GIVEN 10% packet loss
|
||||
WHEN important game event occurs (goal, kill, etc.)
|
||||
THEN event is eventually delivered
|
||||
AND game state remains consistent
|
||||
AND no duplicate events processed
|
||||
|
||||
SCENARIO: Player Disconnection
|
||||
GIVEN Player 2 disconnects unexpectedly
|
||||
WHEN 5 seconds pass
|
||||
THEN other players are notified
|
||||
AND Player 2's character handles gracefully (despawn/AI takeover)
|
||||
AND game continues without crash
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
```
|
||||
SCENARIO: Simultaneous Actions
|
||||
GIVEN Player 1 and Player 2 grab same item simultaneously
|
||||
WHEN both inputs arrive at server
|
||||
THEN only one player receives item
|
||||
AND other player sees consistent state
|
||||
AND no item duplication
|
||||
|
||||
SCENARIO: Host Migration
|
||||
GIVEN host disconnects
|
||||
WHEN migration begins
|
||||
THEN new host is selected
|
||||
AND game state transfers correctly
|
||||
AND gameplay resumes within 10 seconds
|
||||
|
||||
SCENARIO: Reconnection
|
||||
GIVEN Player 2 disconnects temporarily
|
||||
WHEN Player 2 reconnects within 60 seconds
|
||||
THEN Player 2 rejoins same session
|
||||
AND state is synchronized
|
||||
AND progress is preserved
|
||||
```
|
||||
|
||||
## Network Simulation Tools
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
// Using Unity Transport with Network Simulator
|
||||
using Unity.Netcode;
|
||||
|
||||
public class NetworkSimulator : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int latencyMs = 100;
|
||||
[SerializeField] private float packetLossPercent = 5f;
|
||||
[SerializeField] private int jitterMs = 20;
|
||||
|
||||
void Start()
|
||||
{
|
||||
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
|
||||
var simulator = transport.GetSimulatorParameters();
|
||||
|
||||
simulator.PacketDelayMS = latencyMs;
|
||||
simulator.PacketDropRate = (int)(packetLossPercent * 100);
|
||||
simulator.PacketJitterMS = jitterMs;
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
[UnityTest]
|
||||
public IEnumerator Position_UnderLatency_ConvergesWithinThreshold()
|
||||
{
|
||||
EnableNetworkSimulation(latencyMs: 200);
|
||||
|
||||
// Move player
|
||||
player1.Move(Vector3.forward * 10);
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Check other client's view
|
||||
var player1OnClient2 = client2.GetPlayerPosition(player1.Id);
|
||||
var actualPosition = player1.transform.position;
|
||||
|
||||
Assert.Less(Vector3.Distance(player1OnClient2, actualPosition), 0.5f);
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal
|
||||
|
||||
```cpp
|
||||
// Using Network Emulation
|
||||
void UNetworkTestHelper::EnableLatencySimulation(int32 LatencyMs)
|
||||
{
|
||||
if (UNetDriver* NetDriver = GetWorld()->GetNetDriver())
|
||||
{
|
||||
FPacketSimulationSettings Settings;
|
||||
Settings.PktLag = LatencyMs;
|
||||
Settings.PktLagVariance = LatencyMs / 10;
|
||||
Settings.PktLoss = 0;
|
||||
|
||||
NetDriver->SetPacketSimulationSettings(Settings);
|
||||
}
|
||||
}
|
||||
|
||||
// Functional test for sync
|
||||
void AMultiplayerSyncTest::StartTest()
|
||||
{
|
||||
Super::StartTest();
|
||||
|
||||
// Spawn item on server
|
||||
APickupItem* Item = GetWorld()->SpawnActor<APickupItem>(
|
||||
ItemClass, FVector(0, 0, 100));
|
||||
|
||||
// Wait for replication
|
||||
FTimerHandle TimerHandle;
|
||||
GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this, Item]()
|
||||
{
|
||||
// Verify client has item
|
||||
if (VerifyItemExistsOnAllClients(Item))
|
||||
{
|
||||
FinishTest(EFunctionalTestResult::Succeeded, "Item replicated");
|
||||
}
|
||||
else
|
||||
{
|
||||
FinishTest(EFunctionalTestResult::Failed, "Item not found on clients");
|
||||
}
|
||||
}, 2.0f, false);
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
# Network simulation
|
||||
extends Node
|
||||
|
||||
var simulated_latency_ms := 0
|
||||
var packet_loss_percent := 0.0
|
||||
|
||||
func _ready():
|
||||
# Hook into network to simulate conditions
|
||||
multiplayer.peer_packet_received.connect(_on_packet_received)
|
||||
|
||||
func _on_packet_received(id: int, packet: PackedByteArray):
|
||||
if packet_loss_percent > 0 and randf() < packet_loss_percent / 100:
|
||||
return # Drop packet
|
||||
|
||||
if simulated_latency_ms > 0:
|
||||
await get_tree().create_timer(simulated_latency_ms / 1000.0).timeout
|
||||
|
||||
_process_packet(id, packet)
|
||||
|
||||
# Test
|
||||
func test_position_sync_under_latency():
|
||||
NetworkSimulator.simulated_latency_ms = 200
|
||||
|
||||
# Move player on host
|
||||
host_player.position = Vector3(100, 0, 100)
|
||||
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
# Check client view
|
||||
var client_view_position = client.get_remote_player_position(host_player.id)
|
||||
var distance = host_player.position.distance_to(client_view_position)
|
||||
|
||||
assert_lt(distance, 1.0, "Position should converge within 1 unit")
|
||||
```
|
||||
|
||||
## Dedicated Server Testing
|
||||
|
||||
### Test Matrix
|
||||
|
||||
| Scenario | Test Focus |
|
||||
| --------------------- | ------------------------------------ |
|
||||
| Server startup | Clean initialization, port binding |
|
||||
| Client authentication | Login validation, session management |
|
||||
| Server tick rate | Consistent updates under load |
|
||||
| Maximum players | Performance at player cap |
|
||||
| Server crash recovery | State preservation, reconnection |
|
||||
|
||||
### Load Testing
|
||||
|
||||
```
|
||||
SCENARIO: Maximum Players
|
||||
GIVEN server configured for 64 players
|
||||
WHEN 64 players connect
|
||||
THEN all connections succeed
|
||||
AND server tick rate stays above 60Hz
|
||||
AND latency stays below 50ms
|
||||
|
||||
SCENARIO: Stress Test
|
||||
GIVEN 64 players performing actions simultaneously
|
||||
WHEN running for 10 minutes
|
||||
THEN no memory leaks
|
||||
AND no desync events
|
||||
AND server CPU below 80%
|
||||
```
|
||||
|
||||
## Matchmaking Testing
|
||||
|
||||
```
|
||||
SCENARIO: Skill-Based Matching
|
||||
GIVEN players with skill ratings [1000, 1050, 2000, 2100]
|
||||
WHEN matchmaking runs
|
||||
THEN [1000, 1050] are grouped together
|
||||
AND [2000, 2100] are grouped together
|
||||
|
||||
SCENARIO: Region Matching
|
||||
GIVEN players from US-East, US-West, EU
|
||||
WHEN matchmaking runs
|
||||
THEN players prefer same-region matches
|
||||
AND cross-region only when necessary
|
||||
AND latency is acceptable for all players
|
||||
|
||||
SCENARIO: Queue Timeout
|
||||
GIVEN player waiting in queue
|
||||
WHEN 3 minutes pass without match
|
||||
THEN matchmaking expands search criteria
|
||||
AND player is notified of expanded search
|
||||
```
|
||||
|
||||
## Security Testing
|
||||
|
||||
| Vulnerability | Test Method |
|
||||
| ---------------- | --------------------------- |
|
||||
| Speed hacking | Validate movement on server |
|
||||
| Teleportation | Check position delta limits |
|
||||
| Damage hacking | Server-authoritative damage |
|
||||
| Packet injection | Validate packet checksums |
|
||||
| Replay attacks | Use unique session tokens |
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
| Metric | Good | Acceptable | Poor |
|
||||
| --------------------- | --------- | ---------- | ---------- |
|
||||
| Round-trip latency | < 50ms | < 100ms | > 150ms |
|
||||
| Sync delta | < 100ms | < 200ms | > 500ms |
|
||||
| Packet loss tolerance | < 5% | < 10% | > 15% |
|
||||
| Bandwidth per player | < 10 KB/s | < 50 KB/s | > 100 KB/s |
|
||||
| Server tick rate | 60+ Hz | 30+ Hz | < 20 Hz |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Test with real network conditions, not just localhost
|
||||
- Simulate worst-case scenarios (high latency + packet loss)
|
||||
- Use server-authoritative design for competitive games
|
||||
- Implement lag compensation for fast-paced games
|
||||
- Test host migration paths
|
||||
- Log network events for debugging
|
||||
|
||||
### DON'T
|
||||
|
||||
- Trust client data for important game state
|
||||
- Assume stable connections
|
||||
- Skip testing with maximum player counts
|
||||
- Ignore edge cases (simultaneous actions)
|
||||
- Test only in ideal network conditions
|
||||
- Forget to test reconnection flows
|
||||
@@ -0,0 +1,204 @@
|
||||
# Performance Testing for Games
|
||||
|
||||
## Overview
|
||||
|
||||
Performance testing ensures your game runs smoothly on target hardware. Frame rate, load times, and memory usage directly impact player experience.
|
||||
|
||||
## Key Performance Metrics
|
||||
|
||||
### Frame Rate
|
||||
|
||||
- **Target:** 30fps, 60fps, 120fps depending on platform/genre
|
||||
- **Measure:** Average, minimum, 1% low, 0.1% low
|
||||
- **Goal:** Consistent frame times, no stutters
|
||||
|
||||
### Frame Time Budget
|
||||
|
||||
At 60fps, you have 16.67ms per frame:
|
||||
|
||||
```
|
||||
Rendering: 8ms (48%)
|
||||
Game Logic: 4ms (24%)
|
||||
Physics: 2ms (12%)
|
||||
Audio: 1ms (6%)
|
||||
UI: 1ms (6%)
|
||||
Headroom: 0.67ms (4%)
|
||||
```
|
||||
|
||||
### Memory
|
||||
|
||||
- **RAM:** Total allocation, peak usage, fragmentation
|
||||
- **VRAM:** Texture memory, render targets, buffers
|
||||
- **Goal:** Stay within platform limits with headroom
|
||||
|
||||
### Load Times
|
||||
|
||||
- **Initial Load:** Time to main menu
|
||||
- **Level Load:** Time between scenes
|
||||
- **Streaming:** Asset loading during gameplay
|
||||
- **Goal:** Meet platform certification requirements
|
||||
|
||||
## Profiling Tools by Engine
|
||||
|
||||
### Unity
|
||||
|
||||
- **Profiler Window** - CPU, GPU, memory, rendering
|
||||
- **Frame Debugger** - Draw call analysis
|
||||
- **Memory Profiler** - Heap snapshots
|
||||
- **Profile Analyzer** - Compare captures
|
||||
|
||||
### Unreal Engine
|
||||
|
||||
- **Unreal Insights** - Comprehensive profiling
|
||||
- **Stat Commands** - Runtime statistics
|
||||
- **GPU Visualizer** - GPU timing breakdown
|
||||
- **Memory Report** - Allocation tracking
|
||||
|
||||
### Godot
|
||||
|
||||
- **Debugger** - Built-in profiler
|
||||
- **Monitors** - Real-time metrics
|
||||
- **Remote Debugger** - Profile on device
|
||||
|
||||
### Platform Tools
|
||||
|
||||
- **PIX** (Xbox/Windows) - GPU debugging
|
||||
- **RenderDoc** - GPU capture and replay
|
||||
- **Instruments** (iOS/macOS) - Apple profiling
|
||||
- **Android Profiler** - Android Studio tools
|
||||
|
||||
## Performance Testing Process
|
||||
|
||||
### 1. Establish Baselines
|
||||
|
||||
- Profile on target hardware
|
||||
- Record key metrics
|
||||
- Create benchmark scenes
|
||||
|
||||
### 2. Set Budgets
|
||||
|
||||
- Define frame time budgets per system
|
||||
- Set memory limits
|
||||
- Establish load time targets
|
||||
|
||||
### 3. Monitor Continuously
|
||||
|
||||
- Integrate profiling in CI
|
||||
- Track metrics over time
|
||||
- Alert on regressions
|
||||
|
||||
### 4. Optimize When Needed
|
||||
|
||||
- Profile before optimizing
|
||||
- Target biggest bottlenecks
|
||||
- Verify improvements
|
||||
|
||||
## Common Performance Issues
|
||||
|
||||
### CPU Bottlenecks
|
||||
|
||||
| Issue | Symptoms | Solution |
|
||||
| --------------------- | ----------------- | --------------------------------- |
|
||||
| Too many game objects | Slow update loop | Object pooling, LOD |
|
||||
| Expensive AI | Spiky frame times | Budget AI, spread over frames |
|
||||
| Physics overload | Physics spikes | Simplify colliders, reduce bodies |
|
||||
| GC stutter | Regular hitches | Avoid runtime allocations |
|
||||
|
||||
### GPU Bottlenecks
|
||||
|
||||
| Issue | Symptoms | Solution |
|
||||
| ------------------- | ----------------- | -------------------------------- |
|
||||
| Overdraw | Fill rate limited | Occlusion culling, reduce layers |
|
||||
| Too many draw calls | CPU-GPU bound | Batching, instancing, atlasing |
|
||||
| Shader complexity | Long GPU times | Simplify shaders, LOD |
|
||||
| Resolution too high | Fill rate limited | Dynamic resolution, FSR/DLSS |
|
||||
|
||||
### Memory Issues
|
||||
|
||||
| Issue | Symptoms | Solution |
|
||||
| ------------- | ----------------- | ---------------------------- |
|
||||
| Texture bloat | High VRAM | Compress, mipmap, stream |
|
||||
| Leaks | Growing memory | Track allocations, fix leaks |
|
||||
| Fragmentation | OOM despite space | Pool allocations, defrag |
|
||||
|
||||
## Benchmark Scenes
|
||||
|
||||
Create standardized test scenarios:
|
||||
|
||||
### Stress Test Scene
|
||||
|
||||
- Maximum entities on screen
|
||||
- Complex visual effects
|
||||
- Worst-case for performance
|
||||
|
||||
### Typical Gameplay Scene
|
||||
|
||||
- Representative of normal play
|
||||
- Average entity count
|
||||
- Baseline for comparison
|
||||
|
||||
### Isolated System Tests
|
||||
|
||||
- Combat only (no rendering)
|
||||
- Rendering only (no game logic)
|
||||
- AI only (pathfinding stress)
|
||||
|
||||
## Automated Performance Testing
|
||||
|
||||
### CI Integration
|
||||
|
||||
```yaml
|
||||
# Example: Fail build if frame time exceeds budget
|
||||
performance_test:
|
||||
script:
|
||||
- run_benchmark --scene stress_test
|
||||
- check_metrics --max-frame-time 16.67ms --max-memory 2GB
|
||||
artifacts:
|
||||
- performance_report.json
|
||||
```
|
||||
|
||||
### Regression Detection
|
||||
|
||||
- Compare against previous builds
|
||||
- Alert on significant changes (>10%)
|
||||
- Track trends over time
|
||||
|
||||
## Platform-Specific Considerations
|
||||
|
||||
### Console
|
||||
|
||||
- Fixed hardware targets
|
||||
- Strict certification requirements
|
||||
- Thermal throttling concerns
|
||||
|
||||
### PC
|
||||
|
||||
- Wide hardware range
|
||||
- Scalable quality settings
|
||||
- Min/recommended specs
|
||||
|
||||
### Mobile
|
||||
|
||||
- Thermal throttling
|
||||
- Battery impact
|
||||
- Memory constraints
|
||||
- Background app pressure
|
||||
|
||||
## Performance Testing Checklist
|
||||
|
||||
### Before Release
|
||||
|
||||
- [ ] Profiled on all target platforms
|
||||
- [ ] Frame rate targets met
|
||||
- [ ] No memory leaks
|
||||
- [ ] Load times acceptable
|
||||
- [ ] No GC stutters in gameplay
|
||||
- [ ] Thermal tests passed (mobile/console)
|
||||
- [ ] Certification requirements met
|
||||
|
||||
### Ongoing
|
||||
|
||||
- [ ] Performance tracked in CI
|
||||
- [ ] Regression alerts configured
|
||||
- [ ] Benchmark scenes maintained
|
||||
- [ ] Budgets documented and enforced
|
||||
@@ -0,0 +1,384 @@
|
||||
# Playtesting Fundamentals
|
||||
|
||||
## Overview
|
||||
|
||||
Playtesting is the process of having people play your game to gather feedback and identify issues. It's distinct from QA testing in that it focuses on player experience, fun factor, and design validation rather than bug hunting.
|
||||
|
||||
## Types of Playtesting
|
||||
|
||||
### Internal Playtesting
|
||||
|
||||
- **Developer Testing** - Daily testing during development
|
||||
- **Team Testing** - Cross-discipline team plays together
|
||||
- **Best for:** Rapid iteration, catching obvious issues
|
||||
|
||||
### External Playtesting
|
||||
|
||||
- **Friends & Family** - Trusted external testers
|
||||
- **Focus Groups** - Targeted demographic testing
|
||||
- **Public Beta** - Large-scale community testing
|
||||
- **Best for:** Fresh perspectives, UX validation
|
||||
|
||||
### Specialized Playtesting
|
||||
|
||||
- **Accessibility Testing** - Players with disabilities
|
||||
- **Localization Testing** - Regional/cultural validation
|
||||
- **Competitive Testing** - Balance and meta testing
|
||||
|
||||
## Playtesting Process
|
||||
|
||||
### 1. Define Goals
|
||||
|
||||
Before each playtest session, define:
|
||||
|
||||
- What questions are you trying to answer?
|
||||
- What features are you testing?
|
||||
- What metrics will you gather?
|
||||
|
||||
### 2. Prepare the Build
|
||||
|
||||
- Create a stable, playable build
|
||||
- Include telemetry/logging if needed
|
||||
- Prepare any necessary documentation
|
||||
|
||||
### 3. Brief Testers
|
||||
|
||||
- Explain what to test (or don't, for blind testing)
|
||||
- Set expectations for bugs/polish level
|
||||
- Provide feedback mechanisms
|
||||
|
||||
### 4. Observe and Record
|
||||
|
||||
- Watch players without intervening
|
||||
- Note confusion points, frustration, delight
|
||||
- Record gameplay if possible
|
||||
|
||||
### 5. Gather Feedback
|
||||
|
||||
- Structured surveys for quantitative data
|
||||
- Open discussion for qualitative insights
|
||||
- Allow time for "what else?" comments
|
||||
|
||||
### 6. Analyze and Act
|
||||
|
||||
- Identify patterns across testers
|
||||
- Prioritize issues by frequency and severity
|
||||
- Create actionable tasks from findings
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
### Engagement Metrics
|
||||
|
||||
- Session length
|
||||
- Return rate
|
||||
- Completion rate
|
||||
- Drop-off points
|
||||
|
||||
### Difficulty Metrics
|
||||
|
||||
- Deaths/failures per section
|
||||
- Time to complete sections
|
||||
- Hint/help usage
|
||||
- Difficulty setting distribution
|
||||
|
||||
### UX Metrics
|
||||
|
||||
- Time to first action
|
||||
- Tutorial completion rate
|
||||
- Menu navigation patterns
|
||||
- Control scheme preferences
|
||||
|
||||
## Playtesting by Game Type
|
||||
|
||||
Different genres require different playtesting approaches and focus areas.
|
||||
|
||||
### Action/Platformer Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Control responsiveness and "game feel"
|
||||
- Difficulty curve across levels
|
||||
- Checkpoint placement and frustration points
|
||||
- Visual clarity during fast-paced action
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Does the character feel good to control?
|
||||
- Are deaths feeling fair or cheap?
|
||||
- Is the player learning organically or hitting walls?
|
||||
|
||||
### RPG/Story Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Narrative pacing and engagement
|
||||
- Quest clarity and tracking
|
||||
- Character/dialogue believability
|
||||
- Progression and reward timing
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Do players understand their current objective?
|
||||
- Are choices feeling meaningful?
|
||||
- Is the story holding attention or being skipped?
|
||||
|
||||
### Puzzle Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Solution discoverability
|
||||
- "Aha moment" timing
|
||||
- Hint system effectiveness
|
||||
- Difficulty progression
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Are players solving puzzles the intended way?
|
||||
- How long before frustration sets in?
|
||||
- Do solutions feel satisfying or arbitrary?
|
||||
|
||||
### Multiplayer/Competitive Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Balance across characters/builds/strategies
|
||||
- Meta development and dominant strategies
|
||||
- Social dynamics and toxicity vectors
|
||||
- Matchmaking feel
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Are there "must-pick" or "never-pick" options?
|
||||
- Do losing players understand why they lost?
|
||||
- Is the skill ceiling high enough for mastery?
|
||||
|
||||
### Survival/Sandbox Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Early game onboarding and survival
|
||||
- Goal clarity vs. freedom balance
|
||||
- Resource economy and pacing
|
||||
- Emergent gameplay moments
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Do players know what to do first?
|
||||
- Is the loop engaging beyond the first hour?
|
||||
- Are players creating their own goals?
|
||||
|
||||
### Mobile/Casual Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Session length appropriateness
|
||||
- One-hand playability (if applicable)
|
||||
- Interruption handling (calls, notifications)
|
||||
- Monetization friction points
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- Can players play in 2-minute sessions?
|
||||
- Is the core loop immediately understandable?
|
||||
- Where do players churn?
|
||||
|
||||
### Horror Games
|
||||
|
||||
**Focus Areas:**
|
||||
|
||||
- Tension and release pacing
|
||||
- Scare effectiveness and desensitization
|
||||
- Safe space placement
|
||||
- Audio/visual atmosphere
|
||||
|
||||
**Key Questions:**
|
||||
|
||||
- When do players feel safe vs. threatened?
|
||||
- Are scares landing or becoming predictable?
|
||||
- Is anxiety sustainable or exhausting?
|
||||
|
||||
## Processing Feedback Effectively
|
||||
|
||||
Raw feedback is noise. Processed feedback is signal.
|
||||
|
||||
### The Feedback Processing Pipeline
|
||||
|
||||
```
|
||||
Raw Feedback → Categorize → Pattern Match → Root Cause → Prioritize → Action
|
||||
```
|
||||
|
||||
### Step 1: Categorize Feedback
|
||||
|
||||
Sort all feedback into buckets:
|
||||
|
||||
| Category | Examples |
|
||||
| ------------- | ---------------------------------- |
|
||||
| **Bugs** | Crashes, glitches, broken features |
|
||||
| **Usability** | Confusing UI, unclear objectives |
|
||||
| **Balance** | Too hard, too easy, unfair |
|
||||
| **Feel** | Controls, pacing, satisfaction |
|
||||
| **Content** | Wants more of X, dislikes Y |
|
||||
| **Polish** | Audio, visuals, juice |
|
||||
|
||||
### Step 2: Pattern Matching
|
||||
|
||||
Individual feedback is anecdotal. Patterns are data.
|
||||
|
||||
**Threshold Guidelines:**
|
||||
|
||||
- 1 person mentions it → Note it
|
||||
- 3+ people mention it → Investigate
|
||||
- 50%+ mention it → Priority issue
|
||||
|
||||
**Watch for:**
|
||||
|
||||
- Same complaint, different words
|
||||
- Same area, different complaints (signals deeper issue)
|
||||
- Contradictory feedback (may indicate preference split)
|
||||
|
||||
### Step 3: Root Cause Analysis
|
||||
|
||||
Players report symptoms, not diseases.
|
||||
|
||||
**Example:**
|
||||
|
||||
- **Symptom:** "The boss is too hard"
|
||||
- **Possible Root Causes:**
|
||||
- Boss mechanics unclear
|
||||
- Player didn't learn required skill earlier
|
||||
- Checkpoint too far from boss
|
||||
- Health/damage tuning off
|
||||
- Boss pattern has no safe windows
|
||||
|
||||
**Ask "Why?" five times** to get to root cause.
|
||||
|
||||
### Step 4: Separate Fact from Opinion
|
||||
|
||||
| Fact (Actionable) | Opinion (Context) |
|
||||
| --------------------------------- | ----------------------- |
|
||||
| "I died 12 times on level 3" | "Level 3 is too hard" |
|
||||
| "I didn't use the shield ability" | "The shield is useless" |
|
||||
| "I quit after 20 minutes" | "The game is boring" |
|
||||
|
||||
**Facts tell you WHAT happened. Opinions tell you how they FELT about it.**
|
||||
|
||||
Both matter, but facts drive solutions.
|
||||
|
||||
### Step 5: The Feedback Matrix
|
||||
|
||||
Plot issues on impact vs. effort:
|
||||
|
||||
```
|
||||
High Impact
|
||||
│
|
||||
Quick │ Major
|
||||
Wins │ Projects
|
||||
│
|
||||
─────────────┼─────────────
|
||||
│
|
||||
Fill │ Reconsider
|
||||
Time │
|
||||
│
|
||||
Low Impact
|
||||
Low Effort ──────── High Effort
|
||||
```
|
||||
|
||||
### Step 6: Validate Before Acting
|
||||
|
||||
Before making changes based on feedback:
|
||||
|
||||
1. **Reproduce** - Can you see the issue yourself?
|
||||
2. **Quantify** - How many players affected?
|
||||
3. **Contextualize** - Is this your target audience?
|
||||
4. **Test solutions** - Will the fix create new problems?
|
||||
|
||||
### Handling Contradictory Feedback
|
||||
|
||||
When Player A wants X and Player B wants the opposite:
|
||||
|
||||
1. **Check sample size** - Is it really split or just 2 loud voices?
|
||||
2. **Segment audiences** - Are these different player types?
|
||||
3. **Find the underlying need** - Both may want the same thing differently
|
||||
4. **Consider options** - Difficulty settings, toggles, multiple paths
|
||||
5. **Make a decision** - You can't please everyone; know your target
|
||||
|
||||
### Feedback Red Flags
|
||||
|
||||
**Dismiss or investigate carefully:**
|
||||
|
||||
- "Make it like [other game]" - They want a feeling, not a clone
|
||||
- "Add multiplayer" - Feature creep disguised as feedback
|
||||
- "I would have bought it if..." - Hypothetical customers aren't real
|
||||
- Feedback from non-target audience - Know who you're building for
|
||||
|
||||
**Take seriously:**
|
||||
|
||||
- Confusion about core mechanics
|
||||
- Consistent drop-off at same point
|
||||
- "I wanted to like it but..."
|
||||
- Silent quitting (no feedback, just gone)
|
||||
|
||||
### Documentation Best Practices
|
||||
|
||||
**For each playtest session, record:**
|
||||
|
||||
- Date and build version
|
||||
- Tester demographics/experience
|
||||
- Session length
|
||||
- Key observations (timestamped if recorded)
|
||||
- Quantitative survey results
|
||||
- Top 3 issues identified
|
||||
- Actions taken as result
|
||||
|
||||
**Maintain a living document** that tracks:
|
||||
|
||||
- Issue → First reported → Times reported → Status → Resolution
|
||||
- This prevents re-discovering the same issues
|
||||
|
||||
## Common Playtesting Pitfalls
|
||||
|
||||
### Leading Questions
|
||||
|
||||
**Bad:** "Did you find the combat exciting?"
|
||||
**Good:** "How would you describe the combat?"
|
||||
|
||||
### Intervening Too Soon
|
||||
|
||||
Let players struggle before helping. Confusion is valuable data.
|
||||
|
||||
### Testing Too Late
|
||||
|
||||
Start playtesting early with paper prototypes and gray boxes.
|
||||
|
||||
### Ignoring Negative Feedback
|
||||
|
||||
Negative feedback is often the most valuable. Don't dismiss it.
|
||||
|
||||
### Over-Relying on Verbal Feedback
|
||||
|
||||
Watch what players DO, not just what they SAY. Actions reveal truth.
|
||||
|
||||
## Playtesting Checklist
|
||||
|
||||
### Pre-Session
|
||||
|
||||
- [ ] Goals defined
|
||||
- [ ] Build stable and deployed
|
||||
- [ ] Recording setup (if applicable)
|
||||
- [ ] Feedback forms ready
|
||||
- [ ] Testers briefed
|
||||
|
||||
### During Session
|
||||
|
||||
- [ ] Observing without intervening
|
||||
- [ ] Taking notes on behavior
|
||||
- [ ] Tracking time markers for notable moments
|
||||
- [ ] Noting emotional reactions
|
||||
|
||||
### Post-Session
|
||||
|
||||
- [ ] Feedback collected
|
||||
- [ ] Patterns identified
|
||||
- [ ] Priority issues flagged
|
||||
- [ ] Action items created
|
||||
- [ ] Results shared with team
|
||||
@@ -0,0 +1,190 @@
|
||||
# QA Automation for Games
|
||||
|
||||
## Overview
|
||||
|
||||
Automated testing in games requires different approaches than traditional software. Games have complex state, real-time interactions, and subjective quality measures that challenge automation.
|
||||
|
||||
## Testing Pyramid for Games
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ Manual Playtesting
|
||||
/----\ (Experience, Feel, Fun)
|
||||
/ \
|
||||
/--------\ Integration Tests
|
||||
/ \ (Systems, Workflows)
|
||||
/------------\
|
||||
/ \ Unit Tests
|
||||
/________________\ (Pure Logic, Math, Data)
|
||||
```
|
||||
|
||||
### Unit Tests (Foundation)
|
||||
|
||||
Test pure logic that doesn't depend on engine runtime:
|
||||
|
||||
- Math utilities (vectors, transforms, curves)
|
||||
- Data validation (save files, configs)
|
||||
- State machines (isolated logic)
|
||||
- Algorithm correctness
|
||||
|
||||
### Integration Tests (Middle Layer)
|
||||
|
||||
Test system interactions:
|
||||
|
||||
- Combat system + inventory
|
||||
- Save/load round-trips
|
||||
- Scene transitions
|
||||
- Network message handling
|
||||
|
||||
### Manual Testing (Top)
|
||||
|
||||
What can't be automated:
|
||||
|
||||
- "Does this feel good?"
|
||||
- "Is this fun?"
|
||||
- "Is the difficulty right?"
|
||||
|
||||
## Automation Strategies by Engine
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
// Unity Test Framework
|
||||
[Test]
|
||||
public void DamageCalculation_CriticalHit_DoublesDamage()
|
||||
{
|
||||
var baseDamage = 100;
|
||||
var result = DamageCalculator.Calculate(baseDamage, isCritical: true);
|
||||
Assert.AreEqual(200, result);
|
||||
}
|
||||
|
||||
// Play Mode Tests (runtime)
|
||||
[UnityTest]
|
||||
public IEnumerator PlayerJump_WhenGrounded_BecomesAirborne()
|
||||
{
|
||||
var player = CreateTestPlayer();
|
||||
player.Jump();
|
||||
yield return new WaitForFixedUpdate();
|
||||
Assert.IsFalse(player.IsGrounded);
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal Engine
|
||||
|
||||
```cpp
|
||||
// Automation Framework
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDamageTest, "Game.Combat.Damage",
|
||||
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)
|
||||
|
||||
bool FDamageTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
float BaseDamage = 100.f;
|
||||
float Result = UDamageCalculator::Calculate(BaseDamage, true);
|
||||
TestEqual("Critical hit doubles damage", Result, 200.f);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
# GUT Testing Framework
|
||||
func test_damage_critical_hit():
|
||||
var base_damage = 100
|
||||
var result = DamageCalculator.calculate(base_damage, true)
|
||||
assert_eq(result, 200, "Critical hit should double damage")
|
||||
```
|
||||
|
||||
## What to Automate
|
||||
|
||||
### High Value Targets
|
||||
|
||||
- **Save/Load** - Data integrity is critical
|
||||
- **Economy** - Currency, items, progression math
|
||||
- **Combat Math** - Damage, stats, modifiers
|
||||
- **Localization** - String loading, formatting
|
||||
- **Network Serialization** - Message encoding/decoding
|
||||
|
||||
### Medium Value Targets
|
||||
|
||||
- **State Machines** - Character states, game states
|
||||
- **Pathfinding** - Known scenarios
|
||||
- **Spawning** - Wave generation, loot tables
|
||||
- **UI Data Binding** - Correct values displayed
|
||||
|
||||
### Low Value / Avoid
|
||||
|
||||
- **Visual Quality** - Screenshots drift, hard to maintain
|
||||
- **Input Feel** - Timing-sensitive, needs human judgment
|
||||
- **Audio** - Subjective, context-dependent
|
||||
- **Fun** - Cannot be automated
|
||||
|
||||
## Continuous Integration for Games
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
1. **Compile** - Build game executable
|
||||
2. **Unit Tests** - Fast, isolated tests
|
||||
3. **Integration Tests** - Longer, system tests
|
||||
4. **Smoke Test** - Can the game launch and reach main menu?
|
||||
5. **Nightly** - Extended test suites, performance benchmarks
|
||||
|
||||
### CI Gotchas for Games
|
||||
|
||||
- **Long build times** - Games take longer than web apps
|
||||
- **GPU requirements** - Some tests need graphics hardware
|
||||
- **Asset dependencies** - Large files, binary formats
|
||||
- **Platform builds** - Multiple targets to maintain
|
||||
|
||||
## Regression Testing
|
||||
|
||||
### Automated Regression
|
||||
|
||||
- Run full test suite on every commit
|
||||
- Flag performance regressions (frame time, memory)
|
||||
- Track test stability (flaky tests)
|
||||
|
||||
### Save File Regression
|
||||
|
||||
- Maintain library of save files from previous versions
|
||||
- Test that new builds can load old saves
|
||||
- Alert on schema changes
|
||||
|
||||
## Test Data Management
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
```
|
||||
tests/
|
||||
├── fixtures/
|
||||
│ ├── save_files/
|
||||
│ │ ├── new_game.sav
|
||||
│ │ ├── mid_game.sav
|
||||
│ │ └── endgame.sav
|
||||
│ ├── configs/
|
||||
│ │ └── test_balance.json
|
||||
│ └── scenarios/
|
||||
│ └── boss_fight_setup.scene
|
||||
```
|
||||
|
||||
### Deterministic Testing
|
||||
|
||||
- Seed random number generators
|
||||
- Control time/delta time
|
||||
- Mock external services
|
||||
|
||||
## Metrics and Reporting
|
||||
|
||||
### Track Over Time
|
||||
|
||||
- Test count (growing is good)
|
||||
- Pass rate (should be ~100%)
|
||||
- Execution time (catch slow tests)
|
||||
- Code coverage (where applicable)
|
||||
- Flaky test rate (should be ~0%)
|
||||
|
||||
### Alerts
|
||||
|
||||
- Immediate: Any test failure on main branch
|
||||
- Daily: Coverage drops, new flaky tests
|
||||
- Weekly: Trend analysis, slow test growth
|
||||
@@ -0,0 +1,280 @@
|
||||
# Regression Testing for Games
|
||||
|
||||
## Overview
|
||||
|
||||
Regression testing catches bugs introduced by new changes. In games, this includes functional regressions, performance regressions, and design regressions.
|
||||
|
||||
## Types of Regression
|
||||
|
||||
### Functional Regression
|
||||
|
||||
- Features that worked before now break
|
||||
- New bugs introduced by unrelated changes
|
||||
- Broken integrations between systems
|
||||
|
||||
### Performance Regression
|
||||
|
||||
- Frame rate drops
|
||||
- Memory usage increases
|
||||
- Load time increases
|
||||
- Battery drain (mobile)
|
||||
|
||||
### Design Regression
|
||||
|
||||
- Balance changes with unintended side effects
|
||||
- UX changes that hurt usability
|
||||
- Art changes that break visual consistency
|
||||
|
||||
### Save Data Regression
|
||||
|
||||
- Old save files no longer load
|
||||
- Progression lost or corrupted
|
||||
- Achievements/unlocks reset
|
||||
|
||||
## Regression Testing Strategy
|
||||
|
||||
### Test Suite Layers
|
||||
|
||||
```
|
||||
High-Frequency (Every Commit)
|
||||
├── Unit Tests - Fast, isolated
|
||||
├── Smoke Tests - Can game launch and run?
|
||||
└── Critical Path - Core gameplay works
|
||||
|
||||
Medium-Frequency (Nightly)
|
||||
├── Integration Tests - System interactions
|
||||
├── Full Playthrough - Automated or manual
|
||||
└── Performance Benchmarks - Frame time, memory
|
||||
|
||||
Low-Frequency (Release)
|
||||
├── Full Matrix - All platforms/configs
|
||||
├── Certification Tests - Platform requirements
|
||||
└── Localization - All languages
|
||||
```
|
||||
|
||||
### What to Test
|
||||
|
||||
#### Critical Path (Must Not Break)
|
||||
|
||||
- Game launches
|
||||
- New game starts
|
||||
- Save/load works
|
||||
- Core gameplay loop completes
|
||||
- Main menu navigation
|
||||
|
||||
#### High Priority
|
||||
|
||||
- All game systems function
|
||||
- Progression works end-to-end
|
||||
- Multiplayer connects and syncs
|
||||
- In-app purchases process
|
||||
- Achievements trigger
|
||||
|
||||
#### Medium Priority
|
||||
|
||||
- Edge cases in systems
|
||||
- Optional content accessible
|
||||
- Settings persist correctly
|
||||
- Localization displays
|
||||
|
||||
## Automated Regression Tests
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
```python
|
||||
# Run on every commit
|
||||
def test_game_launches():
|
||||
process = launch_game()
|
||||
assert wait_for_main_menu(timeout=30)
|
||||
process.terminate()
|
||||
|
||||
def test_new_game_starts():
|
||||
launch_game()
|
||||
click_new_game()
|
||||
assert wait_for_gameplay(timeout=60)
|
||||
|
||||
def test_save_load_roundtrip():
|
||||
launch_game()
|
||||
start_new_game()
|
||||
perform_actions()
|
||||
save_game()
|
||||
load_game()
|
||||
assert verify_state_matches()
|
||||
```
|
||||
|
||||
### Playthrough Bots
|
||||
|
||||
```python
|
||||
# Automated player that plays through content
|
||||
class PlaythroughBot:
|
||||
def run_level(self, level):
|
||||
self.load_level(level)
|
||||
while not self.level_complete:
|
||||
self.perform_action()
|
||||
self.check_for_softlocks()
|
||||
self.record_metrics()
|
||||
```
|
||||
|
||||
### Visual Regression
|
||||
|
||||
```python
|
||||
# Compare screenshots against baselines
|
||||
def test_main_menu_visual():
|
||||
launch_game()
|
||||
screenshot = capture_screen()
|
||||
assert compare_to_baseline(screenshot, 'main_menu', threshold=0.01)
|
||||
```
|
||||
|
||||
## Performance Regression Detection
|
||||
|
||||
### Metrics to Track
|
||||
|
||||
- Average frame time
|
||||
- 1% low frame time
|
||||
- Memory usage (peak, average)
|
||||
- Load times
|
||||
- Draw calls
|
||||
- Texture memory
|
||||
|
||||
### Automated Benchmarks
|
||||
|
||||
```yaml
|
||||
performance_benchmark:
|
||||
script:
|
||||
- run_benchmark_scene --duration 60s
|
||||
- collect_metrics
|
||||
- compare_to_baseline
|
||||
fail_conditions:
|
||||
- frame_time_avg > baseline * 1.1 # 10% tolerance
|
||||
- memory_peak > baseline * 1.05 # 5% tolerance
|
||||
```
|
||||
|
||||
### Trend Tracking
|
||||
|
||||
- Graph metrics over time
|
||||
- Alert on upward trends
|
||||
- Identify problematic commits
|
||||
|
||||
## Save Compatibility Testing
|
||||
|
||||
### Version Matrix
|
||||
|
||||
Maintain save files from:
|
||||
|
||||
- Previous major version
|
||||
- Previous minor version
|
||||
- Current development build
|
||||
|
||||
### Automated Validation
|
||||
|
||||
```python
|
||||
def test_save_compatibility():
|
||||
for save_file in LEGACY_SAVES:
|
||||
load_save(save_file)
|
||||
assert no_errors()
|
||||
assert progress_preserved()
|
||||
assert inventory_intact()
|
||||
```
|
||||
|
||||
### Schema Versioning
|
||||
|
||||
- Version your save format
|
||||
- Implement upgrade paths
|
||||
- Log migration issues
|
||||
|
||||
## Regression Bug Workflow
|
||||
|
||||
### 1. Detection
|
||||
|
||||
- Automated test fails
|
||||
- Manual tester finds issue
|
||||
- Player report comes in
|
||||
|
||||
### 2. Verification
|
||||
|
||||
- Confirm it worked before
|
||||
- Identify when it broke
|
||||
- Find the breaking commit
|
||||
|
||||
### 3. Triage
|
||||
|
||||
- Assess severity
|
||||
- Determine fix urgency
|
||||
- Assign to appropriate developer
|
||||
|
||||
### 4. Fix and Verify
|
||||
|
||||
- Implement fix
|
||||
- Add regression test
|
||||
- Verify fix doesn't break other things
|
||||
|
||||
### 5. Post-Mortem
|
||||
|
||||
- Why wasn't this caught?
|
||||
- How can we prevent similar issues?
|
||||
- Do we need new tests?
|
||||
|
||||
## Bisecting Regressions
|
||||
|
||||
When a regression is found, identify the breaking commit:
|
||||
|
||||
### Git Bisect
|
||||
|
||||
```bash
|
||||
git bisect start
|
||||
git bisect bad HEAD # Current is broken
|
||||
git bisect good v1.2.0 # Known good version
|
||||
# Git will checkout commits to test
|
||||
# Run test, mark good/bad
|
||||
git bisect good/bad
|
||||
# Repeat until culprit found
|
||||
```
|
||||
|
||||
### Automated Bisect
|
||||
|
||||
```bash
|
||||
git bisect start HEAD v1.2.0
|
||||
git bisect run ./run_regression_test.sh
|
||||
```
|
||||
|
||||
## Regression Testing Checklist
|
||||
|
||||
### Per Commit
|
||||
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Smoke tests pass
|
||||
- [ ] Build succeeds on all platforms
|
||||
|
||||
### Per Merge to Main
|
||||
|
||||
- [ ] Integration tests pass
|
||||
- [ ] Performance benchmarks within tolerance
|
||||
- [ ] Save compatibility verified
|
||||
|
||||
### Per Release
|
||||
|
||||
- [ ] Full playthrough completed
|
||||
- [ ] All platforms tested
|
||||
- [ ] Legacy saves load correctly
|
||||
- [ ] No new critical regressions
|
||||
- [ ] All previous hotfix issues still resolved
|
||||
|
||||
## Building a Regression Suite
|
||||
|
||||
### Start Small
|
||||
|
||||
1. Add tests for bugs as they're fixed
|
||||
2. Cover critical path first
|
||||
3. Expand coverage over time
|
||||
|
||||
### Maintain Quality
|
||||
|
||||
- Delete flaky tests
|
||||
- Keep tests fast
|
||||
- Update tests with design changes
|
||||
|
||||
### Measure Effectiveness
|
||||
|
||||
- Track bugs caught by tests
|
||||
- Track bugs that slipped through
|
||||
- Identify coverage gaps
|
||||
@@ -0,0 +1,280 @@
|
||||
# Save System Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Save system testing ensures data persistence, integrity, and compatibility across game versions. Save bugs are among the most frustrating for players—data loss destroys trust.
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Data Integrity
|
||||
|
||||
| Test Type | Description | Priority |
|
||||
| -------------------- | ------------------------------------------- | -------- |
|
||||
| Round-trip | Save → Load → Verify all data matches | P0 |
|
||||
| Corruption detection | Tampered/corrupted files handled gracefully | P0 |
|
||||
| Partial write | Power loss during save doesn't corrupt | P0 |
|
||||
| Large saves | Performance with max-size save files | P1 |
|
||||
| Edge values | Min/max values for all saved fields | P1 |
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
| Scenario | Expected Behavior |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| Current → Current | Full compatibility |
|
||||
| Old → New (upgrade) | Migration with data preservation |
|
||||
| New → Old (downgrade) | Graceful rejection or limited support |
|
||||
| Corrupted version field | Fallback to recovery mode |
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Core Save/Load Tests
|
||||
|
||||
```
|
||||
SCENARIO: Basic Save Round-Trip
|
||||
GIVEN player has 100 health, 50 gold, position (10, 5, 20)
|
||||
AND player has inventory: ["sword", "potion", "key"]
|
||||
WHEN game is saved
|
||||
AND game is reloaded
|
||||
THEN player health equals 100
|
||||
AND player gold equals 50
|
||||
AND player position equals (10, 5, 20)
|
||||
AND inventory contains exactly ["sword", "potion", "key"]
|
||||
|
||||
SCENARIO: Save During Gameplay
|
||||
GIVEN player is in combat
|
||||
AND enemy has 50% health remaining
|
||||
WHEN autosave triggers
|
||||
AND game is reloaded
|
||||
THEN combat state is restored
|
||||
AND enemy health equals 50%
|
||||
|
||||
SCENARIO: Multiple Save Slots
|
||||
GIVEN save slot 1 has character "Hero" at level 10
|
||||
AND save slot 2 has character "Mage" at level 5
|
||||
WHEN switching between slots
|
||||
THEN correct character data loads for each slot
|
||||
AND no cross-contamination between slots
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
```
|
||||
SCENARIO: Maximum Inventory Save
|
||||
GIVEN player has 999 items in inventory
|
||||
WHEN game is saved
|
||||
AND game is reloaded
|
||||
THEN all 999 items are preserved
|
||||
AND save/load completes within 5 seconds
|
||||
|
||||
SCENARIO: Unicode Character Names
|
||||
GIVEN player name is "プレイヤー名"
|
||||
WHEN game is saved
|
||||
AND game is reloaded
|
||||
THEN player name displays correctly
|
||||
|
||||
SCENARIO: Extreme Play Time
|
||||
GIVEN play time is 9999:59:59
|
||||
WHEN game is saved
|
||||
AND game is reloaded
|
||||
THEN play time displays correctly
|
||||
AND timer continues from saved value
|
||||
```
|
||||
|
||||
### Corruption Recovery
|
||||
|
||||
```
|
||||
SCENARIO: Corrupted Save Detection
|
||||
GIVEN save file has been manually corrupted
|
||||
WHEN game attempts to load
|
||||
THEN error is detected before loading
|
||||
AND user is informed of corruption
|
||||
AND game does not crash
|
||||
|
||||
SCENARIO: Missing Save File
|
||||
GIVEN save file has been deleted externally
|
||||
WHEN game attempts to load
|
||||
THEN graceful error handling
|
||||
AND option to start new game or restore backup
|
||||
|
||||
SCENARIO: Interrupted Save (Power Loss)
|
||||
GIVEN save operation is interrupted mid-write
|
||||
WHEN game restarts
|
||||
THEN backup save is detected and offered
|
||||
AND no data loss from previous valid save
|
||||
```
|
||||
|
||||
## Platform-Specific Testing
|
||||
|
||||
### PC (Steam/Epic)
|
||||
|
||||
- Cloud save sync conflicts
|
||||
- Multiple Steam accounts on same PC
|
||||
- Offline → Online sync
|
||||
- Save location permissions (Program Files issues)
|
||||
|
||||
### Console (PlayStation/Xbox/Switch)
|
||||
|
||||
- System-level save management
|
||||
- Storage full scenarios
|
||||
- User switching mid-game
|
||||
- Suspend/resume with unsaved changes
|
||||
- Cloud save quota limits
|
||||
|
||||
### Mobile
|
||||
|
||||
- App termination during save
|
||||
- Low storage warnings
|
||||
- iCloud/Google Play sync
|
||||
- Device migration
|
||||
|
||||
## Automated Test Examples
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void SaveLoad_PlayerStats_PreservesAllValues()
|
||||
{
|
||||
var original = new PlayerData
|
||||
{
|
||||
Health = 75,
|
||||
MaxHealth = 100,
|
||||
Gold = 1234567,
|
||||
Position = new Vector3(100.5f, 0, -50.25f),
|
||||
PlayTime = 36000f // 10 hours
|
||||
};
|
||||
|
||||
SaveManager.Save(original, "test_slot");
|
||||
var loaded = SaveManager.Load("test_slot");
|
||||
|
||||
Assert.AreEqual(original.Health, loaded.Health);
|
||||
Assert.AreEqual(original.Gold, loaded.Gold);
|
||||
Assert.AreEqual(original.Position, loaded.Position);
|
||||
Assert.AreEqual(original.PlayTime, loaded.PlayTime, 0.01f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveLoad_CorruptedFile_HandlesGracefully()
|
||||
{
|
||||
File.WriteAllText(SaveManager.GetPath("corrupt"), "INVALID DATA");
|
||||
|
||||
Assert.Throws<SaveCorruptedException>(() =>
|
||||
SaveManager.Load("corrupt"));
|
||||
|
||||
// Game should not crash
|
||||
Assert.IsTrue(SaveManager.IsValidSaveSlot("corrupt") == false);
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal
|
||||
|
||||
```cpp
|
||||
bool FSaveSystemTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Create test save
|
||||
USaveGame* SaveGame = UGameplayStatics::CreateSaveGameObject(
|
||||
UMySaveGame::StaticClass());
|
||||
UMySaveGame* MySave = Cast<UMySaveGame>(SaveGame);
|
||||
|
||||
MySave->PlayerLevel = 50;
|
||||
MySave->Gold = 999999;
|
||||
MySave->QuestsCompleted = {"Quest1", "Quest2", "Quest3"};
|
||||
|
||||
// Save
|
||||
UGameplayStatics::SaveGameToSlot(MySave, "TestSlot", 0);
|
||||
|
||||
// Load
|
||||
USaveGame* Loaded = UGameplayStatics::LoadGameFromSlot("TestSlot", 0);
|
||||
UMySaveGame* LoadedSave = Cast<UMySaveGame>(Loaded);
|
||||
|
||||
TestEqual("Level preserved", LoadedSave->PlayerLevel, 50);
|
||||
TestEqual("Gold preserved", LoadedSave->Gold, 999999);
|
||||
TestEqual("Quests count", LoadedSave->QuestsCompleted.Num(), 3);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
func test_save_load_round_trip():
|
||||
var original = {
|
||||
"health": 100,
|
||||
"position": Vector3(10, 0, 20),
|
||||
"inventory": ["sword", "shield"],
|
||||
"quest_flags": {"intro_complete": true, "boss_defeated": false}
|
||||
}
|
||||
|
||||
SaveManager.save_game(original, "test_save")
|
||||
var loaded = SaveManager.load_game("test_save")
|
||||
|
||||
assert_eq(loaded.health, 100)
|
||||
assert_eq(loaded.position, Vector3(10, 0, 20))
|
||||
assert_eq(loaded.inventory.size(), 2)
|
||||
assert_true(loaded.quest_flags.intro_complete)
|
||||
assert_false(loaded.quest_flags.boss_defeated)
|
||||
|
||||
func test_corrupted_save_detection():
|
||||
var file = FileAccess.open("user://saves/corrupt.sav", FileAccess.WRITE)
|
||||
file.store_string("CORRUPTED GARBAGE DATA")
|
||||
file.close()
|
||||
|
||||
var result = SaveManager.load_game("corrupt")
|
||||
|
||||
assert_null(result, "Should return null for corrupted save")
|
||||
assert_false(SaveManager.is_valid_save("corrupt"))
|
||||
```
|
||||
|
||||
## Migration Testing
|
||||
|
||||
### Version Upgrade Matrix
|
||||
|
||||
| From Version | To Version | Test Focus |
|
||||
| -------------- | ---------------- | ---------------------------- |
|
||||
| 1.0 → 1.1 | Minor update | New fields default correctly |
|
||||
| 1.x → 2.0 | Major update | Schema migration works |
|
||||
| Beta → Release | Launch migration | All beta saves convert |
|
||||
|
||||
### Migration Test Template
|
||||
|
||||
```
|
||||
SCENARIO: Save Migration v1.0 to v2.0
|
||||
GIVEN save file from version 1.0
|
||||
AND save contains old inventory format (array)
|
||||
WHEN game version 2.0 loads the save
|
||||
THEN inventory is migrated to new format (dictionary)
|
||||
AND all items are preserved
|
||||
AND migration is logged
|
||||
AND backup of original is created
|
||||
```
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
| Metric | Target | Maximum |
|
||||
| ------------------------ | --------------- | ------- |
|
||||
| Save time (typical) | < 500ms | 2s |
|
||||
| Save time (large) | < 2s | 5s |
|
||||
| Load time (typical) | < 1s | 3s |
|
||||
| Save file size (typical) | < 1MB | 10MB |
|
||||
| Memory during save | < 50MB overhead | 100MB |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use atomic saves (write to temp, then rename)
|
||||
- Keep backup of previous save
|
||||
- Version your save format
|
||||
- Encrypt sensitive data
|
||||
- Test on minimum-spec hardware
|
||||
- Compress large saves
|
||||
|
||||
### DON'T
|
||||
|
||||
- Store absolute file paths
|
||||
- Save derived/calculated data
|
||||
- Trust save file contents blindly
|
||||
- Block gameplay during save
|
||||
- Forget to handle storage-full scenarios
|
||||
- Skip testing save migration paths
|
||||
@@ -0,0 +1,404 @@
|
||||
# Smoke Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Smoke testing (Build Verification Testing) validates that a build's critical functionality works before investing time in detailed testing. A failed smoke test means "stop, this build is broken."
|
||||
|
||||
## Purpose
|
||||
|
||||
| Goal | Description |
|
||||
| ------------------- | ---------------------------------------------- |
|
||||
| Fast feedback | Know within minutes if build is viable |
|
||||
| Block bad builds | Prevent broken builds from reaching QA/players |
|
||||
| Critical path focus | Test only what matters most |
|
||||
| CI/CD integration | Automated gate before deployment |
|
||||
|
||||
## Smoke Test Principles
|
||||
|
||||
### What Makes a Good Smoke Test
|
||||
|
||||
- **Fast**: Complete in 5-15 minutes
|
||||
- **Critical**: Tests only essential functionality
|
||||
- **Deterministic**: Same result every run
|
||||
- **Automated**: No human intervention required
|
||||
- **Clear**: Pass/fail with actionable feedback
|
||||
|
||||
### What to Include
|
||||
|
||||
| Category | Examples |
|
||||
| ----------------- | ------------------------------ |
|
||||
| Boot sequence | Game launches without crash |
|
||||
| Core loop | Player can perform main action |
|
||||
| Save/Load | Data persists correctly |
|
||||
| Critical UI | Menus are navigable |
|
||||
| Platform services | Connects to required services |
|
||||
|
||||
### What NOT to Include
|
||||
|
||||
- Edge cases and boundary conditions
|
||||
- Performance benchmarks (separate tests)
|
||||
- Full feature coverage
|
||||
- Content verification
|
||||
- Balance testing
|
||||
|
||||
## Smoke Test Scenarios
|
||||
|
||||
### Boot and Load
|
||||
|
||||
```
|
||||
TEST: Game Launches
|
||||
WHEN game executable is started
|
||||
THEN main menu appears within 60 seconds
|
||||
AND no crashes occur
|
||||
AND required services connect
|
||||
|
||||
TEST: New Game Start
|
||||
GIVEN game at main menu
|
||||
WHEN "New Game" is selected
|
||||
THEN gameplay loads within 30 seconds
|
||||
AND player can control character
|
||||
|
||||
TEST: Continue Game
|
||||
GIVEN existing save file
|
||||
WHEN "Continue" is selected
|
||||
THEN correct save loads
|
||||
AND game state matches saved state
|
||||
```
|
||||
|
||||
### Core Gameplay
|
||||
|
||||
```
|
||||
TEST: Player Movement
|
||||
GIVEN player in game world
|
||||
WHEN movement input applied
|
||||
THEN player moves in expected direction
|
||||
AND no physics glitches occur
|
||||
|
||||
TEST: Core Action (Game-Specific)
|
||||
GIVEN player can perform primary action
|
||||
WHEN action is triggered
|
||||
THEN action executes correctly
|
||||
AND expected results occur
|
||||
|
||||
Examples:
|
||||
- Shooter: Can fire weapon, bullets hit targets
|
||||
- RPG: Can attack enemy, damage is applied
|
||||
- Puzzle: Can interact with puzzle elements
|
||||
- Platformer: Can jump, platforms are solid
|
||||
```
|
||||
|
||||
### Save System
|
||||
|
||||
```
|
||||
TEST: Save Creates File
|
||||
GIVEN player makes progress
|
||||
WHEN save is triggered
|
||||
THEN save file is created
|
||||
AND save completes without error
|
||||
|
||||
TEST: Load Restores State
|
||||
GIVEN valid save file exists
|
||||
WHEN load is triggered
|
||||
THEN saved state is restored
|
||||
AND gameplay can continue
|
||||
```
|
||||
|
||||
### Critical UI
|
||||
|
||||
```
|
||||
TEST: Menu Navigation
|
||||
GIVEN main menu is displayed
|
||||
WHEN each menu option is selected
|
||||
THEN correct screen/action occurs
|
||||
AND navigation back works
|
||||
|
||||
TEST: Settings Persist
|
||||
GIVEN settings are changed
|
||||
WHEN game is restarted
|
||||
THEN settings remain changed
|
||||
```
|
||||
|
||||
## Automated Smoke Test Examples
|
||||
|
||||
### Unity
|
||||
|
||||
```csharp
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.TestTools;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
[TestFixture]
|
||||
public class SmokeTests
|
||||
{
|
||||
[UnityTest, Timeout(60000)]
|
||||
public IEnumerator Game_Launches_ToMainMenu()
|
||||
{
|
||||
// Load main menu scene
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
yield return new WaitForSeconds(5f);
|
||||
|
||||
// Verify menu is active
|
||||
var mainMenu = GameObject.Find("MainMenuCanvas");
|
||||
Assert.IsNotNull(mainMenu, "Main menu should be present");
|
||||
Assert.IsTrue(mainMenu.activeInHierarchy, "Main menu should be active");
|
||||
}
|
||||
|
||||
[UnityTest, Timeout(120000)]
|
||||
public IEnumerator NewGame_LoadsGameplay()
|
||||
{
|
||||
// Start from main menu
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
// Click new game
|
||||
var newGameButton = GameObject.Find("NewGameButton")
|
||||
.GetComponent<Button>();
|
||||
newGameButton.onClick.Invoke();
|
||||
|
||||
yield return new WaitForSeconds(10f);
|
||||
|
||||
// Verify gameplay scene loaded
|
||||
Assert.AreEqual("GameplayScene", SceneManager.GetActiveScene().name);
|
||||
|
||||
// Verify player exists and can be controlled
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
Assert.IsNotNull(player, "Player should exist");
|
||||
}
|
||||
|
||||
[UnityTest, Timeout(30000)]
|
||||
public IEnumerator Player_CanMove()
|
||||
{
|
||||
// Load gameplay
|
||||
SceneManager.LoadScene("GameplayScene");
|
||||
yield return new WaitForSeconds(3f);
|
||||
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
var startPos = player.transform.position;
|
||||
|
||||
// Simulate movement input
|
||||
var controller = player.GetComponent<PlayerController>();
|
||||
controller.SetMoveInput(Vector2.right);
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
// Verify movement occurred
|
||||
Assert.Greater(player.transform.position.x, startPos.x,
|
||||
"Player should have moved");
|
||||
}
|
||||
|
||||
[UnityTest, Timeout(30000)]
|
||||
public IEnumerator SaveLoad_RoundTrip_Works()
|
||||
{
|
||||
// Setup test state
|
||||
SceneManager.LoadScene("GameplayScene");
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
var player = GameObject.FindWithTag("Player");
|
||||
player.transform.position = new Vector3(100, 0, 100);
|
||||
|
||||
// Save
|
||||
SaveManager.Save("smoke_test");
|
||||
yield return null;
|
||||
|
||||
// Reset position
|
||||
player.transform.position = Vector3.zero;
|
||||
|
||||
// Load
|
||||
SaveManager.Load("smoke_test");
|
||||
yield return null;
|
||||
|
||||
// Verify
|
||||
Assert.AreEqual(100f, player.transform.position.x, 1f);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal
|
||||
|
||||
```cpp
|
||||
// SmokeTests.cpp
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FGameLaunchTest,
|
||||
"Smoke.Launch.MainMenu",
|
||||
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter
|
||||
)
|
||||
|
||||
bool FGameLaunchTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Verify main menu widget exists
|
||||
UWorld* World = GEngine->GetWorldContexts()[0].World();
|
||||
APlayerController* PC = World->GetFirstPlayerController();
|
||||
|
||||
TestNotNull("Player controller exists", PC);
|
||||
|
||||
// Check main menu is visible
|
||||
AMyHUD* HUD = Cast<AMyHUD>(PC->GetHUD());
|
||||
TestTrue("Main menu is visible", HUD->IsMainMenuVisible());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FPlayerMovementTest,
|
||||
"Smoke.Gameplay.Movement",
|
||||
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter
|
||||
)
|
||||
|
||||
bool FPlayerMovementTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
APawn* Player = GetTestPlayer();
|
||||
FVector StartPos = Player->GetActorLocation();
|
||||
|
||||
// Apply movement
|
||||
APlayerController* PC = Cast<APlayerController>(Player->GetController());
|
||||
PC->AddMovementInput(FVector::ForwardVector, 1.0f);
|
||||
|
||||
// Wait for physics
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FWaitForSeconds(0.5f));
|
||||
// Note: FVerifyPlayerMoved is a custom latent command - implement to verify player position changed
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FVerifyPlayerMoved(StartPos));
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Godot
|
||||
|
||||
```gdscript
|
||||
# test_smoke.gd
|
||||
extends GutTest
|
||||
|
||||
func test_game_launches():
|
||||
# Switch to main menu
|
||||
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||
await get_tree().process_frame
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
|
||||
# Verify main menu loaded
|
||||
var menu = get_tree().current_scene
|
||||
assert_not_null(menu, "Main menu should load")
|
||||
assert_eq(menu.name, "MainMenu", "Should be main menu scene")
|
||||
|
||||
func test_new_game_starts():
|
||||
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||
await get_tree().process_frame
|
||||
|
||||
# Find and click new game button
|
||||
var button = get_tree().current_scene.get_node("NewGameButton")
|
||||
button.pressed.emit()
|
||||
|
||||
await get_tree().create_timer(5.0).timeout
|
||||
|
||||
# Verify gameplay loaded
|
||||
var scene = get_tree().current_scene
|
||||
assert_eq(scene.name, "GameWorld", "Should load gameplay scene")
|
||||
|
||||
var player = scene.get_node("Player")
|
||||
assert_not_null(player, "Player should exist")
|
||||
|
||||
func test_player_can_move():
|
||||
get_tree().change_scene_to_file("res://scenes/game_world.tscn")
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
var player = get_tree().current_scene.get_node("Player")
|
||||
var start_pos = player.position
|
||||
|
||||
# Simulate input
|
||||
Input.action_press("move_right")
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
Input.action_release("move_right")
|
||||
|
||||
assert_gt(player.position.x, start_pos.x, "Player should have moved right")
|
||||
|
||||
func test_save_load_works():
|
||||
get_tree().change_scene_to_file("res://scenes/game_world.tscn")
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
var player = get_tree().current_scene.get_node("Player")
|
||||
player.position = Vector2(500, 300)
|
||||
|
||||
# Save
|
||||
SaveManager.save_game("smoke_test")
|
||||
await get_tree().process_frame
|
||||
|
||||
# Reset
|
||||
player.position = Vector2.ZERO
|
||||
|
||||
# Load
|
||||
SaveManager.load_game("smoke_test")
|
||||
await get_tree().process_frame
|
||||
|
||||
assert_almost_eq(player.position.x, 500.0, 1.0, "Position should restore")
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: Smoke Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build Game
|
||||
run: ./build.sh --configuration Release
|
||||
|
||||
- name: Run Smoke Tests
|
||||
run: |
|
||||
./game --headless --run-tests=Smoke --test-timeout=600
|
||||
|
||||
- name: Upload Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: smoke-test-results
|
||||
path: test-results/
|
||||
```
|
||||
|
||||
### Test Execution Order
|
||||
|
||||
1. **Build verification** - Binary exists and is valid
|
||||
2. **Launch test** - Game starts without crash
|
||||
3. **Menu navigation** - Can navigate to gameplay
|
||||
4. **Core loop** - Primary mechanic works
|
||||
5. **Save/Load** - Persistence functions
|
||||
6. **Cleanup** - No resource leaks
|
||||
|
||||
## Smoke Test Metrics
|
||||
|
||||
| Metric | Target | Action if Failed |
|
||||
| -------------- | -------- | ------------------ |
|
||||
| Pass rate | 100% | Block deployment |
|
||||
| Execution time | < 15 min | Optimize tests |
|
||||
| Flakiness | 0% | Fix or remove test |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Run smoke tests on every build
|
||||
- Keep tests fast and focused
|
||||
- Fail loudly and clearly
|
||||
- Test on target platform, not just dev environment
|
||||
- Include platform service connectivity
|
||||
- Run before any manual QA begins
|
||||
|
||||
### DON'T
|
||||
|
||||
- Include slow or flaky tests
|
||||
- Test edge cases or rare scenarios
|
||||
- Allow smoke test failures to ship
|
||||
- Skip smoke tests for "small" changes
|
||||
- Make smoke tests depend on external services
|
||||
- Let smoke suite grow beyond 15 minutes
|
||||
@@ -0,0 +1,271 @@
|
||||
# Test Priorities Matrix
|
||||
|
||||
## Overview
|
||||
|
||||
Not all tests are equal. This guide provides a framework for prioritizing test creation, execution, and maintenance based on risk, impact, and resources.
|
||||
|
||||
## Priority Levels
|
||||
|
||||
### P0 - Critical (Ship Blockers)
|
||||
|
||||
**Definition**: Failures that prevent the game from being playable or shippable.
|
||||
|
||||
| Criteria | Examples |
|
||||
| ---------------------- | ------------------------------ |
|
||||
| Game cannot start | Crash on launch, infinite load |
|
||||
| Core loop broken | Cannot perform primary action |
|
||||
| Data loss | Saves corrupted, progress lost |
|
||||
| Platform certification | TRC/XR failures |
|
||||
| Legal/compliance | Rating violations |
|
||||
|
||||
**Testing Approach**:
|
||||
|
||||
- Automated smoke tests on every build
|
||||
- Manual verification before any release
|
||||
- Zero tolerance for P0 bugs in release builds
|
||||
|
||||
### P1 - High (Major Features)
|
||||
|
||||
**Definition**: Significant functionality that affects most players' experience.
|
||||
|
||||
| Criteria | Examples |
|
||||
| --------------------- | -------------------------------- |
|
||||
| Major features broken | Multiplayer, progression systems |
|
||||
| Frequent player paths | Main story, common actions |
|
||||
| Significant UX issues | Confusing UI, missing feedback |
|
||||
| Performance problems | Unplayable frame rates |
|
||||
|
||||
**Testing Approach**:
|
||||
|
||||
- Comprehensive automated tests
|
||||
- Regular regression testing
|
||||
- Full coverage in release candidates
|
||||
|
||||
### P2 - Medium (Standard Features)
|
||||
|
||||
**Definition**: Important functionality that affects significant portions of gameplay.
|
||||
|
||||
| Criteria | Examples |
|
||||
| ------------------ | ----------------------------- |
|
||||
| Secondary features | Side quests, optional content |
|
||||
| Edge cases | Unusual player actions |
|
||||
| Platform-specific | Single-platform issues |
|
||||
| Minor progression | Non-critical collectibles |
|
||||
|
||||
**Testing Approach**:
|
||||
|
||||
- Selective automation
|
||||
- Milestone regression testing
|
||||
- Coverage prioritized by usage data
|
||||
|
||||
### P3 - Low (Polish Items)
|
||||
|
||||
**Definition**: Issues that are noticeable but don't significantly impact gameplay.
|
||||
|
||||
| Criteria | Examples |
|
||||
| -------------- | ------------------------------ |
|
||||
| Visual polish | Minor clipping, texture issues |
|
||||
| Audio polish | Volume inconsistencies |
|
||||
| Rare scenarios | Edge cases with workarounds |
|
||||
| Nice-to-have | QoL improvements |
|
||||
|
||||
**Testing Approach**:
|
||||
|
||||
- Manual exploratory testing
|
||||
- Automated only if easy/cheap
|
||||
- Fixed as time permits
|
||||
|
||||
## Risk-Based Prioritization
|
||||
|
||||
### Risk Factors
|
||||
|
||||
| Factor | High Risk | Low Risk |
|
||||
| ------------------- | ------------------- | --------------- |
|
||||
| Usage frequency | Core loop | Rarely accessed |
|
||||
| Player visibility | Always visible | Hidden/optional |
|
||||
| Data impact | Saves, progression | Cosmetic only |
|
||||
| Recovery difficulty | No workaround | Easy to retry |
|
||||
| Change frequency | Frequently modified | Stable code |
|
||||
|
||||
### Risk Assessment Matrix
|
||||
|
||||
```
|
||||
IMPACT
|
||||
Low High
|
||||
┌─────────┬─────────┐
|
||||
High │ P2 │ P0 │
|
||||
LIKELIHOOD ├─────────┼─────────┤
|
||||
Low │ P3 │ P1 │
|
||||
└─────────┴─────────┘
|
||||
```
|
||||
|
||||
## Coverage Targets by Priority
|
||||
|
||||
| Priority | Unit Test | Integration | E2E/Smoke | Manual |
|
||||
| -------- | --------- | ----------- | --------- | ----------- |
|
||||
| P0 | 100% | 100% | Required | Pre-release |
|
||||
| P1 | 80%+ | 80%+ | As needed | Milestone |
|
||||
| P2 | 60%+ | Key paths | Optional | Sprint |
|
||||
| P3 | Optional | Optional | No | Ad-hoc |
|
||||
|
||||
## Test Type Distribution
|
||||
|
||||
### Recommended Test Pyramid (Games)
|
||||
|
||||
```
|
||||
▲
|
||||
/│\
|
||||
/ │ \ E2E/Smoke Tests (5%)
|
||||
/ │ \ - Full game flow
|
||||
/ │ \ - Platform certification
|
||||
───────────
|
||||
/ │ \
|
||||
/ │ \ Integration Tests (25%)
|
||||
/ │ \ - System interactions
|
||||
/ │ \ - Network, save, audio
|
||||
─────────────────────
|
||||
/ │ \
|
||||
/ │ \ Unit Tests (70%)
|
||||
/ │ \ - Pure logic
|
||||
/ │ \- Algorithms, calculations
|
||||
───────────────────────────────
|
||||
```
|
||||
|
||||
### Game-Specific Considerations
|
||||
|
||||
Unlike web apps, games have unique testing needs:
|
||||
|
||||
| Test Type | Standard App | Game-Specific |
|
||||
| ----------- | -------------- | -------------------------- |
|
||||
| Unit | Business logic | Damage calc, AI decisions |
|
||||
| Integration | API + DB | Physics, audio, network |
|
||||
| E2E | User flows | Gameplay scenarios |
|
||||
| Additional | N/A | Playtesting, balance, feel |
|
||||
|
||||
## Execution Order
|
||||
|
||||
### CI Pipeline (Every Commit)
|
||||
|
||||
1. P0 smoke tests (5-10 minutes)
|
||||
2. P0/P1 unit tests (10-15 minutes)
|
||||
3. P0 integration tests (5-10 minutes)
|
||||
|
||||
### Daily/Nightly
|
||||
|
||||
1. Full P0 suite
|
||||
2. Full P1 suite
|
||||
3. P2 regression suite
|
||||
4. Performance benchmarks
|
||||
|
||||
### Milestone/Release
|
||||
|
||||
1. All automated tests
|
||||
2. Full P0-P2 manual testing
|
||||
3. Platform certification tests
|
||||
4. Exploratory testing
|
||||
5. Performance profiling
|
||||
|
||||
## Bug Triage Criteria
|
||||
|
||||
### Priority Assignment
|
||||
|
||||
| Question | P0 | P1 | P2 | P3 |
|
||||
| -------------------------- | ---- | --------- | ---- | ---- |
|
||||
| Can player complete game? | No | Affected | No | No |
|
||||
| How many players affected? | All | Most | Some | Few |
|
||||
| Is there a workaround? | No | Difficult | Yes | Easy |
|
||||
| Data at risk? | Yes | Possible | No | No |
|
||||
| Platform certification? | Fail | Risk | Pass | Pass |
|
||||
|
||||
### Severity vs Priority
|
||||
|
||||
```
|
||||
SEVERITY: How bad is the bug?
|
||||
Critical → Crash, data loss
|
||||
Major → Feature broken
|
||||
Minor → Incorrect behavior
|
||||
Trivial → Cosmetic
|
||||
|
||||
PRIORITY: How soon to fix?
|
||||
P0 → Immediately (blocks release)
|
||||
P1 → This sprint
|
||||
P2 → This milestone
|
||||
P3 → Backlog
|
||||
```
|
||||
|
||||
## Resource Allocation
|
||||
|
||||
### QA Time Distribution
|
||||
|
||||
| Activity | Percentage |
|
||||
| ---------------- | ---------- |
|
||||
| P0 verification | 30% |
|
||||
| P1 testing | 30% |
|
||||
| P2 testing | 20% |
|
||||
| Exploratory | 15% |
|
||||
| Test maintenance | 5% |
|
||||
|
||||
### Automation Investment
|
||||
|
||||
| Priority | Automation Value | ROI |
|
||||
| -------- | ---------------- | -------------- |
|
||||
| P0 | Essential | Highest |
|
||||
| P1 | High | High |
|
||||
| P2 | Medium | Medium |
|
||||
| P3 | Low | Often negative |
|
||||
|
||||
## Platform Priority Matrix
|
||||
|
||||
### Multi-Platform Prioritization
|
||||
|
||||
| Platform | Player Base | Certification | Testing Priority |
|
||||
| ----------------------- | ----------- | ------------- | ----------------- |
|
||||
| Primary (e.g., PC) | 60% | Light | P0: All, P1: All |
|
||||
| Secondary (e.g., PS5) | 25% | Heavy | P0: All, P1: Most |
|
||||
| Tertiary (e.g., Switch) | 15% | Medium | P0: All, P1: Core |
|
||||
|
||||
### Cross-Platform Testing Strategy
|
||||
|
||||
```
|
||||
Platform Testing Coverage
|
||||
|
||||
PC PS5 Xbox Switch Mobile
|
||||
P0 ████ ████ ████ ████ ████
|
||||
P1 ████ ███░ ███░ ██░░ ██░░
|
||||
P2 ███░ ██░░ ██░░ █░░░ █░░░
|
||||
P3 ██░░ █░░░ █░░░ ░░░░ ░░░░
|
||||
|
||||
████ = Full coverage
|
||||
███░ = High coverage
|
||||
██░░ = Medium coverage
|
||||
█░░░ = Low coverage
|
||||
░░░░ = Minimal/none
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Reassess priorities as development progresses
|
||||
- Weight user-facing features higher
|
||||
- Consider platform certification requirements
|
||||
- Focus automation on stable, high-value areas
|
||||
- Track bug escape rates by priority
|
||||
|
||||
### DON'T
|
||||
|
||||
- Treat all tests equally
|
||||
- Automate P3 before P0/P1 coverage is solid
|
||||
- Skip P0 testing for "small changes"
|
||||
- Ignore platform-specific requirements
|
||||
- Let P1/P2 bugs accumulate
|
||||
|
||||
## Metrics to Track
|
||||
|
||||
| Metric | Target | Purpose |
|
||||
| ------------------- | ------------- | -------------------- |
|
||||
| P0 test pass rate | 100% | Build quality |
|
||||
| P0 bug escape rate | 0% | Test effectiveness |
|
||||
| P1 coverage | 80%+ | Feature coverage |
|
||||
| Test execution time | < 30 min (CI) | Development velocity |
|
||||
| Flaky test rate | < 1% | Test reliability |
|
||||
@@ -0,0 +1,383 @@
|
||||
# Unity Test Framework Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Unity provides a built-in Test Framework based on NUnit for writing and running automated tests. It supports Edit Mode tests (run without playing) and Play Mode tests (run during gameplay simulation).
|
||||
|
||||
## Test Framework Setup
|
||||
|
||||
### Package Installation
|
||||
|
||||
```json
|
||||
// manifest.json - usually pre-installed
|
||||
{
|
||||
"dependencies": {
|
||||
"com.unity.test-framework": "1.6.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
Assets/
|
||||
├── Scripts/
|
||||
│ └── Runtime/
|
||||
│ ├── Player/
|
||||
│ │ └── PlayerController.cs
|
||||
│ └── Combat/
|
||||
│ └── DamageCalculator.cs
|
||||
└── Tests/
|
||||
├── EditMode/
|
||||
│ └── DamageCalculatorTests.cs
|
||||
└── PlayMode/
|
||||
└── PlayerMovementTests.cs
|
||||
```
|
||||
|
||||
### Assembly Definitions
|
||||
|
||||
Create `.asmdef` files for test assemblies:
|
||||
|
||||
```json
|
||||
// Tests/EditMode/EditModeTests.asmdef
|
||||
{
|
||||
"name": "EditModeTests",
|
||||
"references": ["GameAssembly"],
|
||||
"includePlatforms": ["Editor"],
|
||||
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
||||
}
|
||||
|
||||
// Tests/PlayMode/PlayModeTests.asmdef
|
||||
{
|
||||
"name": "PlayModeTests",
|
||||
"references": ["GameAssembly"],
|
||||
"includePlatforms": [],
|
||||
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
||||
}
|
||||
```
|
||||
|
||||
## Edit Mode Tests
|
||||
|
||||
Edit Mode tests run in the Editor without entering Play Mode. Best for testing pure logic.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```csharp
|
||||
using NUnit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class DamageCalculatorTests
|
||||
{
|
||||
private DamageCalculator _calculator;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_calculator = new DamageCalculator();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Calculate_BaseDamage_ReturnsCorrectValue()
|
||||
{
|
||||
float result = _calculator.Calculate(100f, 1f);
|
||||
Assert.AreEqual(100f, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Calculate_CriticalHit_DoublesDamage()
|
||||
{
|
||||
float result = _calculator.Calculate(100f, multiplier: 2f);
|
||||
Assert.AreEqual(200f, result);
|
||||
}
|
||||
|
||||
[TestCase(100f, 0.5f, 50f)]
|
||||
[TestCase(100f, 1.5f, 150f)]
|
||||
[TestCase(50f, 2f, 100f)]
|
||||
public void Calculate_Parameterized_ReturnsExpected(
|
||||
float base_, float mult, float expected)
|
||||
{
|
||||
Assert.AreEqual(expected, _calculator.Calculate(base_, mult));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing ScriptableObjects
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void WeaponStats_DPS_CalculatesCorrectly()
|
||||
{
|
||||
var weapon = ScriptableObject.CreateInstance<WeaponStats>();
|
||||
weapon.baseDamage = 10f;
|
||||
weapon.attacksPerSecond = 2f;
|
||||
|
||||
Assert.AreEqual(20f, weapon.DPS);
|
||||
|
||||
// Cleanup
|
||||
Object.DestroyImmediate(weapon);
|
||||
}
|
||||
```
|
||||
|
||||
## Play Mode Tests
|
||||
|
||||
Play Mode tests run during gameplay simulation. Required for testing MonoBehaviours, physics, and runtime behavior.
|
||||
|
||||
### Basic Play Mode Test
|
||||
|
||||
```csharp
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
public class PlayerMovementTests
|
||||
{
|
||||
private GameObject _player;
|
||||
private PlayerController _controller;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_player = new GameObject("Player");
|
||||
_controller = _player.AddComponent<PlayerController>();
|
||||
_player.AddComponent<Rigidbody>();
|
||||
_player.AddComponent<CapsuleCollider>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
Object.Destroy(_player);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Move_WhenInputApplied_ChangesPosition()
|
||||
{
|
||||
Vector3 startPos = _player.transform.position;
|
||||
|
||||
_controller.SetInput(Vector2.right);
|
||||
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
|
||||
Assert.Greater(_player.transform.position.x, startPos.x);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Jump_WhenGrounded_BecomesAirborne()
|
||||
{
|
||||
// Setup ground
|
||||
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
||||
_player.transform.position = Vector3.up;
|
||||
|
||||
yield return new WaitForFixedUpdate();
|
||||
|
||||
_controller.Jump();
|
||||
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
Assert.IsFalse(_controller.IsGrounded);
|
||||
|
||||
Object.Destroy(ground);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Coroutines
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator Attack_Cooldown_PreventsSpam()
|
||||
{
|
||||
_controller.Attack();
|
||||
Assert.IsTrue(_controller.IsAttacking);
|
||||
|
||||
_controller.Attack(); // Should be blocked
|
||||
Assert.AreEqual(1, _controller.AttackCount);
|
||||
|
||||
yield return new WaitForSeconds(_controller.AttackCooldown + 0.1f);
|
||||
|
||||
_controller.Attack();
|
||||
Assert.AreEqual(2, _controller.AttackCount);
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Testing
|
||||
|
||||
```csharp
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator MainMenu_StartButton_LoadsGameScene()
|
||||
{
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
yield return null; // Wait for scene load
|
||||
|
||||
var startButton = GameObject.Find("StartButton")
|
||||
.GetComponent<Button>();
|
||||
startButton.onClick.Invoke();
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
Assert.AreEqual("GameScene", SceneManager.GetActiveScene().name);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
### Prefab Testing
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator EnemyPrefab_Spawns_WithCorrectComponents()
|
||||
{
|
||||
var prefab = Resources.Load<GameObject>("Prefabs/Enemy");
|
||||
var instance = Object.Instantiate(prefab);
|
||||
|
||||
yield return null;
|
||||
|
||||
Assert.IsNotNull(instance.GetComponent<EnemyAI>());
|
||||
Assert.IsNotNull(instance.GetComponent<Health>());
|
||||
Assert.IsNotNull(instance.GetComponent<NavMeshAgent>());
|
||||
|
||||
Object.Destroy(instance);
|
||||
}
|
||||
```
|
||||
|
||||
### Input System Testing
|
||||
|
||||
```csharp
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator InputAction_Fire_TriggersWeapon()
|
||||
{
|
||||
var keyboard = InputSystem.AddDevice<Keyboard>();
|
||||
|
||||
yield return null;
|
||||
|
||||
Press(keyboard.spaceKey);
|
||||
yield return null;
|
||||
|
||||
Assert.IsTrue(_controller.IsFiring);
|
||||
|
||||
Release(keyboard.spaceKey);
|
||||
yield return null;
|
||||
|
||||
Assert.IsFalse(_controller.IsFiring);
|
||||
}
|
||||
```
|
||||
|
||||
## Test Utilities
|
||||
|
||||
### Custom Assertions
|
||||
|
||||
```csharp
|
||||
public static class GameAssert
|
||||
{
|
||||
public static void AreApproximatelyEqual(
|
||||
Vector3 expected, Vector3 actual, float tolerance = 0.001f)
|
||||
{
|
||||
Assert.AreEqual(expected.x, actual.x, tolerance);
|
||||
Assert.AreEqual(expected.y, actual.y, tolerance);
|
||||
Assert.AreEqual(expected.z, actual.z, tolerance);
|
||||
}
|
||||
|
||||
public static void IsWithinRange(float value, float min, float max)
|
||||
{
|
||||
Assert.GreaterOrEqual(value, min);
|
||||
Assert.LessOrEqual(value, max);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
```csharp
|
||||
public class TestScene : IDisposable
|
||||
{
|
||||
public GameObject Player { get; private set; }
|
||||
public GameObject Ground { get; private set; }
|
||||
|
||||
public TestScene()
|
||||
{
|
||||
Ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
||||
Player = Object.Instantiate(Resources.Load<GameObject>("Player"));
|
||||
Player.transform.position = Vector3.up;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Object.Destroy(Player);
|
||||
Object.Destroy(Ground);
|
||||
}
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Player_FallsToGround()
|
||||
{
|
||||
using var scene = new TestScene();
|
||||
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
Assert.Less(scene.Player.transform.position.y, 0.5f);
|
||||
}
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
### Command Line Execution
|
||||
|
||||
```bash
|
||||
# Run Edit Mode tests
|
||||
Unity -runTests -batchmode -projectPath . \
|
||||
-testPlatform EditMode \
|
||||
-testResults results.xml
|
||||
|
||||
# Run Play Mode tests
|
||||
Unity -runTests -batchmode -projectPath . \
|
||||
-testPlatform PlayMode \
|
||||
-testResults results.xml
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: game-ci/unity-test-runner@v4
|
||||
with:
|
||||
projectPath: .
|
||||
testMode: all
|
||||
artifactsPath: TestResults
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Test pure logic in Edit Mode (faster execution)
|
||||
- Use Play Mode only when needed (physics, coroutines, MonoBehaviour)
|
||||
- Create test fixtures for common setups
|
||||
- Clean up created GameObjects in TearDown
|
||||
- Use `[Category]` attributes for test organization
|
||||
- Run tests before every commit
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't test Unity's built-in functionality
|
||||
- Don't rely on specific frame timing (use WaitForSeconds)
|
||||
- Don't leave test objects in scenes
|
||||
- Don't test private methods directly (test through public API)
|
||||
- Don't create tests that depend on execution order
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
| ---------------------- | ------------------ | ------------------------------------------ |
|
||||
| Tests not appearing | Missing asmdef | Create test assembly definition |
|
||||
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
||||
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
||||
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
||||
@@ -0,0 +1,388 @@
|
||||
# Unreal Engine Automation Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Unreal Engine provides a comprehensive automation system for testing games, including:
|
||||
|
||||
- **Automation Framework** - Low-level test infrastructure
|
||||
- **Functional Tests** - In-game scenario testing
|
||||
- **Gauntlet** - Extended testing and automation
|
||||
|
||||
## Automation Framework
|
||||
|
||||
### Test Types
|
||||
|
||||
| Type | Flag | Use Case |
|
||||
| ------------- | --------------- | -------------------------- |
|
||||
| Unit Tests | `SmokeFilter` | Fast, isolated logic tests |
|
||||
| Feature Tests | `ProductFilter` | Feature validation |
|
||||
| Stress Tests | `StressFilter` | Performance under load |
|
||||
| Perf Tests | `PerfFilter` | Benchmark comparisons |
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```cpp
|
||||
// MyGameTests.cpp
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDamageCalculationTest,
|
||||
"MyGame.Combat.DamageCalculation",
|
||||
EAutomationTestFlags::ApplicationContextMask |
|
||||
EAutomationTestFlags::ProductFilter
|
||||
)
|
||||
|
||||
bool FDamageCalculationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Arrange
|
||||
float BaseDamage = 100.f;
|
||||
float CritMultiplier = 2.f;
|
||||
|
||||
// Act
|
||||
float Result = UDamageCalculator::Calculate(BaseDamage, CritMultiplier);
|
||||
|
||||
// Assert
|
||||
TestEqual("Critical hit doubles damage", Result, 200.f);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Test with Setup/Teardown
|
||||
|
||||
```cpp
|
||||
IMPLEMENT_COMPLEX_AUTOMATION_TEST(
|
||||
FInventorySystemTest,
|
||||
"MyGame.Systems.Inventory",
|
||||
EAutomationTestFlags::ApplicationContextMask |
|
||||
EAutomationTestFlags::ProductFilter
|
||||
)
|
||||
|
||||
void FInventorySystemTest::GetTests(
|
||||
TArray<FString>& OutBeautifiedNames,
|
||||
TArray<FString>& OutTestCommands) const
|
||||
{
|
||||
OutBeautifiedNames.Add("AddItem");
|
||||
OutTestCommands.Add("AddItem");
|
||||
|
||||
OutBeautifiedNames.Add("RemoveItem");
|
||||
OutTestCommands.Add("RemoveItem");
|
||||
|
||||
OutBeautifiedNames.Add("StackItems");
|
||||
OutTestCommands.Add("StackItems");
|
||||
}
|
||||
|
||||
bool FInventorySystemTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Setup
|
||||
UInventoryComponent* Inventory = NewObject<UInventoryComponent>();
|
||||
|
||||
if (Parameters == "AddItem")
|
||||
{
|
||||
UItemData* Sword = NewObject<UItemData>();
|
||||
Sword->ItemID = "sword_01";
|
||||
|
||||
bool bAdded = Inventory->AddItem(Sword);
|
||||
|
||||
TestTrue("Item added successfully", bAdded);
|
||||
TestEqual("Inventory count", Inventory->GetItemCount(), 1);
|
||||
}
|
||||
else if (Parameters == "RemoveItem")
|
||||
{
|
||||
// ... test logic
|
||||
}
|
||||
else if (Parameters == "StackItems")
|
||||
{
|
||||
// ... test logic
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Latent Actions (Async Tests)
|
||||
|
||||
```cpp
|
||||
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(
|
||||
FWaitForActorSpawn,
|
||||
FString, ActorName
|
||||
);
|
||||
|
||||
bool FWaitForActorSpawn::Update()
|
||||
{
|
||||
UWorld* World = GEngine->GetWorldContexts()[0].World();
|
||||
AActor* Actor = nullptr;
|
||||
|
||||
for (TActorIterator<AActor> It(World); It; ++It)
|
||||
{
|
||||
if (It->GetName() == ActorName)
|
||||
{
|
||||
Actor = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Actor != nullptr; // Return true when complete
|
||||
}
|
||||
|
||||
bool FSpawnTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Spawn enemy
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FSpawnEnemy("Goblin"));
|
||||
|
||||
// Wait for spawn
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FWaitForActorSpawn("Goblin"));
|
||||
|
||||
// Verify
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FVerifyEnemyState("Goblin", "Idle"));
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Functional Tests
|
||||
|
||||
Functional tests run inside the game world and can test gameplay scenarios.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Create a test map (`TestMap_Combat.umap`)
|
||||
2. Add `AFunctionalTest` actors to the map
|
||||
3. Configure test parameters in Details panel
|
||||
|
||||
### Blueprint Functional Test
|
||||
|
||||
```cpp
|
||||
// In Blueprint:
|
||||
// 1. Create child of AFunctionalTest
|
||||
// 2. Override "Start Test" event
|
||||
// 3. Call "Finish Test" when complete
|
||||
```
|
||||
|
||||
### C++ Functional Test
|
||||
|
||||
```cpp
|
||||
UCLASS()
|
||||
class APlayerCombatTest : public AFunctionalTest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void StartTest() override;
|
||||
|
||||
protected:
|
||||
UPROPERTY(EditAnywhere)
|
||||
TSubclassOf<AEnemy> EnemyClass;
|
||||
|
||||
UPROPERTY(EditAnywhere)
|
||||
float ExpectedDamage = 50.f;
|
||||
|
||||
private:
|
||||
void OnEnemyDamaged(float Damage);
|
||||
};
|
||||
|
||||
void APlayerCombatTest::StartTest()
|
||||
{
|
||||
Super::StartTest();
|
||||
|
||||
// Spawn test enemy
|
||||
AEnemy* Enemy = GetWorld()->SpawnActor<AEnemy>(EnemyClass);
|
||||
Enemy->OnDamaged.AddDynamic(this, &APlayerCombatTest::OnEnemyDamaged);
|
||||
|
||||
// Get player and attack
|
||||
APlayerCharacter* Player = Cast<APlayerCharacter>(
|
||||
UGameplayStatics::GetPlayerCharacter(this, 0));
|
||||
Player->Attack(Enemy);
|
||||
}
|
||||
|
||||
void APlayerCombatTest::OnEnemyDamaged(float Damage)
|
||||
{
|
||||
if (FMath::IsNearlyEqual(Damage, ExpectedDamage, 0.1f))
|
||||
{
|
||||
FinishTest(EFunctionalTestResult::Succeeded, "Damage correct");
|
||||
}
|
||||
else
|
||||
{
|
||||
FinishTest(EFunctionalTestResult::Failed,
|
||||
FString::Printf(TEXT("Expected %f, got %f"),
|
||||
ExpectedDamage, Damage));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gauntlet Framework
|
||||
|
||||
Gauntlet extends automation for large-scale testing, performance benchmarking, and multi-client scenarios.
|
||||
|
||||
### Gauntlet Test Configuration
|
||||
|
||||
```cpp
|
||||
// MyGameTest.cs (Gauntlet config)
|
||||
namespace MyGame.Automation
|
||||
{
|
||||
public class PerformanceTestConfig : UnrealTestConfig
|
||||
{
|
||||
[AutoParam]
|
||||
public string MapName = "TestMap_Performance";
|
||||
|
||||
[AutoParam]
|
||||
public int Duration = 300; // 5 minutes
|
||||
|
||||
public override void ApplyToConfig(UnrealAppConfig Config)
|
||||
{
|
||||
base.ApplyToConfig(Config);
|
||||
Config.AddCmdLineArg("-game");
|
||||
Config.AddCmdLineArg($"-ExecCmds=open {MapName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running Gauntlet
|
||||
|
||||
```bash
|
||||
# Run performance test
|
||||
RunUAT.bat RunUnreal -project=MyGame -platform=Win64 \
|
||||
-configuration=Development -build=local \
|
||||
-test=MyGame.PerformanceTest -Duration=300
|
||||
```
|
||||
|
||||
## Blueprint Testing
|
||||
|
||||
### Test Helpers in Blueprint
|
||||
|
||||
Create a Blueprint Function Library with test utilities:
|
||||
|
||||
```cpp
|
||||
UCLASS()
|
||||
class UTestHelpers : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Testing")
|
||||
static void AssertTrue(bool Condition, const FString& Message);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Testing")
|
||||
static void AssertEqual(int32 A, int32 B, const FString& Message);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Testing")
|
||||
static AActor* SpawnTestActor(
|
||||
UObject* WorldContext,
|
||||
TSubclassOf<AActor> ActorClass,
|
||||
FVector Location);
|
||||
};
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Frame Time Measurement
|
||||
|
||||
```cpp
|
||||
bool FFrameTimeTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TArray<float> FrameTimes;
|
||||
float TotalTime = 0.f;
|
||||
|
||||
// Collect frame times
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FCollectFrameTimes(
|
||||
FrameTimes, 1000 // frames
|
||||
));
|
||||
|
||||
// Analyze
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FAnalyzeFrameTimes(
|
||||
FrameTimes,
|
||||
16.67f, // Target: 60fps
|
||||
0.99f // 99th percentile threshold
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Tracking
|
||||
|
||||
```cpp
|
||||
bool FMemoryLeakTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
SIZE_T BaselineMemory = FPlatformMemory::GetStats().UsedPhysical;
|
||||
|
||||
// Perform operations
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
UObject* Obj = NewObject<UMyObject>();
|
||||
// ... use object
|
||||
Obj->MarkAsGarbage(); // UE5 API (was MarkPendingKill in UE4)
|
||||
}
|
||||
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
|
||||
SIZE_T FinalMemory = FPlatformMemory::GetStats().UsedPhysical;
|
||||
SIZE_T Leaked = FinalMemory - BaselineMemory;
|
||||
|
||||
TestTrue("No significant leak", Leaked < 1024 * 1024); // 1MB tolerance
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
# Run all tests (UE5)
|
||||
UnrealEditor.exe MyGame -ExecCmds="Automation RunTests Now" -unattended -nopause
|
||||
|
||||
# Run specific test
|
||||
UnrealEditor.exe MyGame -ExecCmds="Automation RunTests MyGame.Combat" -unattended
|
||||
|
||||
# Run with report
|
||||
UnrealEditor.exe MyGame \
|
||||
-ExecCmds="Automation RunTests Now; Automation ReportResults" \
|
||||
-ReportOutputPath=TestResults.xml
|
||||
|
||||
# Note: For UE4, use UE4Editor.exe instead of UnrealEditor.exe
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
test:
|
||||
runs-on: [self-hosted, windows, unreal]
|
||||
steps:
|
||||
- name: Run Tests
|
||||
run: |
|
||||
# UE5: UnrealEditor-Cmd.exe, UE4: UE4Editor-Cmd.exe
|
||||
& "$env:UE_ROOT/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" `
|
||||
"${{ github.workspace }}/MyGame.uproject" `
|
||||
-ExecCmds="Automation RunTests Now" `
|
||||
-unattended -nopause -nullrhi
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use `SmokeFilter` for fast CI tests
|
||||
- Create dedicated test maps for functional tests
|
||||
- Clean up spawned actors after tests
|
||||
- Use latent commands for async operations
|
||||
- Profile tests to keep CI fast
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't test engine functionality
|
||||
- Don't rely on specific tick order
|
||||
- Don't leave test actors in production maps
|
||||
- Don't ignore test warnings
|
||||
- Don't skip garbage collection in tests
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
| -------------- | --------------- | ---------------------------- |
|
||||
| Test not found | Wrong flags | Check `EAutomationTestFlags` |
|
||||
| Crash in test | Missing world | Use proper test context |
|
||||
| Flaky results | Timing issues | Use latent commands |
|
||||
| Slow tests | Too many actors | Optimize test setup |
|
||||
Reference in new issue
Block a user