Documentation

CREATING_TESTS

AI Powered
Back to Docs

Advertisement

CREATING_TESTS

Documentation & Guides

RiftSurge Testing Strategy & Guidelines

Overview

This document outlines the comprehensive testing strategy for RiftSurge, focusing on functional testing over visual/UI testing. Our goal is to ensure the application works correctly without getting bogged down in testing visual elements like gradients, colors, or layout specifics.

Core Testing Philosophy

✅ What We Test (Functional Testing)

1. Business Logic - Core application functionality

2. API Endpoints - Request/response handling, status codes, data validation

3. Database Operations - CRUD operations, relationships, data integrity

4. Service Layer - Business logic, data processing, external API integration

5. Livewire Components - Component state, methods, user interactions (NOT UI rendering)

6. Jobs & Queues - Background processing, error handling

7. Authentication & Authorization - User permissions, access control

8. Data Validation - Form requests, model validation rules

9. External Integrations - Riot API, Discord, AI services (mocked)

❌ What We Don't Test (Visual/UI Testing)

1. Visual Elements - Colors, gradients, fonts, spacing

2. Layout & Styling - CSS classes, responsive design, visual positioning

3. UI Components - Button appearance, form styling, visual feedback

4. Design System - Flux UI component visual rendering

5. Browser-Specific Rendering - Cross-browser compatibility

6. Accessibility Visuals - Screen reader compatibility (test functionality, not visuals)

🤔 What We Test Conditionally (Functional UI)

1. Livewire Component Functionality - State changes, method calls, data binding

2. Form Submissions - Data processing, validation, redirects

3. Navigation - Route handling, page loads, redirects

4. User Interactions - Click handlers, form inputs, component state

5. Error Handling - Error messages, exception handling, fallbacks

Test Structure & Organization

Directory Structure

tests/

├── Feature/ # Full application tests

│ ├── Auth/ # Authentication & authorization

│ ├── API/ # API endpoints

│ ├── Livewire/ # Livewire component functionality

│ ├── Jobs/ # Background jobs

│ ├── Services/ # Service layer

│ └── Actions/ # Action classes

├── Unit/ # Isolated unit tests

│ ├── Models/ # Model logic, relationships, accessors

│ ├── Services/ # Service methods (isolated)

│ ├── Helpers/ # Helper functions

│ └── Enums/ # Enum behavior

└── Helpers/ # Test utilities and mocks

├── ExternalApiTestHelper.php

├── RiotApiTestHelper.php

└── PrismTestHelper.php

Pest Testing Framework

RiftSurge uses Pest as the primary testing framework. Pest provides a clean, expressive syntax that makes tests more readable and maintainable.

Pest Configuration

  • Config File: pest.json - Contains test environment settings
  • Base Test Case: tests/TestCase.php - Extends Laravel's base test case
  • Test Helpers: Located in tests/Helpers/ directory

Test Categories

1. Feature Tests (tests/Feature/)

  • Purpose: Test complete user workflows and application features
  • Scope: Full Laravel application context with database, HTTP, etc.
  • Examples:
  • User registration flow
  • Scrim creation and management
  • Team statistics calculation
  • API endpoint responses

2. Unit Tests (tests/Unit/)

  • Purpose: Test individual components in isolation
  • Scope: Minimal dependencies, focused on single classes/methods
  • Examples:
  • Model accessors and mutators
  • Service method logic
  • Helper function behavior
  • Enum value handling

Testing Standards & Best Practices

1. Pest Test Naming Conventions

// Feature Tests - Describe user actions

it('allows authenticated users to create scrims', function () {

// Test implementation

});

it('prevents unauthenticated users from accessing team data', function () {

// Test implementation

});

it('calculates team statistics correctly', function () {

// Test implementation

});

// Unit Tests - Describe method behavior

it('returns correct win rate when games are played', function () {

// Test implementation

});

it('throws exception when invalid data is provided', function () {

// Test implementation

});

it('formats date correctly for display', function () {

// Test implementation

});

// Grouped Tests with describe()

describe('Team Statistics', function () {

it('calculates win rate correctly', function () {

// Test implementation

});

it('handles zero games played', function () {

// Test implementation

});

});

2. Pest Test Structure (AAA Pattern)

it('calculates team win rate correctly', function () {

// Arrange - Set up test data

$team = Team::factory()->create();

$scrim1 = Scrim::factory()->create(['winner_id' => $team->id]);

$scrim2 = Scrim::factory()->create(['winner_id' => null]); // Loss

// Act - Execute the functionality

$winRate = $team->calculateWinRate();

// Assert - Verify the result using Pest's expect() syntax

expect($winRate)->toBe(50.0);

});

