Quick answer: Regression testing is the practice of re-testing previously fixed bugs and core gameplay systems after every code change to ensure that new updates have not reintroduced old problems.
This guide covers regression testing strategies for indie games to strengthen your development process. You fixed the inventory duplication bug two patches ago. Players confirmed it was gone. Then last week, after a seemingly unrelated change to the crafting system, the bug came back. Players are furious, reviews mention it by name, and you have no idea which commit reintroduced it. This is a regression — a fixed bug that returns — and it is one of the most damaging things that can happen to player trust. Here is how to build a regression testing strategy that catches these problems before your players do.
Why Regressions Happen in Games
Game code is deeply interconnected. The inventory system touches the save system, which touches the serialization layer, which touches the crafting system. A change to how crafting recipes are stored can subtly break inventory slot assignment logic that was fixed three months ago. Unlike web applications where features are often isolated behind API boundaries, game systems share state through scene trees, global singletons, and physics engines in ways that make unintended side effects common.
The most frequent causes of regressions in indie games are: refactoring shared utility code that multiple systems depend on, merging feature branches that were developed against an older version of the codebase, changing serialization formats without updating all consumers, and modifying physics or collision parameters that affect multiple gameplay systems simultaneously.
The Smoke Test Suite
A smoke test suite is a small, fast set of tests that verify the most critical paths in your game still work. It should run in under five minutes and answer one question: is this build fundamentally broken? If the smoke suite passes, the build is worth testing further. If it fails, something critical is wrong and the build should not go to players.
Every indie game's smoke suite should cover at minimum:
Launch and load: The game starts without crashing and reaches the main menu. A save file from the current version loads successfully. A new game can be started.
Core loop: The primary gameplay mechanic works. If your game is a platformer, the character can move, jump, and land. If it is an RPG, the player can enter combat and deal damage. Test the one thing players do most often.
Scene transitions: The player can move between at least two major areas without crashing. Scene transitions are a common source of null reference crashes when nodes are freed before dependent code finishes executing.
Save and load round-trip: The game can save, quit, relaunch, and load the save with the player in the correct state. This catches serialization regressions early.
Automated Testing with Unity Test Runner
Unity's built-in Test Framework supports two modes. Edit Mode tests run outside of Play Mode and are ideal for testing pure logic: damage calculations, inventory operations, pathfinding algorithms. Play Mode tests run inside a live scene and can test runtime behavior like physics interactions, UI flows, and scene loading.
Here is a regression test for an inventory duplication bug in Unity:
using NUnit.Framework;
[TestFixture]
public class InventoryRegressionTests {
[Test]
public void AddItem_DoesNotDuplicateWhenSlotIsFull() {
// Regression test for bug #142: items duplicated when
// adding to a full inventory slot
var inventory = new Inventory(maxSlots: 10);
var sword = new Item("Sword", stackable: false);
inventory.AddItem(sword);
inventory.AddItem(sword); // Attempt to add duplicate
Assert.AreEqual(1, inventory.GetItemCount("Sword"));
}
[Test]
public void CraftingDoesNotAffectInventorySlotOrder() {
// Regression test for bug #198: crafting shuffled
// inventory slot positions
var inventory = new Inventory(maxSlots: 10);
inventory.AddItem(new Item("Wood", stackable: true), slot: 0);
inventory.AddItem(new Item("Stone", stackable: true), slot: 1);
CraftingSystem.Craft(inventory, "Pickaxe");
Assert.AreEqual("Pickaxe", inventory.GetItemAt(0).Name);
}
}
The key practice: every time you fix a bug, write a test that reproduces the original failure condition. Reference the bug number in a comment. That test becomes a permanent guard against the specific regression. Over time, your test suite grows into a living record of every bug your game has ever had.
Automated Testing with GUT for Godot
GUT (Godot Unit Test) is the standard testing framework for Godot projects. It provides assertion methods, setup and teardown hooks, and the ability to instantiate and test scenes. Install it as an addon from the Godot Asset Library.
extends GutTest
# Regression test for bug #87: health dropping below zero
# caused a division-by-zero crash in the damage display
func test_health_does_not_go_below_zero():
var player = preload("res://scenes/player.tscn").instantiate()
add_child(player)
player.health = 10
player.take_damage(25) # Damage exceeds health
assert_eq(player.health, 0, "Health should clamp at zero")
assert_true(player.is_alive == false, "Player should be dead")
player.queue_free()
# Regression test for bug #103: scene transition lost player
# inventory when autoload was reloaded
func test_inventory_persists_across_scene_change():
var game_state = preload("res://autoload/game_state.gd").new()
game_state.add_item("sword")
# Simulate scene change by calling the transition method
game_state.prepare_for_scene_change()
game_state.on_scene_loaded()
assert_true(game_state.has_item("sword"), "Inventory should survive scene transition")
GUT tests can be run from the editor or from the command line, which makes them easy to integrate into CI pipelines. Run your GUT suite with godot --headless --script addons/gut/gut_cmdln.gd in your CI configuration.
Save File Compatibility Testing
Save file regressions are among the most painful for players because they destroy progress. Every time you change your save format — adding new fields, renaming properties, changing data structures — you risk breaking compatibility with saves from previous versions.
The strategy is straightforward: keep a collection of save files from every released version of your game. Store them in your version control repository in a test/fixtures/saves/ directory. After each update, run a test that loads every historical save file and verifies it either loads correctly or migrates gracefully.
// C#: Save file compatibility test
[Test]
[TestCase("v0.5.0_forest_checkpoint.sav")]
[TestCase("v0.6.0_boss_fight.sav")]
[TestCase("v0.7.2_endgame.sav")]
[TestCase("v0.8.0_new_game_plus.sav")]
public void OldSaveFile_LoadsWithoutCrash(string saveFileName) {
var path = Path.Combine(TestFixturePath, saveFileName);
var saveData = SaveSystem.Load(path);
Assert.IsNotNull(saveData, $"Save file {saveFileName} should load");
Assert.IsTrue(saveData.PlayerHealth > 0, "Player health should be valid");
Assert.IsNotEmpty(saveData.CurrentScene, "Current scene should be set");
}
This approach catches migration bugs before they reach players. If a save from version 0.5.0 fails to load on your current build, you will know immediately rather than finding out from a player who has lost thirty hours of progress.
Version-Gated Tests
Some bugs only appear in specific contexts: a particular game version, a specific platform, or after a certain sequence of patches has been applied. Version-gated tests let you run different test logic depending on the build version or platform.
# GDScript: Platform-specific regression test
func test_audio_playback_on_web_export():
if OS.get_name() != "Web":
pending("Skipping: web-only regression test")
return
# Bug #156: AudioStreamPlayer didn't play on web due to
# browser autoplay policy not being handled
var player = AudioStreamPlayer.new()
add_child(player)
player.stream = preload("res://audio/test_beep.ogg")
player.play()
assert_true(player.playing, "Audio should be playing on web")
player.queue_free()
CI Integration for Automated Builds and Test Runs
Running tests manually is better than not running them at all, but the real power comes from running them automatically on every commit. A CI pipeline that builds your game and runs your test suite on every push ensures that no regression makes it into your main branch without being caught.
Here is a minimal GitHub Actions workflow for a Godot project with GUT tests:
# .github/workflows/regression-tests.yml
name: Regression Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
container:
image: barichello/godot-ci:4.3
steps:
- uses: actions/checkout@v4
- name: Run GUT tests
run: godot --headless --script addons/gut/gut_cmdln.gd -gexit
For Unity, use the game-ci/unity-test-runner action which handles license activation and runs both Edit Mode and Play Mode tests. The key configuration is setting testMode: all and ensuring your test assemblies are included in the build.
Block merges to your main branch when tests fail. This is the enforcement mechanism that makes the entire system work. Without it, developers will merge "just this once" with failing tests, and the test suite will rot within weeks.
The Regression Spreadsheet for Manual Testing
Not everything can be automated. Visual regressions, audio glitches, "feel" issues in controls, and complex multiplayer interactions are difficult or impossible to capture in automated tests. For these, maintain a manual regression checklist — a spreadsheet that your team or playtesters run through before every release.
Structure the spreadsheet with columns for: test case description, steps to perform, expected result, actual result, pass/fail, tester name, and date. Group test cases by system: combat, inventory, UI, audio, save/load, scene transitions. Add a new row every time you fix a bug that cannot be tested automatically.
The spreadsheet should take no more than 30 to 45 minutes to complete. If it grows beyond that, you have two options: automate more of the tests, or split the checklist into "every release" and "every major release" tiers. The core smoke tests run every time. The exhaustive list runs before milestones and public releases.
"A bug you fixed once is an anecdote. A bug you wrote a test for is a promise to your players that it will never come back."
Related Issues
For a comprehensive pre-release testing checklist that goes beyond regression testing, our guide on building a QA checklist for game release covers the full process. If you want to take your testing further with automated playtesting and bot-driven coverage, see the article on automated QA testing for indie game studios.
Every fixed bug deserves a test. Your future self will thank you when that bug tries to come back.