Files
Gnuboard7/tests/Unit/Seo/SeoCacheStatsServiceTest.php
T
2026-04-01 10:30:52 +09:00

284 lines
12 KiB
PHP

<?php
namespace Tests\Unit\Seo;
use App\Models\SeoCacheStat;
use App\Seo\SeoCacheStatsService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class SeoCacheStatsServiceTest extends TestCase
{
use RefreshDatabase;
private SeoCacheStatsService $statsService;
/**
* 테스트 초기화 - SeoCacheStatsService 인스턴스를 준비합니다.
*/
protected function setUp(): void
{
parent::setUp();
$this->statsService = new SeoCacheStatsService;
}
/**
* recordHit이 type='hit'인 레코드를 생성합니다.
*/
public function test_record_hit_creates_hit_record(): void
{
$this->statsService->recordHit('/products/1', 'ko', 'shop/show', 'sirsoft-ecommerce');
$this->assertDatabaseHas('seo_cache_stats', [
'url' => '/products/1',
'locale' => 'ko',
'layout_name' => 'shop/show',
'module_identifier' => 'sirsoft-ecommerce',
'type' => 'hit',
'response_time_ms' => null,
]);
}
/**
* recordMiss가 type='miss'이고 response_time_ms를 포함한 레코드를 생성합니다.
*/
public function test_record_miss_creates_miss_record_with_response_time(): void
{
$this->statsService->recordMiss('/products/1', 'ko', 'shop/show', 'sirsoft-ecommerce', 150);
$this->assertDatabaseHas('seo_cache_stats', [
'url' => '/products/1',
'locale' => 'ko',
'layout_name' => 'shop/show',
'module_identifier' => 'sirsoft-ecommerce',
'type' => 'miss',
'response_time_ms' => 150,
]);
}
/**
* recordHit에서 예외 발생 시 Log::warning으로 처리됩니다.
*/
public function test_record_hit_handles_exception_gracefully(): void
{
Log::shouldReceive('warning')
->once()
->withArgs(function (string $message, array $context) {
return str_contains($message, '캐시 히트 기록 실패')
&& $context['url'] === '/products/1';
});
// 테이블을 삭제하여 DB 예외를 강제 발생
Schema::drop('seo_cache_stats');
// 예외가 전파되지 않음을 확인
$this->statsService->recordHit('/products/1', 'ko');
}
/**
* recordMiss에서 예외 발생 시 Log::warning으로 처리됩니다.
*/
public function test_record_miss_handles_exception_gracefully(): void
{
Log::shouldReceive('warning')
->once()
->withArgs(function (string $message, array $context) {
return str_contains($message, '캐시 미스 기록 실패')
&& $context['url'] === '/products/1';
});
// 테이블을 삭제하여 DB 예외를 강제 발생
Schema::drop('seo_cache_stats');
// 예외가 전파되지 않음을 확인
$this->statsService->recordMiss('/products/1', 'ko', null, null, 200);
}
/**
* 데이터가 없을 때 getStats가 기본값을 반환합니다.
*/
public function test_get_stats_returns_defaults_with_no_data(): void
{
$stats = $this->statsService->getStats();
$this->assertSame(0, $stats['total_entries']);
$this->assertSame(0, $stats['hits']);
$this->assertSame(0, $stats['misses']);
$this->assertSame(0.0, $stats['hit_rate']);
$this->assertNull($stats['avg_response_time_ms']);
}
/**
* getStats가 올바른 히트율을 계산합니다.
*/
public function test_get_stats_returns_correct_hit_rate(): void
{
// 3개 히트, 2개 미스 = 60% 히트율
SeoCacheStat::create(['url' => '/p/1', 'locale' => 'ko', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/2', 'locale' => 'ko', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/3', 'locale' => 'ko', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/4', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 100]);
SeoCacheStat::create(['url' => '/p/5', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 200]);
$stats = $this->statsService->getStats();
$this->assertSame(5, $stats['total_entries']);
$this->assertSame(3, $stats['hits']);
$this->assertSame(2, $stats['misses']);
$this->assertSame(60.0, $stats['hit_rate']);
}
/**
* getStats가 since 파라미터로 기간 필터링합니다.
*/
public function test_get_stats_filters_by_since_date(): void
{
// 오래된 레코드 (5일 전)
$old = SeoCacheStat::create(['url' => '/old', 'locale' => 'ko', 'type' => 'hit']);
$old->created_at = Carbon::now()->subDays(5);
$old->save();
// 최근 레코드 (1일 전)
$recent = SeoCacheStat::create(['url' => '/recent', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 100]);
$recent->created_at = Carbon::now()->subDays(1);
$recent->save();
// 3일 이내 데이터만 조회
$stats = $this->statsService->getStats(Carbon::now()->subDays(3));
$this->assertSame(1, $stats['total_entries']);
$this->assertSame(0, $stats['hits']);
$this->assertSame(1, $stats['misses']);
}
/**
* getStats가 미스 레코드의 평균 응답 시간을 계산합니다.
*/
public function test_get_stats_calculates_avg_response_time_from_misses_only(): void
{
// 히트 (response_time_ms 없음)
SeoCacheStat::create(['url' => '/p/1', 'locale' => 'ko', 'type' => 'hit']);
// 미스 (response_time_ms 있음) - 평균 = (100 + 300) / 2 = 200
SeoCacheStat::create(['url' => '/p/2', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 100]);
SeoCacheStat::create(['url' => '/p/3', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 300]);
// 미스이지만 response_time_ms가 null
SeoCacheStat::create(['url' => '/p/4', 'locale' => 'ko', 'type' => 'miss']);
$stats = $this->statsService->getStats();
$this->assertSame(4, $stats['total_entries']);
$this->assertSame(200.0, $stats['avg_response_time_ms']);
}
/**
* getStatsByLayout이 레이아웃별로 통계를 그룹화합니다.
*/
public function test_get_stats_by_layout_groups_by_layout_name(): void
{
// shop/show 레이아웃: 2히트, 1미스
SeoCacheStat::create(['url' => '/p/1', 'locale' => 'ko', 'layout_name' => 'shop/show', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/2', 'locale' => 'ko', 'layout_name' => 'shop/show', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/3', 'locale' => 'ko', 'layout_name' => 'shop/show', 'type' => 'miss', 'response_time_ms' => 150]);
// shop/category 레이아웃: 1히트, 1미스
SeoCacheStat::create(['url' => '/c/1', 'locale' => 'ko', 'layout_name' => 'shop/category', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/c/2', 'locale' => 'ko', 'layout_name' => 'shop/category', 'type' => 'miss', 'response_time_ms' => 200]);
$result = $this->statsService->getStatsByLayout();
$this->assertCount(2, $result);
// layout_name 기준으로 정렬하여 검증
$byLayout = collect($result)->keyBy('layout_name');
$showStats = $byLayout['shop/show'];
$this->assertSame(3, $showStats['total']);
$this->assertSame(2, $showStats['hits']);
$this->assertSame(1, $showStats['misses']);
$this->assertSame(66.67, $showStats['hit_rate']);
$this->assertSame(150.0, $showStats['avg_response_time_ms']);
$categoryStats = $byLayout['shop/category'];
$this->assertSame(2, $categoryStats['total']);
$this->assertSame(1, $categoryStats['hits']);
$this->assertSame(1, $categoryStats['misses']);
$this->assertSame(50.0, $categoryStats['hit_rate']);
$this->assertSame(200.0, $categoryStats['avg_response_time_ms']);
}
/**
* getStatsByModule이 모듈별로 통계를 그룹화합니다.
*/
public function test_get_stats_by_module_groups_by_module_identifier(): void
{
// sirsoft-ecommerce 모듈: 3히트, 1미스
SeoCacheStat::create(['url' => '/p/1', 'locale' => 'ko', 'module_identifier' => 'sirsoft-ecommerce', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/2', 'locale' => 'ko', 'module_identifier' => 'sirsoft-ecommerce', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/3', 'locale' => 'ko', 'module_identifier' => 'sirsoft-ecommerce', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/p/4', 'locale' => 'ko', 'module_identifier' => 'sirsoft-ecommerce', 'type' => 'miss', 'response_time_ms' => 250]);
// sirsoft-blog 모듈: 1히트, 2미스
SeoCacheStat::create(['url' => '/b/1', 'locale' => 'ko', 'module_identifier' => 'sirsoft-blog', 'type' => 'hit']);
SeoCacheStat::create(['url' => '/b/2', 'locale' => 'ko', 'module_identifier' => 'sirsoft-blog', 'type' => 'miss', 'response_time_ms' => 100]);
SeoCacheStat::create(['url' => '/b/3', 'locale' => 'ko', 'module_identifier' => 'sirsoft-blog', 'type' => 'miss', 'response_time_ms' => 300]);
$result = $this->statsService->getStatsByModule();
$this->assertCount(2, $result);
$byModule = collect($result)->keyBy('module_identifier');
$ecommerceStats = $byModule['sirsoft-ecommerce'];
$this->assertSame(4, $ecommerceStats['total']);
$this->assertSame(3, $ecommerceStats['hits']);
$this->assertSame(1, $ecommerceStats['misses']);
$this->assertSame(75.0, $ecommerceStats['hit_rate']);
$this->assertSame(250.0, $ecommerceStats['avg_response_time_ms']);
$blogStats = $byModule['sirsoft-blog'];
$this->assertSame(3, $blogStats['total']);
$this->assertSame(1, $blogStats['hits']);
$this->assertSame(2, $blogStats['misses']);
$this->assertSame(33.33, $blogStats['hit_rate']);
$this->assertSame(200.0, $blogStats['avg_response_time_ms']);
}
/**
* cleanup이 오래된 레코드를 삭제하고 최근 레코드를 유지합니다.
*/
public function test_cleanup_deletes_old_records_and_keeps_recent(): void
{
// 오래된 레코드 (40일 전)
$old1 = SeoCacheStat::create(['url' => '/old/1', 'locale' => 'ko', 'type' => 'hit']);
$old1->created_at = Carbon::now()->subDays(40);
$old1->save();
$old2 = SeoCacheStat::create(['url' => '/old/2', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 100]);
$old2->created_at = Carbon::now()->subDays(35);
$old2->save();
// 최근 레코드 (10일 전)
$recent1 = SeoCacheStat::create(['url' => '/recent/1', 'locale' => 'ko', 'type' => 'hit']);
$recent1->created_at = Carbon::now()->subDays(10);
$recent1->save();
// 오늘 레코드
SeoCacheStat::create(['url' => '/today/1', 'locale' => 'ko', 'type' => 'miss', 'response_time_ms' => 200]);
$deleted = $this->statsService->cleanup(30);
$this->assertSame(2, $deleted);
$this->assertDatabaseCount('seo_cache_stats', 2);
$this->assertDatabaseMissing('seo_cache_stats', ['url' => '/old/1']);
$this->assertDatabaseMissing('seo_cache_stats', ['url' => '/old/2']);
$this->assertDatabaseHas('seo_cache_stats', ['url' => '/recent/1']);
$this->assertDatabaseHas('seo_cache_stats', ['url' => '/today/1']);
}
}