QA Pipeline for Indie Game Developers: Test Without a Team

QA Pipeline for Indie Game Developers: Test Without a Team

Indie developers ship games with 1-5 person teams, no dedicated QA engineers, and budgets that don't stretch to $40K/year test automation platforms. But shipping without a QA pipeline means bugs reach players — and players leave negative reviews.

This guide shows how to build a QA pipeline that punches above its weight: automated testing for game logic, CI/CD for builds, structured playtesting, and bug tracking — all achievable solo.

The Indie QA Stack

The right tools for an indie team balance effectiveness with cost:

Layer Tool Cost
Unit tests Unity Test Framework / GUT (Godot) Free
CI/CD GitHub Actions Free (2000 min/mo)
Build artifacts itch.io butler / Steam Free
Bug tracking GitHub Issues or Linear Free / $8/mo
Playtesting Steam Playtest / itch.io beta Free
Monitoring HelpMeTest Free (up to 10 tests)
Crash reporting Sentry (Unity SDK) Free tier

Total cost for a solid indie QA pipeline: $0-16/month.

Layer 1: Automated Unit Tests

Focus automated tests on logic that's expensive to test manually:

High-value test targets for indie games:

  • Save/load system
  • Inventory and item interactions
  • Combat calculations (damage, crit, status effects)
  • Achievement/unlock conditions
  • Level progression logic
  • Economy balance constraints
// Save/load: the most common source of game-breaking bugs
[TestFixture]
public class SaveSystemTests
{
    [Test]
    public void SaveAndLoad_PreservesAllPlayerStats()
    {
        var player = CreatePlayer(level: 15, health: 80, gold: 2500);
        player.AddItem(new Item("Flame Sword", rarity: Rarity.Rare));
        player.UnlockAchievement("FirstBoss");
        
        string saveData = SaveSystem.Serialize(player);
        var loaded = SaveSystem.Deserialize(saveData);
        
        Assert.AreEqual(15, loaded.Level);
        Assert.AreEqual(80, loaded.Health);
        Assert.AreEqual(2500, loaded.Gold);
        Assert.AreEqual(1, loaded.Inventory.Count);
        Assert.IsTrue(loaded.HasAchievement("FirstBoss"));
    }

    [Test]
    public void SaveSystem_HandlesVersion_Migration()
    {
        // Old save format from v1.0 (before inventory was added)
        string v1Save = @"{""version"":1,""player"":{""level"":5,""health"":100}}";
        
        var player = SaveSystem.Deserialize(v1Save);
        
        Assert.AreEqual(5, player.Level);
        Assert.IsNotNull(player.Inventory, "v1 save should migrate with empty inventory");
        Assert.AreEqual(0, player.Inventory.Count);
    }

    [Test]
    public void SaveSystem_DoesNotCorrupt_OnInterruptedWrite()
    {
        var player = CreatePlayer();
        
        // Write save to disk, then kill it halfway through
        // (Test with atomic write pattern)
        SaveSystem.Save(player);
        
        // Verify file is either complete or old version — never corrupt
        var loaded = SaveSystem.Load();
        Assert.IsNotNull(loaded, "Corrupted save should return null or old save, not crash");
    }
}

// Combat math: verify balance constraints
[TestFixture]
public class CombatCalculationTests
{
    [Test]
    [TestCase(100, 10, 0, 90)]    // Base damage
    [TestCase(100, 10, 50, 95)]   // 50% armor = 50% damage reduction
    [TestCase(100, 10, 100, 100)] // Full armor = 0 damage
    public void DamageCalculation_AppliesArmorCorrectly(
        int health, int damage, int armor, int expectedHealth)
    {
        var target = new Character(health: health, armor: armor);
        CombatSystem.ApplyDamage(target, damage);
        Assert.AreEqual(expectedHealth, target.Health);
    }

    [Test]
    public void CriticalHit_ExactlyDoublesDamage()
    {
        var target = new Character(health: 100, armor: 0);
        int normalDamage = CombatSystem.CalculateDamage(attacker: new Character(), weapon: Sword, isCrit: false);
        int critDamage = CombatSystem.CalculateDamage(attacker: new Character(), weapon: Sword, isCrit: true);
        
        Assert.AreEqual(normalDamage * 2, critDamage, "Crit should deal exactly 2x damage");
    }
}

Layer 2: CI/CD with GitHub Actions

Run tests automatically on every commit:

# .github/workflows/ci.yml
name: Game CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache Unity
        uses: actions/cache@v4
        with:
          path: Library
          key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}

      - name: Run Tests
        uses: game-ci/unity-test-runner@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        with:
          testMode: editmode
          artifactsPath: test-results

      - name: Upload Test Results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: test-results/

  build:
    name: Build Check
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4

      - name: Build
        uses: game-ci/unity-builder@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
        with:
          targetPlatform: StandaloneWindows64
          buildName: GameBuild

For Godot:

# .github/workflows/godot-ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    container: barichello/godot-ci:4.2.1
    steps:
      - uses: actions/checkout@v4

      - name: Run GUT Tests
        run: |
          godot --headless --script addons/gut/gut_cmdln.gd \
            -gdir=res://tests/ \
            -gexit

      - name: Build Export
        run: |
          mkdir -p exports/windows
          godot --headless --export-release "Windows Desktop" exports/windows/game.exe