// Using Pest's expect() assertions

it('validates team name is required', function () {

$response = $this->postJson('/api/teams', []);

expect($response->status())->toBe(422);

expect($response->json('errors.name'))->toContain('The name field is required.');

});

3. Pest Mocking Strategy

External APIs (Always Mock)

CRITICAL: All external API calls must be mocked to prevent real network requests during tests.

##### Riot API Mocking

// Mock Riot API calls globally in TestCase.php

Http::fake([

'.api.riotgames.com/' => Http::response([

'puuid' => 'test-puuid-123',

'gameName' => 'TestPlayer',

'tagLine' => 'NA1',

'id' => 'test-summoner-id',

'accountId' => 'test-account-id',

'name' => 'TestPlayer',

'profileIconId' => 1234,

'summonerLevel' => 100,

'queueType' => 'RANKED_SOLO_5x5',

'tier' => 'DIAMOND',

'rank' => 'II',

'leaguePoints' => 75,

'wins' => 150,

'losses' => 100,

], 200),

]);

// Mock specific Riot API endpoints

it('fetches player data from Riot API', function () {

Http::fake([

'.api.riotgames.com/riot/account/v1/accounts/by-riot-id/' => Http::response([

'puuid' => 'test-puuid-123',

'gameName' => 'TestPlayer',

'tagLine' => 'NA1',

], 200),

]);

$service = new RiotApiService();

$result = $service->getAccount('NA', 'TestPlayer', 'NA1');

expect($result['puuid'])->toBe('test-puuid-123');

});

##### Discord API Mocking

// Mock Discord API calls

Http::fake([

'discord.com/api/' => Http::response([], 200),

'.discord.com/api/' => Http::response([], 200),

]);

// Mock specific Discord operations

it('sends Discord notification', function () {

Http::fake([

'discord.com/api/webhooks/' => Http::response([], 200),

]);

$discordService = new DiscordService();

$result = $discordService->sendWebhook('test-webhook-url', [

'content' => 'Test message'

]);

expect($result)->toBeTrue();

});

##### AI Service Mocking (Prism)

// Mock Prism AI calls for text responses

use Prism\Prism\Prism;

use Prism\Prism\Testing\TextResponseFake;

use Prism\Prism\ValueObjects\Usage;

use Prism\Prism\ValueObjects\Meta;

it('generates AI analysis', function () {

$fakeResponse = TextResponseFake::make()

->withText('Mocked AI analysis response')

->withUsage(new Usage(10, 20))

->withMeta(new Meta('fake-1', 'fake-model'));

Prism::fake([$fakeResponse]);

$aiService = new AiService();

$result = $aiService->analyzeText('Test prompt');

expect($result->text)->toBe('Mocked AI analysis response');

});

// Mock Prism AI calls for structured responses

use Prism\Prism\Testing\StructuredResponseFake;

use Prism\Prism\Enums\FinishReason;

it('generates structured AI analysis', function () {

$mockStructuredData = [

'analysis' => [

'summary' => 'Team performed well',

'recommendations' => ['Focus on early game', 'Improve teamfighting'],

'confidence_score' => 0.85

]

];

$fakeResponse = StructuredResponseFake::make()

->withText(json_encode($mockStructuredData, JSON_THROW_ON_ERROR))

->withStructured($mockStructuredData)

->withFinishReason(FinishReason::Stop)

->withUsage(new Usage(10, 20))

->withMeta(new Meta('fake-1', 'fake-model'));

Prism::fake([$fakeResponse]);

$aiService = new AiService();

$result = $aiService->analyzeStructured('Test prompt', $schema);

expect($result->structured['analysis']['confidence_score'])->toBe(0.85);

});

// Mock multiple AI responses for conversations

it('handles AI conversation flow', function () {

$responses = [

TextResponseFake::make()

->withText('Initial analysis response')

->withUsage(new Usage(15, 25))

->withMeta(new Meta('fake-1', 'fake-model')),

TextResponseFake::make()

->withText('Follow-up analysis response')

->withUsage(new Usage(20, 30))

->withMeta(new Meta('fake-2', 'fake-model')),

];

Prism::fake($responses);

$aiService = new AiService();

$result1 = $aiService->analyzeText('Initial prompt');

$result2 = $aiService->analyzeText('Follow-up prompt');

expect($result1->text)->toBe('Initial analysis response');

expect($result2->text)->toBe('Follow-up analysis response');

});

// Mock streamed AI responses

