Quick answer: A regression test is an automated test that verifies a previously fixed bug has not been reintroduced by subsequent code changes. When a bug is found and fixed, a regression test is written that reproduces the original bug conditions and asserts the correct behavior.

Learning how to write regression tests for game bugs is a common challenge for game developers. You fixed the inventory duplication bug three sprints ago. This week, a player found it again. Different code path, same result — items cloned when dragging between containers. The fix did not regress because someone reverted it; it regressed because a new feature touched the same transfer logic and nobody noticed. A regression test would have caught it in CI before it ever reached a build. This article covers how to write regression tests specifically for game bugs, with practical examples in Unity, Unreal, and Godot.

From Bug Report to Test Case

Every regression test starts with a bug report. The report contains the three things you need: preconditions (what state the game was in), actions (what the player did), and the unexpected result. These map directly to the Arrange-Act-Assert pattern used in testing.

Consider this bug report:

Title: Items duplicated when transferring between inventory and chest.

Steps to reproduce: 1. Open a chest with at least one item. 2. Drag an item from the chest to the player inventory while the inventory is full. 3. The item appears in both the chest and the inventory.

Expected result: The item stays in the chest because the inventory is full.

The test writes itself from this report. Arrange: create an inventory at max capacity and a chest with one item. Act: attempt to transfer the item from chest to inventory. Assert: the item count in the chest is unchanged and the inventory count is unchanged.

Writing the Test in Unity

Unity’s Test Framework supports EditMode tests (no scene required, runs instantly) and PlayMode tests (runs in a live scene over multiple frames). For gameplay logic, PlayMode tests are usually necessary because they simulate the game loop.

using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using System.Collections;

public class InventoryRegressionTests
{
    // Regression test for BUG-1042: item duplication on full inventory transfer
    [UnityTest]
    public IEnumerator Transfer_ToFullInventory_DoesNotDuplicateItem()
    {
        // Arrange: create inventory at max capacity
        var inventory = new Inventory(maxSlots: 10);
        for (int i = 0; i < 10; i++)
        {
            inventory.AddItem(new Item("filler_" + i));
        }

        // Arrange: create chest with one item
        var chest = new Chest();
        var sword = new Item("iron_sword");
        chest.AddItem(sword);

        yield return null; // Wait one frame for initialization

        // Act: attempt to transfer from chest to full inventory
        bool result = inventory.TransferFrom(chest, sword);

        // Assert: transfer should fail, item counts unchanged
        Assert.IsFalse(result, "Transfer should fail when inventory is full");
        Assert.AreEqual(10, inventory.ItemCount(),
            "Inventory count should remain at max");
        Assert.AreEqual(1, chest.ItemCount(),
            "Chest should still contain the item");
        Assert.IsTrue(chest.Contains(sword),
            "Sword should remain in chest");
    }
}

The key practice here is to name the test after the bug or the expected behavior, not the implementation. Transfer_ToFullInventory_DoesNotDuplicateItem tells you exactly what scenario is being guarded against, even if the internal transfer logic changes completely.

Writing the Test in Unreal

Unreal’s Automation Framework uses IMPLEMENT_SIMPLE_AUTOMATION_TEST for synchronous tests and latent automation tests for multi-frame scenarios. For gameplay regression tests, latent tests are common because you often need to simulate multiple frames of gameplay.

// Unreal Automation Test for inventory transfer regression
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
    FInventoryTransferFullTest,
    "Game.Inventory.TransferToFullInventory.DoesNotDuplicate",
    EAutomationTestFlags::ApplicationContextMask |
    EAutomationTestFlags::ProductFilter)

bool FInventoryTransferFullTest::RunTest(
    const FString& Parameters)
{
    // Arrange
    UInventoryComponent* Inventory = NewObject<UInventoryComponent>();
    Inventory->MaxSlots = 10;
    for (int32 i = 0; i < 10; i++)
    {
        Inventory->AddItem(
            FName(*FString::Printf(TEXT("Filler_%d"), i)), 1);
    }

    UChestComponent* Chest = NewObject<UChestComponent>();
    Chest->AddItem(FName("IronSword"), 1);

    // Act
    bool bResult = Inventory->TransferFrom(
        Chest, FName("IronSword"), 1);

    // Assert
    TestFalse(TEXT("Transfer should fail"), bResult);
    TestEqual(TEXT("Inventory count"),
        Inventory->GetItemCount(), 10);
    TestEqual(TEXT("Chest count"),
        Chest->GetItemCount(), 1);

    return true;
}

