Advertisement
Advertisement
Advertisement
Advertisement
Advertisement
Navigation
Getting Started
Subscriptions
Ai Features
User Guide
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
DatabaseTransactionsfor unit tests (faster thanRefreshDatabase) - 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.
Advertisement