it('handles streamed AI responses', function () {

Prism::fake([

TextResponseFake::make()

->withText('Streamed AI response text')

->withFinishReason(FinishReason::Stop)

]);

$aiService = new AiService();

$stream = $aiService->analyzeTextStream('Test prompt');

$outputText = '';

foreach ($stream as $chunk) {

$outputText .= $chunk->text;

}

expect($outputText)->toBe('Streamed AI response text');

});

Services (Mock when testing consumers)

// Using Pest's mock() helper

it('uses stats service to calculate team performance', function () {

$mockService = $this->mock(StatsService::class);

$mockService->shouldReceive('calculateTeamStats')

->once()

->andReturn(['win_rate' => 75.0, 'total_games' => 20]);

$team = Team::factory()->create();

$result = $team->getPerformanceStats();

expect($result['win_rate'])->toBe(75.0);

});

// Using Mockery directly in Pest

it('handles service failures gracefully', function () {

$this->mock(StatsService::class, function ($mock) {

$mock->shouldReceive('calculateTeamStats')

->andThrow(new \Exception('Service unavailable'));

});

$team = Team::factory()->create();

expect(fn() => $team->getPerformanceStats())

->toThrow(\Exception::class, 'Service unavailable');

});

4. Pest Database Testing

// Use RefreshDatabase for feature tests

use RefreshDatabase;

// Use DatabaseTransactions for unit tests (faster)

use DatabaseTransactions;

// Always use factories for test data

it('creates team with valid data', function () {

$team = Team::factory()->create([

'name' => 'Test Team',

'region' => 'na1'

]);

expect($team->name)->toBe('Test Team');

expect($team->region->value)->toBe('na1');

expect($team->exists)->toBeTrue();

});

// Test database relationships

it('associates user with team correctly', function () {

$user = User::factory()->create();

$team = Team::factory()->create(['user_id' => $user->id]);

expect($team->user->id)->toBe($user->id);

expect($user->teams)->toContain($team);

});

5. Pest Livewire Component Testing

// Test component functionality, not UI

it('updates team name when form is submitted', function () {

$team = Team::factory()->create(['name' => 'Old Name']);

Livewire::test(EditTeam::class, ['team' => $team])

->set('name', 'New Name')

->call('update')

->assertSet('name', 'New Name');

expect($team->fresh()->name)->toBe('New Name');

});

// Test Livewire component state changes

it('validates required fields in team creation form', function () {

Livewire::test(CreateTeam::class)

->set('name', '')

->call('create')

->assertHasErrors(['name' => 'required']);

});

// Test Livewire component methods

it('calculates team statistics when component loads', function () {

$team = Team::factory()->create();

$scrims = Scrim::factory()->count(3)->create(['winner_id' => $team->id]);

$component = Livewire::test(TeamStats::class, ['team' => $team]);

expect($component->get('winRate'))->toBe(100.0);

expect($component->get('totalGames'))->toBe(3);

});

// DON'T test UI elements

// ❌ expect($component->get('name'))->toContain('bg-blue-500');

// ✅ expect($component->get('name'))->toBe('New Name');

Pest-Specific Features & Best Practices

1. Pest Test Organization

// Group related tests with describe()

describe('Team Management', function () {

it('creates team with valid data', function () {

// Test implementation

});

it('validates team name is required', function () {

// Test implementation

});

it('updates team information', function () {

// Test implementation

});

});

// Nested describe blocks for complex features

describe('Team Statistics', function () {

describe('Win Rate Calculation', function () {

it('calculates win rate correctly', function () {

// Test implementation

});

it('handles zero games played', function () {

// Test implementation

});

});

describe('Performance Metrics', function () {

it('calculates average game duration', function () {

// Test implementation

});

});

});

2. Pest Assertions & Expectations

// Basic assertions

expect($value)->toBe($expected);

expect($value)->not->toBe($unexpected);

expect($value)->toBeTrue();

expect($value)->toBeFalse();

expect($value)->toBeNull();

expect($value)->not->toBeNull();

// Array assertions

expect($array)->toHaveCount(3);

expect($array)->toContain('value');

expect($array)->not->toContain('unwanted');

expect($array)->toHaveKey('key');

// String assertions

expect($string)->toContain('substring');

expect($string)->toStartWith('prefix');

expect($string)->toEndWith('suffix');

// Exception assertions

expect(fn() => $this->methodThatThrows())

->toThrow(\Exception::class);

expect(fn() => $this->methodThatThrows())

->toThrow(\Exception::class, 'Specific message');

// Collection assertions

expect($collection)->toHaveCount(5);

expect($collection)->toContain($item);

