Back Index Next
Debugging Index Summary

Testing & Verification

Click a technique to see how it applies to Heist.exe.


Testing & Verification in Heist.exe

Testing confirms that your code does what you think it does. In Heist.exe, this means verifying gem collection state, Guard bounce behavior, collision sequences, and API error handling — before a bug reaches the player.


flowchart LR
    A[Feature Built] --> B{Does it work\ncorrectly?}
    B -- Unknown --> C[Write a Test]
    C --> D{Test Passes?}
    D -- Yes --> E[✅ Verified]
    D -- No --> F[🐛 Bug Found]
    F --> G[Fix Code]
    G --> C
    B -- Yes --> E

The Five Testing Techniques

Unit Testing — test one method in isolation with known inputs

Exactly what it is: Isolate a single method. Call it with test inputs. Check that the outputs match what you expected. Don’t test the whole game — test Gem.collect() alone, without the game running, without collisions, without anything else interfering.

From Heist.exe — Gem.collect():

// Test setup: Create a simple mock Gem
const gameEnv = { stats: {} };

const gem = {
  value: 5,
  permanentlyCollected: false,
  canvas: { style: { display: '' } },
  collect() {
    if (this.permanentlyCollected) return;      // Gate: don't collect twice
    this.permanentlyCollected = true;           // Mark collected
    gameEnv.stats.coinsCollected = (gameEnv.stats.coinsCollected || 0) + this.value;
    this.canvas.style.display = 'none';         // Hide canvas
    console.log(`Gem collected! +${this.value}`);
  }
};

// Test 1: First collection should work
gem.collect();
console.assert(gem.permanentlyCollected === true,        "FAIL: should be marked collected");
console.assert(gameEnv.stats.coinsCollected === 5,       "FAIL: score should be 5");
console.assert(gem.canvas.style.display === 'none',      "FAIL: canvas should be hidden");

// Test 2: Second collection should be blocked
gem.collect();  // Try collecting again
console.assert(gameEnv.stats.coinsCollected === 5,       "FAIL: score should still be 5, not 10");

console.log("✓ All unit tests passed!");

Each assertion checks one specific behavior. If any assertion fails, you know exactly which part broke.


Integration Testing — test how two systems interact end-to-end

Exactly what it is: Unit tests only verify individual pieces. Integration tests verify that those pieces work together correctly when combined. Test Guard + Player collision from start to finish: Guard moves into Player, collision fires, both Guard and Player update state correctly.

From Heist.exe — Guard + Player collision sequence:

// Test setup: Create mock Guard and Player
const player = {
  destroyed: false,
  destroy() {
    this.destroyed = true;
    console.log("Player destroyed!");
  }
};

const guard = {
  playerDestroyed: false,
  collisionChecks() { return true; },  // Simulate collision detected
  handleCollisionEvent() {
    console.log("Collision detected!");
    player.destroy();
    this.playerDestroyed = true;
  },
  update() {
    if (!this.playerDestroyed && this.collisionChecks()) {
      this.handleCollisionEvent();  // Full sequence: condition → event → state update
    }
  }
};

// Test: Full collision sequence
guard.update();  // First frame — collision should fire
console.assert(player.destroyed === true,      "FAIL: player should be destroyed");
console.assert(guard.playerDestroyed === true, "FAIL: guard should track destruction");

// Test: Collision should not fire twice
let callCount = 0;
const trackedGuard = {
  playerDestroyed: false,
  collisionChecks() { return true; },
  handleCollisionEvent() { callCount++; this.playerDestroyed = true; },
  update() {
    if (!this.playerDestroyed && this.collisionChecks()) {
      this.handleCollisionEvent();
    }
  }
};
trackedGuard.update();  // First collision
trackedGuard.update();  // Try again — playerDestroyed blocks it
console.assert(callCount === 1, "FAIL: collision should only fire once");

console.log("✓ Integration tests passed!");

This test verifies the entire flow: condition check → event firing → state change blocking future events.


State Verification — confirm runtime values match expected values after actions

Exactly what it is: After an action completes, inspect the actual state of your objects. Do the variables have the values you expected? Are flags set correctly? Did counters increment? This catches subtle bugs where code runs but doesn’t actually change state the way you intended.

From Heist.exe — After collecting gems:

// Scenario: Player collects both gems in a level
// After collection, verify:

console.assert(gameEnv.stats.coinsCollected === 10, 
  "FAIL: Expected 10 points (5 per gem), but got " + gameEnv.stats.coinsCollected);

console.assert(gem1.permanentlyCollected === true,  
  "FAIL: gem1 should be locked from re-collection");

console.assert(gem2.permanentlyCollected === true,  
  "FAIL: gem2 should be locked from re-collection");

