domain project / gaming

Online Gaming

Test strategies for game lobbies, matchmaking, leaderboards, in-game purchases, anti-cheat, and live event systems.

Game lobby & matchmakingLeaderboards & rankingsIn-game purchasesLive eventsAnti-cheat controlsPlayer profiles & progression

Domain Overview

Online gaming has extreme performance sensitivity — latency matters in ways other domains do not. Real-money transactions (loot boxes, battle passes) and high-value accounts create significant fraud risk including account takeover for valuable items. Testing must cover real-time multiplayer state, WebSocket-driven lobby systems, and purchase flows that involve virtual currency intermediaries.

Typical stack: Node.js or Go game server, Redis pub/sub for lobby state, WebSocket connections for real-time events, Stripe or platform billing for IAP, CDN-cached leaderboard snapshots.

Test pyramid skews heavily toward API and integration for game logic — E2E is reserved only for critical purchase flows and lobby entry paths where UI bugs have direct revenue impact.

Key Test Areas

AreaWhat to testRisk
LobbyCreate/join/leave room, player count limits, reconnect on dropPlayers stuck outside game
MatchmakingRank-based matching, wait time display, cancel mid-queueUnfair matches, abandoned queues
LeaderboardScore update latency, tied ranks, pagination, seasonal resetIncorrect rankings, cheating perception
PurchasesVirtual currency, cosmetics, subscription, refund policyRevenue loss, duplicate charge
Live eventsTime-gated content, countdown timer accuracy, reward claimPlayers miss rewards, legal complaints
Anti-cheatRate limiting on score submit, stat anomaly detectionCheater infestation, player churn

Test Pyramid for Online Gaming

Unit (65%):        Game rules, scoring logic, matchmaking algorithm, rank calculation
Integration (30%): WebSocket lobby, purchase API, leaderboard write pipeline, anti-cheat hooks
E2E (5%):          Purchase flow (real currency), profile update, lobby join → match start

Critical E2E tests (must pass before every deploy):

  1. Player purchases virtual currency pack and sees balance updated
  2. Player joins lobby, lobby fills, match starts within timeout
  3. Match score submitted, leaderboard rank updates within SLA
  4. Time-gated live event inaccessible before and after window
  5. Duplicate purchase attempt returns error, balance charged once

Common Failure Scenarios

1. Leaderboard shows stale score after match end (cache not invalidated)
   Test: submit score via API, poll leaderboard until updated or timeout, assert rank

2. In-game purchase succeeds (payment confirmed) but item not delivered to inventory
   Test: complete purchase flow, call inventory API, assert item present

3. Live event countdown shows wrong timezone for player's locale
   Test: override browser timezone with page.emulateMedia / locale, verify countdown matches UTC offset

4. Player can join full lobby via direct URL bypass (client-side gate only)
   Test: fill lobby to max via API, attempt join via direct URL, assert 403 or redirect

5. Virtual currency deducted twice on rapid double-tap of buy button
   Test: simulate rapid double-click on purchase button, assert single debit in transaction log

Playwright Patterns for Online Gaming

// Test WebSocket game lobby by mocking ws messages
test('lobby shows player joined notification', async ({ page }) => {
  await page.goto('/lobby/room-123');

  // Inject a fake player-joined ws message from the server
  await page.evaluate(() => {
    const ws = (window as any).__lobbySocket;
    ws.dispatchEvent(new MessageEvent('message', {
      data: JSON.stringify({ type: 'PLAYER_JOINED', playerId: 'bot-99', name: 'TestBot' })
    }));
  });

  await expect(page.getByText('TestBot')).toBeVisible();
});

// Test purchase idempotency with rapid double-click
test('double-click buy does not charge twice', async ({ page }) => {
  let purchaseCount = 0;
  await page.route('**/api/purchase', route => {
    purchaseCount++;
    return route.fulfill({ json: { success: true, newBalance: 900 } });
  });

  await page.goto('/store');
  const buyButton = page.getByRole('button', { name: 'Buy Now' });
  await buyButton.dblclick();

  await page.waitForTimeout(500);
  expect(purchaseCount).toBe(1);
});