expect($collection->first())->toBeInstanceOf(Model::class);

3. Pest Test Lifecycle

// beforeEach() - runs before each test

beforeEach(function () {

$this->user = User::factory()->create();

$this->team = Team::factory()->create(['user_id' => $this->user->id]);

});

// afterEach() - runs after each test

afterEach(function () {

// Cleanup code if needed

});

// beforeAll() - runs once before all tests in the file

beforeAll(function () {

// One-time setup

});

// afterAll() - runs once after all tests in the file

afterAll(function () {

// One-time cleanup

});

4. Pest Datasets

// Test with multiple data sets

it('validates team region', function (string $region, bool $expected) {

$team = Team::factory()->create(['region' => $region]);

expect($team->isValidRegion())->toBe($expected);

})->with([

['na1', true],

['euw1', true],

['kr', true],

['invalid', false],

['', false],

]);

// Named datasets

it('calculates win rate for different scenarios', function (int $wins, int $losses, float $expectedRate) {

$team = Team::factory()->create();

Scrim::factory()->count($wins)->create(['winner_id' => $team->id]);

Scrim::factory()->count($losses)->create(['winner_id' => null]);

expect($team->winRate)->toBe($expectedRate);

})->with([

'perfect record' => [5, 0, 100.0],

'even split' => [3, 3, 50.0],

'losing record' => [1, 4, 20.0],

'no games' => [0, 0, 0.0],

]);

5. Pest Test Helpers

// Custom test helpers

function createTeamWithScrims(int $scrimCount = 3): Team

{

$team = Team::factory()->create();

Scrim::factory()->count($scrimCount)->create(['winner_id' => $team->id]);

return $team;

}

// Using custom helpers in tests

it('calculates statistics for team with multiple scrims', function () {

$team = createTeamWithScrims(5);

expect($team->scrims->count())->toBe(5);

expect($team->winRate)->toBe(100.0);

});

Test Implementation Guidelines

1. Feature Tests

API Endpoints

it('returns team data for authenticated users', function () {

$user = User::factory()->create();

$team = Team::factory()->create(['user_id' => $user->id]);

$response = $this->actingAs($user)

->getJson('/api/teams/' . $team->id);

expect($response->status())->toBe(200);

expect($response->json())->toHaveKeys(['id', 'name', 'region', 'created_at']);

expect($response->json('id'))->toBe($team->id);

});

it('returns 401 for unauthenticated users', function () {

$team = Team::factory()->create();

$response = $this->getJson('/api/teams/' . $team->id);

expect($response->status())->toBe(401);

});

Livewire Components

it('creates scrim when form is valid', function () {

$user = User::factory()->create();

$team = Team::factory()->create(['user_id' => $user->id]);

Livewire::actingAs($user)

->test(CreateScrim::class)

->set('opponent_name', 'Test Team')

->set('scheduled_for', now()->addDay())

->call('create')

->assertRedirect('/scrims');

expect(Scrim::where('opponent_name', 'Test Team')->exists())->toBeTrue();

});

it('validates required fields in scrim creation', function () {

$user = User::factory()->create();

Livewire::actingAs($user)

->test(CreateScrim::class)

->set('opponent_name', '')

->call('create')

->assertHasErrors(['opponent_name' => 'required']);

});

Background Jobs

it('processes scrim analysis job successfully', function () {

$scrim = Scrim::factory()->create();

// Mock external API calls

ExternalApiTestHelper::mockAllExternalApis();

// Dispatch job

AnalyzeScrimJob::dispatch($scrim);

// Assert job was processed

expect($scrim->fresh()->analysis_completed_at)->not->toBeNull();

});

it('handles job failures gracefully', function () {

$scrim = Scrim::factory()->create();

// Mock API to return error

Http::fake(['.api.riotgames.com/' => Http::response([], 500)]);

expect(fn() => AnalyzeScrimJob::dispatch($scrim))

->not->toThrow(\Exception::class);

});

2. Unit Tests

Model Testing

describe('Team Model', function () {

it('calculates win rate correctly', function () {

$team = Team::factory()->create();

// Create test data

Scrim::factory()->count(3)->create(['winner_id' => $team->id]); // 3 wins

Scrim::factory()->count(2)->create(['winner_id' => null]); // 2 losses

expect($team->winRate)->toBe(60.0);

});

it('handles zero games played', function () {

$team = Team::factory()->create();

expect($team->winRate)->toBe(0.0);

expect($team->totalGames)->toBe(0);

});

it('validates required fields', function () {

expect(fn() => Team::create([]))

->toThrow(\Illuminate\Database\QueryException::class);

});

});