Writing the Test in Godot

Godot uses GDScript and the GUT (Godot Unit Testing) framework or the built-in test runner. For regression tests, you write test scripts that set up game state, execute actions, and check results.

# test_inventory_regression.gd
extends GutTest

func test_transfer_to_full_inventory_does_not_duplicate() -> void:
    # Arrange: create full inventory
    var inventory := Inventory.new(10)
    for i in range(10):
        inventory.add_item(Item.new("filler_%d" % i))

    # Arrange: create chest with one item
    var chest := Chest.new()
    var sword := Item.new("iron_sword")
    chest.add_item(sword)

    # Act: attempt transfer
    var result := inventory.transfer_from(chest, sword)

    # Assert
    assert_false(result, "Transfer should fail when inventory is full")
    assert_eq(inventory.item_count(), 10,
        "Inventory should remain full")
    assert_eq(chest.item_count(), 1,
        "Chest should still have the item")
    assert_true(chest.contains(sword),
        "Sword should remain in chest")

Testing Physics and Movement Regressions

Physics bugs are notoriously hard to regression-test because physics simulations are non-deterministic across different machines and frame rates. However, you can test for the observable outcomes rather than exact positions.

For example, if a bug report says “player falls through the floor at coordinates (100, 0, 200),” the regression test should verify that a character placed at those coordinates remains above the floor after a few seconds of simulation.

// Unity: test that the player does not fall through the floor
[UnityTest]
public IEnumerator Player_AtKnownProblemLocation_DoesNotFallThrough()
{
    // Arrange: load the scene and place the player
    yield return SceneManager.LoadSceneAsync("Level_01");
    var player = GameObject.FindWithTag("Player");
    player.transform.position = new Vector3(100f, 2f, 200f);

    // Act: simulate 3 seconds of physics
    float elapsed = 0f;
    while (elapsed < 3f)
    {
        elapsed += Time.deltaTime;
        yield return null;
    }

    // Assert: player should be above the floor (y > -1)
    Assert.Greater(player.transform.position.y, -1f,
        "Player fell through floor at (100, 0, 200)");
}
Write regression tests that verify outcomes, not exact values. A test that checks the player’s Y position is above zero is more robust than one that checks the position is exactly 1.0, because physics simulations produce slightly different results on different hardware.

Integrating with CI

Regression tests are only useful if they run automatically on every commit. Integrate them into your CI pipeline so that a failing test blocks the build from shipping.

For Unity, the Test Framework can run from the command line:

# Run Unity PlayMode tests in CI
unity-editor \
  -batchmode \
  -nographics \
  -runTests \
  -testPlatform PlayMode \
  -testResults results.xml \
  -logFile unity.log

# Parse results
if grep -q "result=\"Failed\"" results.xml; then
  echo "Regression tests failed!"
  exit 1
fi

For Unreal, the Automation Framework runs via command line:

# Run Unreal automation tests in CI
UnrealEditor-Cmd MyProject.uproject \
  -ExecCmds="Automation RunTests Game.Inventory; Quit" \
  -NullRHI \
  -NoSound \
  -log

For Godot with GUT:

# Run Godot tests in CI
godot --headless -s addons/gut/gut_cmdln.gd \
  -gdir=res://tests/ \
  -gexit

The key is to run these tests on every pull request and block merging if any regression test fails. A regression test that does not run in CI is a test that will be forgotten.

What to Test and What Not to Test

Not every bug needs a regression test. Focus your effort on:

High-severity bugs that cause crashes, data loss, or broken progression. These are the bugs that drive the worst player reviews.

Bugs that have regressed before. If a bug has come back once, it will come back again. These are the highest-value regression tests.

Bugs in core systems. Combat, inventory, save/load, networking, and progression systems are touched by many developers and change frequently. Regression tests here have the highest chance of catching real regressions.

Skip regression tests for visual-only bugs (a particle effect slightly off-color), one-off content bugs (a misplaced prop in a single level), and bugs in systems that are being completely rewritten. These tests either cannot be automated meaningfully or will be immediately obsolete.

Related Issues

For setting up a structured bug tracking workflow that feeds into your testing process, see our guide on bug tracking workflow for indie studios. If you are dealing with platform-specific bugs that need targeted regression tests, check tracking bugs across multiple platforms for organization strategies.

Write the test before the fix. If the test passes before you fix the bug, the test is not actually catching the bug.