// Verify that trying to collect again doesn't change the score
const oldScore = gameEnv.stats.coinsCollected;
gem1.collect();
gem2.collect();
console.assert(gameEnv.stats.coinsCollected === oldScore,
  "FAIL: Collecting twice should not change the score");

State verification is simple but powerful: it catches cases where code appears to run but actually fails silently.


Boundary Testing — test behavior at exact limits of valid input

Exactly what it is: Most bugs hide at boundaries — the exact frame a Guard hits a wall, a value at exactly 0, a boolean at the moment it flips. Test these edge cases explicitly because that’s where the code branches.

From Heist.exe — Guard bounce at canvas edges:

// Guard.js — stayWithinCanvas() boundary conditions
// Test: Guard at bottom edge
guard.position.y = gameEnv.innerHeight;
guard.velocity.y = 3;  // Positive (moving down)
guard.stayWithinCanvas();
console.assert(guard.velocity.y < 0, "FAIL: Should bounce (reverse velocity) at bottom");

// Test: Guard at top edge
guard.position.y = 0;
guard.velocity.y = -3;  // Negative (moving up)
guard.stayWithinCanvas();
console.assert(guard.velocity.y > 0, "FAIL: Should bounce at top");

// Test: Guard slightly over boundary (clipping)
guard.position.y = gameEnv.innerHeight + 100;  // Way past edge
guard.velocity.y = 3;
guard.stayWithinCanvas();
console.assert(guard.position.y <= gameEnv.innerHeight, "FAIL: Should be clamped to boundary");
console.assert(guard.velocity.y < 0, "FAIL: Should still bounce");

// Test: Guard slightly under boundary
guard.position.y = -50;  // Way past top edge
guard.stayWithinCanvas();
console.assert(guard.position.y >= 0, "FAIL: Should be clamped to boundary");

Boundary testing catches physics bugs: does the bounce work at -1? At 0? At exactly the edge? Slightly over?


API Response Testing — verify your code handles both success and failure

Exactly what it is: Any code that calls an external API needs to handle two cases: the API works and returns data, or it fails and returns an error. Test both paths. Also test your filtering logic: do you correctly identify valid data and reject invalid data?

From Heist.exe — heistMusic fetch validation:

// Test 1: Success path — valid API response
const validResponse = {
  ok: true,
  json: async () => ({
    results: [
      { previewUrl: 'https://...', artistName: 'Indie Band' },  // Valid
      { previewUrl: 'https://...', artistName: 'Vocal Artist' },  // Filtered
      { previewUrl: null, artistName: 'No Preview' },  // Filtered (no preview)
    ]
  })
};

// Simulate: validate response, build pool, pick track
console.assert(validResponse.ok === true, "FAIL: should be successful");
// Filter should remove vocal and no-preview tracks
console.assert(filteredPool.length === 1, "FAIL: Should only have 1 valid track");

// Test 2: Failure path — API returns error
const failedResponse = {
  ok: false,
  status: 404  // Not found
};

console.assert(failedResponse.ok === false, "FAIL: should mark as failed");
// Error handling should throw with clear message
console.assert(errorMessage.includes('404'), "FAIL: Should include HTTP status code");

// Test 3: Filter logic — verify VOCAL_HINTS regex works
const VOCAL_HINTS = /vocal|singer|voice|chorus|lyrics|duet|acapella/i;
console.assert('Vocal Artist'.match(VOCAL_HINTS), "FAIL: Regex should match 'Vocal'");
console.assert(!'Indie Band'.match(VOCAL_HINTS), "FAIL: Regex should NOT match 'Indie'");

// Test 4: Edge case — all tracks are filtered out
if (pool.length === 0) {
  console.log("All tracks filtered; falling back to unfiltered list");
  pool = data.results.filter(t => t.previewUrl);  // Accept all with preview
}
console.assert(pool.length > 0, "FAIL: Should have fallback");

console.log("✓ All API tests passed!");

API testing prevents silent failures: test that errors are thrown, caught, and handled gracefully.


Why Testing Matters in Heist.exe

What breaks How testing catches it
Gem collected twice Unit test: collect() twice, assert score only changes once
Guard destroys player repeatedly Integration test: assert playerDestroyed blocks re-entry
Music crashes silently API test: simulate failed fetch, assert error is handled
Guard clips through wall Boundary test: place at exact edge, assert velocity reverses
Score not saved to gameEnv State test: collect gem, inspect gameEnv.stats directly

The goal isn’t to test everything — it’s to test the parts that break in interesting ways. In Heist.exe, that’s the one-time events (collection, collision) and the external dependency (iTunes API).

Here is some more information: