BMAD-METHOD/src/modules/bmgd/gametest/knowledge/save-testing.md

8.0 KiB

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

[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

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

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