Files
michaelschiemer/tests/Database/QueryOptimization/NPlusOneDetectionServiceTest.php

214 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Database\QueryOptimization\Analysis\EagerLoadingAnalyzer;
use App\Framework\Database\QueryOptimization\Analysis\NPlusOneDetector;
use App\Framework\Database\QueryOptimization\NPlusOneDetectionService;
use App\Framework\Database\QueryOptimization\QueryLogger;
use App\Framework\Database\QueryOptimization\ValueObjects\QueryLog;
use App\Framework\Logging\Logger;
describe('NPlusOneDetectionService', function () {
beforeEach(function () {
$this->queryLogger = new QueryLogger();
$this->detector = new NPlusOneDetector();
$this->analyzer = new EagerLoadingAnalyzer();
$this->logger = $this->createMock(Logger::class);
$this->service = new NPlusOneDetectionService(
$this->queryLogger,
$this->detector,
$this->analyzer,
$this->logger
);
});
it('starts and stops logging', function () {
expect($this->queryLogger->isEnabled())->toBeFalse();
$this->service->startLogging();
expect($this->queryLogger->isEnabled())->toBeTrue();
$this->service->stopLogging();
expect($this->queryLogger->isEnabled())->toBeFalse();
});
it('analyzes logged queries', function () {
// Enable logging
$this->service->startLogging();
// Simulate N+1 queries
for ($i = 1; $i <= 10; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM posts WHERE user_id = ?',
bindings: [$i],
executionTimeMs: 5.0,
rowCount: 1
);
}
// Analyze
$result = $this->service->analyze();
expect($result)->toHaveKeys(['detections', 'strategies', 'statistics']);
expect($result['detections'])->not->toBeEmpty();
expect($result['statistics']['total_queries'])->toBe(10);
});
it('generates formatted report', function () {
$this->service->startLogging();
// Simulate N+1 queries
for ($i = 1; $i <= 10; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM posts WHERE user_id = ?',
bindings: [$i],
executionTimeMs: 5.0,
rowCount: 1
);
}
$report = $this->service->analyzeAndReport();
expect($report)->toContain('N+1 Query Detection Report');
expect($report)->toContain('Query Statistics');
expect($report)->toContain('N+1 Problems Detected');
});
it('profiles code execution', function () {
$executedCallable = false;
$analysis = $this->service->profile(function () use (&$executedCallable) {
$executedCallable = true;
return 'result';
});
expect($executedCallable)->toBeTrue();
expect($analysis)->toHaveKey('execution_time_ms');
expect($analysis)->toHaveKey('callback_result');
expect($analysis['callback_result'])->toBe('result');
});
it('returns critical problems only', function () {
$this->service->startLogging();
// Add critical N+1 (high execution count + slow execution)
// Severity calculation: exec_count(50)=+3, avg_time(55ms)=+3, consistent_caller=+2, total_time(2750ms)=+1 → 9 points (CRITICAL)
for ($i = 1; $i <= 50; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM posts WHERE user_id = ?',
bindings: [$i],
executionTimeMs: 55.0, // Changed from 10.0 to 55.0 to reach CRITICAL severity
rowCount: 1
);
}
// Add low severity N+1
for ($i = 1; $i <= 6; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM comments WHERE post_id = ?',
bindings: [$i],
executionTimeMs: 1.0,
rowCount: 1
);
}
$critical = $this->service->getCriticalProblems();
// Should only contain the high-severity posts pattern
expect($critical)->toHaveCount(1);
expect($critical[0]->isCritical())->toBeTrue();
});
it('quick check detects N+1 problems', function () {
$this->service->startLogging();
for ($i = 1; $i <= 10; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM posts WHERE user_id = ?',
bindings: [$i],
executionTimeMs: 5.0,
rowCount: 1
);
}
expect($this->service->hasNPlusOneProblems())->toBeTrue();
});
it('returns query statistics', function () {
$this->service->startLogging();
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0, 10);
$this->queryLogger->logQuery('INSERT INTO logs (message) VALUES (?)', ['test'], 2.0, 1);
$this->queryLogger->logQuery('UPDATE users SET name = ? WHERE id = ?', ['John', 1], 3.0, 1);
$stats = $this->service->getQueryStatistics();
expect($stats['total_queries'])->toBe(3);
expect($stats['select_count'])->toBe(1);
expect($stats['insert_count'])->toBe(1);
expect($stats['update_count'])->toBe(1);
});
it('clears query logs', function () {
$this->service->startLogging();
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0);
expect($this->service->getQueryLogs())->toHaveCount(1);
$this->service->clearLogs();
expect($this->service->getQueryLogs())->toBeEmpty();
});
it('integrates detections with eager loading strategies', function () {
$this->service->startLogging();
// Create N+1 pattern (need >5 queries and severity >= 4)
// Severity: exec_count(15)=+2, avg_time(25ms)=+2, consistent_caller=+2, total_time=+0 → 6 points
for ($i = 1; $i <= 15; $i++) {
$this->queryLogger->logQuery(
sql: 'SELECT * FROM posts WHERE user_id = ?',
bindings: [$i],
executionTimeMs: 25.0, // Increased from 8.0 to reach severity >= 4
rowCount: 1
);
}
$result = $this->service->analyze();
expect($result['detections'])->toHaveCount(1);
expect($result['strategies'])->toHaveCount(1);
$strategy = $result['strategies'][0];
expect($strategy->tableName)->toBe('posts');
expect($strategy->codeExample)->toContain('Eager Loading'); // Updated to match actual case
});
it('handles empty query logs gracefully', function () {
$this->service->startLogging();
$result = $this->service->analyze();
expect($result['detections'])->toBeEmpty();
expect($result['strategies'])->toBeEmpty();
expect($result['statistics']['total_queries'])->toBe(0);
});
it('logs analysis completion', function () {
// Expect 2 log calls: "Starting analysis" and "Analysis completed"
$this->logger->expects($this->exactly(2))
->method('info')
->willReturnCallback(function ($message) {
// Only assert on the second call (Analysis completed)
if (str_contains($message, 'Analysis completed')) {
expect($message)->toContain('Analysis completed');
}
});
$this->service->startLogging();
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0);
$this->service->analyze();
});
});