178 lines
6.4 KiB
PHP
178 lines
6.4 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Feature\Performance;
|
||
|
|
|
||
|
|
use App\Models\Common\Dashboard;
|
||
|
|
use App\Models\Common\Item;
|
||
|
|
use App\Models\Document\Document;
|
||
|
|
use App\Models\Setting\Tax;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Tests\Feature\FeatureTestCase;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test class to validate N+1 query optimizations
|
||
|
|
* Ensures performance improvements are maintained and prevents regression
|
||
|
|
*/
|
||
|
|
class N1QueryOptimizationTest extends FeatureTestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test that the dashboard index page optimization works
|
||
|
|
* This tests the actual fix we implemented in DashboardController
|
||
|
|
*/
|
||
|
|
public function testDashboardIndexOptimization()
|
||
|
|
{
|
||
|
|
$this->loginAs();
|
||
|
|
|
||
|
|
// Create multiple dashboards
|
||
|
|
$dashboards = Dashboard::factory()->count(3)->create([
|
||
|
|
'company_id' => $this->company->id
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Attach user to dashboards
|
||
|
|
foreach ($dashboards as $dashboard) {
|
||
|
|
if (!$dashboard->users()->where('user_id', $this->user->id)->exists()) {
|
||
|
|
$dashboard->users()->attach($this->user->id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test our actual optimization: the way we load dashboards in the controller
|
||
|
|
DB::enableQueryLog();
|
||
|
|
|
||
|
|
// This is exactly what we do in the optimized DashboardController
|
||
|
|
$optimizedDashboards = user()->dashboards()->with('users')->collect();
|
||
|
|
|
||
|
|
$queryCount = count(DB::getQueryLog());
|
||
|
|
DB::disableQueryLog();
|
||
|
|
|
||
|
|
// We should have dashboards and the users should be eager loaded
|
||
|
|
$this->assertGreaterThan(0, $optimizedDashboards->count());
|
||
|
|
|
||
|
|
// Verify users relationship is loaded (preventing N+1 in the view)
|
||
|
|
foreach ($optimizedDashboards as $dashboard) {
|
||
|
|
$this->assertTrue($dashboard->relationLoaded('users'), 'Users relationship should be eager loaded');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query count should be reasonable (not N+1)
|
||
|
|
$this->assertLessThan(20, $queryCount, 'Should not use excessive queries');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test item autocomplete optimization
|
||
|
|
* This validates our fix in the Items controller autocomplete method
|
||
|
|
*/
|
||
|
|
public function testItemAutocompleteOptimization()
|
||
|
|
{
|
||
|
|
$this->loginAs();
|
||
|
|
|
||
|
|
// Create items with taxes (like real data)
|
||
|
|
$tax = Tax::factory()->create(['company_id' => $this->company->id, 'rate' => 10]);
|
||
|
|
$items = Item::factory()->count(3)->create(['company_id' => $this->company->id]);
|
||
|
|
|
||
|
|
foreach ($items as $item) {
|
||
|
|
$item->taxes()->create([
|
||
|
|
'company_id' => $this->company->id,
|
||
|
|
'tax_id' => $tax->id
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test our actual optimization from the Items controller
|
||
|
|
DB::enableQueryLog();
|
||
|
|
|
||
|
|
// This is exactly what we do in the optimized autocomplete method
|
||
|
|
$optimizedItems = Item::with(['taxes.tax'])->take(3)->get();
|
||
|
|
|
||
|
|
$queryCount = count(DB::getQueryLog());
|
||
|
|
DB::disableQueryLog();
|
||
|
|
|
||
|
|
// Verify relationships are loaded (preventing N+1 when processing taxes)
|
||
|
|
foreach ($optimizedItems as $item) {
|
||
|
|
$this->assertTrue($item->relationLoaded('taxes'), 'Item taxes should be eager loaded');
|
||
|
|
|
||
|
|
if ($item->taxes->count() > 0) {
|
||
|
|
$firstTax = $item->taxes->first();
|
||
|
|
$this->assertTrue($firstTax->relationLoaded('tax'), 'Tax details should be eager loaded');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Should use reasonable number of queries (not N+1)
|
||
|
|
$this->assertLessThan(10, $queryCount, 'Autocomplete should not use excessive queries');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test that our optimizations prevent the classic N+1 scenario
|
||
|
|
* This validates that eager loading works correctly
|
||
|
|
*/
|
||
|
|
public function testN1Prevention()
|
||
|
|
{
|
||
|
|
$this->loginAs();
|
||
|
|
|
||
|
|
// Create items with taxes
|
||
|
|
$items = Item::factory()->count(5)->create(['company_id' => $this->company->id]);
|
||
|
|
$tax = Tax::factory()->create(['company_id' => $this->company->id]);
|
||
|
|
|
||
|
|
foreach ($items as $item) {
|
||
|
|
$item->taxes()->create([
|
||
|
|
'company_id' => $this->company->id,
|
||
|
|
'tax_id' => $tax->id
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test that our optimization pattern works correctly
|
||
|
|
DB::enableQueryLog();
|
||
|
|
|
||
|
|
// This is the optimized pattern we use in our fixes
|
||
|
|
$optimizedItems = Item::with('taxes')->take(5)->get();
|
||
|
|
|
||
|
|
// Verify relationships are loaded
|
||
|
|
foreach ($optimizedItems as $item) {
|
||
|
|
$this->assertTrue($item->relationLoaded('taxes'), 'Taxes should be eager loaded');
|
||
|
|
// Access the relationship without triggering additional queries
|
||
|
|
$taxCount = $item->taxes->count();
|
||
|
|
$this->assertGreaterThanOrEqual(0, $taxCount);
|
||
|
|
}
|
||
|
|
|
||
|
|
$queryCount = count(DB::getQueryLog());
|
||
|
|
DB::disableQueryLog();
|
||
|
|
|
||
|
|
// Should use reasonable number of queries with eager loading
|
||
|
|
$this->assertLessThan(10, $queryCount, 'Eager loading should use reasonable number of queries');
|
||
|
|
|
||
|
|
// Verify all items have properly loaded relationships
|
||
|
|
$this->assertEquals(5, $optimizedItems->count());
|
||
|
|
foreach ($optimizedItems as $item) {
|
||
|
|
$this->assertTrue($item->relationLoaded('taxes'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test real-world controller method performance
|
||
|
|
* This tests the actual methods we optimized
|
||
|
|
*/
|
||
|
|
public function testControllerOptimizations()
|
||
|
|
{
|
||
|
|
$this->loginAs();
|
||
|
|
|
||
|
|
// Test dashboard controller optimization
|
||
|
|
$response = $this->get(route('dashboards.index'));
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Create a document to test document controllers
|
||
|
|
$document = Document::factory()->invoice()->create([
|
||
|
|
'company_id' => $this->company->id
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Test invoice show optimization
|
||
|
|
$response = $this->get(route('invoices.show', $document));
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// Test items autocomplete optimization
|
||
|
|
$response = $this->get(route('items.autocomplete') . '?query=test');
|
||
|
|
$response->assertOk();
|
||
|
|
|
||
|
|
// If we get here without timeouts, our optimizations are working
|
||
|
|
$this->assertTrue(true, 'All optimized endpoints respond without performance issues');
|
||
|
|
}
|
||
|
|
}
|