Layer 3: Automated Playtesting with Bots

For single-player games, write bots that play through content automatically:

// Simple bot that plays through the first level
public class PlaytestBot : MonoBehaviour
{
    private PlayerController player;
    
    IEnumerator Start()
    {
        player = FindObjectOfType<PlayerController>();
        yield return RunLevel1();
        
        // Report results
        Debug.Log($"Bot completed Level 1 in {Time.time:F1}s");
        Debug.Log($"Deaths: {BotStats.Deaths}");
        Debug.Log($"Enemies killed: {BotStats.EnemiesKilled}");
        
        #if UNITY_EDITOR
        EditorApplication.isPlaying = false;
        #endif
    }
    
    IEnumerator RunLevel1()
    {
        // Navigate to first enemy
        yield return NavigateTo(FindObjectOfType<Enemy>().transform.position);
        
        // Fight until dead or enemy dies
        yield return FightUntilResolved();
        
        // Collect any items
        foreach (var item in FindObjectsOfType<CollectibleItem>())
        {
            yield return NavigateTo(item.transform.position);
        }
        
        // Find and reach level exit
        yield return NavigateTo(FindObjectOfType<LevelExit>().transform.position);
    }
}

Bots catch regressions that unit tests miss: level geometry holes, stuck enemies, unreachable items.

Layer 4: Structured Playtester Management

Human playtesters find what bots can't. Manage them effectively:

Playtester briefing template:

# Playtest Session Instructions

## Your Mission
Play the game naturally for 30-60 minutes. Focus on [Tutorial + Level 1-3].

## Please Report
- Anything that confused you (even if it worked correctly)
- Moments where you felt stuck > 1 minute
- Anything that felt unfair or random
- Bugs (screenshot if possible)

## Reporting
Use this Google Form: [link]
Fields: Type (Bug/Design/Confused), Description, Screenshot

## Do NOT
- Read the game description before playing
- Ask me how things work (figure it out like a new player)

Bug report template:

**Steps to reproduce:**
1. Start new game
2. Pick up the torch in the starting area
3. Enter the first dungeon room

**Expected:** Torch illuminates the room
**Actual:** Room stays dark, torch has no effect

**Frequency:** Always

**Device:** Windows 10, 16GB RAM

Set up GitHub Issues with labels:

  • bug/critical — game-breaking
  • bug/minor — cosmetic or edge case
  • feedback/design — not a bug, but worth discussing
  • feedback/balance — damage/difficulty feels off

Layer 5: Release Checklist

Before every release, run through this checklist:

# Release Checklist

## Automated (CI must be green)
- [ ] All unit tests pass
- [ ] Build completes without errors
- [ ] No compiler warnings (treat warnings as errors)

## Manual Testing
- [ ] New game start → reach first checkpoint without issue
- [ ] Load game from each save slot
- [ ] All main story paths playable
- [ ] Credits sequence completes
- [ ] Settings menu: graphics, audio, keybinds all save/load correctly
- [ ] Game quits cleanly without crash dialog

## Platform-Specific
- [ ] Windows: tested on Win10 and Win11
- [ ] MacOS: Apple Silicon native build tested
- [ ] Steam: overlay, achievements, cloud saves work
- [ ] Signed builds: no "untrusted developer" warnings

## Performance
- [ ] Minimum spec hardware (or VM simulation) tested
- [ ] No memory leak over 30-minute session
- [ ] Load times under 10 seconds

## Post-Release Monitoring
- [ ] Crash reporting (Sentry) is active
- [ ] HelpMeTest health checks running

Monitoring After Launch

After launch, monitor your game's backend with HelpMeTest:

# Set up health checks for game services
<span class="hljs-comment"># Runs from your server or CI — free tier covers 10 checks

<span class="hljs-comment"># Check leaderboard API
helpmetest health <span class="hljs-string">"leaderboard-api" <span class="hljs-string">"5m"

<span class="hljs-comment"># Check achievement sync
helpmetest health <span class="hljs-string">"achievement-service" <span class="hljs-string">"5m"

<span class="hljs-comment"># Check patch server
helpmetest health <span class="hljs-string">"update-server" <span class="hljs-string">"10m"

If any service goes down, you get alerted within 5 minutes — not when players tweet at you.

Budget Summary

A complete indie QA pipeline costs almost nothing:

Tool Free Tier
GitHub Actions 2,000 min/month
Unity Test Framework Included in Unity
Sentry (crash reports) 5K errors/month
HelpMeTest 10 tests, unlimited health checks
itch.io beta access Free

The bottleneck isn't money — it's writing the tests. Start with save/load tests (they pay back immediately), add combat math tests, then expand to other systems over time.

Summary

An indie QA pipeline doesn't need a budget — it needs discipline. The four layers:

  1. Automated unit tests for save/load, combat math, and game systems
  2. CI/CD to catch regressions on every commit
  3. Bots to automate playthrough testing
  4. Structured playtesting to catch what bots miss

Ship with confidence, not hope.

Read more