Service Testing

describe('StatsService', function () {

it('processes team statistics correctly', function () {

$team = Team::factory()->create();

$mockData = ['wins' => 5, 'losses' => 3];

$service = new StatsService();

$result = $service->calculateTeamStats($team, $mockData);

expect($result['win_rate'])->toBe(62.5);

expect($result['total_games'])->toBe(8);

});

it('handles empty data gracefully', function () {

$team = Team::factory()->create();

$service = new StatsService();

$result = $service->calculateTeamStats($team, []);

expect($result['win_rate'])->toBe(0.0);

expect($result['total_games'])->toBe(0);

});

});

Test Data Management

1. Factories

// Always use factories for consistent test data

$team = Team::factory()->create();

$user = User::factory()->create(['team_id' => $team->id]);

// Use factory states for specific scenarios

$premiumTeam = Team::factory()->premium()->create();

$inactiveUser = User::factory()->inactive()->create();

2. Test Helpers

// Use test helpers for complex setup

$scrimWithGames = ScrimTestHelper::createScrimWithGames($team, 3);

$teamWithStats = TeamTestHelper::createTeamWithStats($user);

3. Database Seeding

// Seed essential data for tests

$this->seed([

ChampionSeeder::class,

PatchNoteSeeder::class,

]);

Performance Considerations

1. Test Speed

  • Use DatabaseTransactions for unit tests (faster than RefreshDatabase)
  • Mock external API calls to avoid network delays
  • Use in-memory SQLite for faster database operations
  • Group related tests to minimize setup/teardown

2. Test Isolation

  • Each test should be independent
  • No shared state between tests
  • Clean up after each test
  • Use unique test data to avoid conflicts

3. Parallel Testing

  • Tests should be able to run in parallel
  • No shared resources or global state
  • Use unique identifiers for test data

Error Handling & Edge Cases

1. Exception Testing

it('throws exception when invalid team ID is provided', function () {

expect(fn() => Team::findOrFail(999999))

->toThrow(ModelNotFoundException::class);

});

2. Edge Case Testing

it('handles division by zero in win rate calculation', function () {

$team = Team::factory()->create();

// No games played

expect($team->winRate)->toBe(0.0);

});

3. Validation Testing

it('validates required fields in team creation', function () {

$response = $this->postJson('/api/teams', []);

$response->assertStatus(422)

->assertJsonValidationErrors(['name', 'region']);

});

Continuous Integration

1. Test Execution

  • Run all tests on every commit
  • Fail builds on test failures
  • Generate coverage reports
  • Run tests in parallel for speed

2. Test Maintenance

  • Review and update tests when features change
  • Remove obsolete tests
  • Refactor tests for better maintainability
  • Document complex test scenarios

Common Anti-Patterns to Avoid

❌ Don't Do This

// Testing UI elements

expect($component->get('buttonClass'))->toContain('bg-blue-500');

// Testing implementation details

expect($service->getCacheKey())->toBe('team_stats_123');

// Testing external API responses directly

expect($riotApiResponse['summonerLevel'])->toBe(100);

// Hardcoded test data

$team = new Team(['name' => 'Hardcoded Team']);

✅ Do This Instead

// Test functionality

expect($component->get('isLoading'))->toBeFalse();

// Test behavior

expect($service->getTeamStats($team))->toBeArray();

// Test with mocked data

Http::fake(['.api.riotgames.com/' => Http::response($mockData)]);

expect($service->getPlayerData('test-player'))->toBeArray();

// Use factories

$team = Team::factory()->create();

Testing Checklist

Before Writing Tests

  • [ ] Understand the feature/functionality being tested
  • [ ] Identify the key behaviors to test
  • [ ] Determine if it's a Feature or Unit test
  • [ ] Plan the test data needed

While Writing Tests

  • [ ] Follow AAA pattern (Arrange, Act, Assert)
  • [ ] Use descriptive test names
  • [ ] Mock external dependencies
  • [ ] Test both happy path and edge cases
  • [ ] Ensure test isolation

After Writing Tests

  • [ ] Run the test to ensure it passes
  • [ ] Verify test covers the intended functionality
  • [ ] Check that test is maintainable
  • [ ] Ensure test runs in reasonable time
  • [ ] Document any complex test scenarios

Conclusion

This testing strategy focuses on functional correctness over visual appearance, ensuring that RiftSurge works as intended without getting bogged down in UI testing. By following these guidelines, we can maintain a robust, fast, and maintainable test suite that provides confidence in the application's functionality.

Remember: We test what the application does, not how it looks.

Need help? Check our FAQ

Advertisement