performanceCollector = Mockery::mock(PerformanceCollectorInterface::class); $this->storage = Mockery::mock(AnalyticsStorage::class); $this->random = Mockery::mock(RandomGenerator::class); // Create real ServerEnvironment with test data (final class, can't be mocked) $this->serverEnvironment = new ServerEnvironment([ 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_USER_AGENT' => 'Test-Agent/1.0', 'REQUEST_URI' => '/test', 'HTTP_REFERER' => 'https://example.com', ]); // Allow any performance collector calls (framework internal) $this->performanceCollector ->shouldReceive('recordMetric') ->zeroOrMoreTimes(); $this->performanceCollector ->shouldReceive('increment') ->zeroOrMoreTimes(); // Allow storage aggregated calls (framework internal) $this->storage ->shouldReceive('storeAggregated') ->zeroOrMoreTimes(); // Allow random float calls for sampling (may or may not be called) $this->random ->shouldReceive('float') ->with(0, 1) ->zeroOrMoreTimes() ->andReturn(0.5); // Allow random bytes calls for session ID generation (may or may not be called) $this->random ->shouldReceive('bytes') ->with(16) ->zeroOrMoreTimes() ->andReturn(str_repeat('a', 16)); // Default: tracking enabled, 100% sampling for tests $this->collector = new AnalyticsCollector( performanceCollector: $this->performanceCollector, storage: $this->storage, random: $this->random, serverEnvironment: $this->serverEnvironment, enabled: true, samplingRate: 1.0 ); }); afterEach(function () { Mockery::close(); }); describe('trackAction', function () { it('tracks user action with category and properties', function () { // Expect raw data storage with flexible array matcher $this->storage ->shouldReceive('storeRawData') ->once() ->with( Mockery::on(function ($data) { return is_array($data) && $data['category'] === 'user_behavior' && $data['action'] === 'button_click' && isset($data['session_id']) && isset($data['timestamp']) && isset($data['button_id']) && $data['button_id'] === 'submit-btn'; }), 1.0 ); // Track action $this->collector->trackAction( action: 'button_click', category: AnalyticsCategory::USER_BEHAVIOR, properties: ['button_id' => 'submit-btn'] ); }); it('does not track when analytics disabled', function () { // Create collector with analytics disabled $disabledCollector = new AnalyticsCollector( performanceCollector: $this->performanceCollector, storage: $this->storage, random: $this->random, serverEnvironment: $this->serverEnvironment, enabled: false, // Disabled samplingRate: 1.0 ); // Storage should NOT be called $this->storage->shouldNotReceive('storeRawData'); // Track action (should be ignored) $disabledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR); }); it('respects sampling rate', function () { // Create new Random mock for this test $randomMock = Mockery::mock(RandomGenerator::class); // Random returns 0.6 (above 0.5 threshold) -> should NOT track (0.6 > 0.5) $randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.6); // Create collector with 50% sampling $sampledCollector = new AnalyticsCollector( performanceCollector: $this->performanceCollector, storage: $this->storage, random: $randomMock, serverEnvironment: $this->serverEnvironment, enabled: true, samplingRate: 0.5 ); // Storage should NOT be called (sampled out) $this->storage->shouldNotReceive('storeRawData'); // Track action (should be sampled out) $sampledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR); }); }); describe('trackPageView', function () { it('tracks page view with path and title', function () { // Expect raw data storage with flexible matcher $this->storage ->shouldReceive('storeRawData') ->once() ->with( Mockery::on(function ($data) { return is_array($data) && $data['path'] === '/dashboard' && $data['title'] === 'Dashboard' && isset($data['timestamp']) && isset($data['session_id']); }), 1.0 ); // Track page view $this->collector->trackPageView( path: '/dashboard', title: 'Dashboard' ); }); }); describe('trackError', function () { it('tracks error with type and message', function () { // trackError only logs to performance collector, not storage // Storage expectations are handled by global mocks // Track error $this->collector->trackError( errorType: 'ValidationException', message: 'Invalid email format' ); // Test passes if no exceptions are thrown expect(true)->toBeTrue(); }); }); describe('trackBusinessEvent', function () { it('tracks business event with value and currency', function () { // trackBusinessEvent only logs to performance collector, not storage // Storage expectations are handled by global mocks // Track business event $this->collector->trackBusinessEvent( event: 'purchase_completed', value: 99.99, currency: 'EUR' ); // Test passes if no exceptions are thrown expect(true)->toBeTrue(); }); }); describe('trackApiCall', function () { it('tracks API call with endpoint and metrics', function () { // trackApiCall only logs to performance collector, not storage // Storage expectations are handled by global mocks // Track API call $this->collector->trackApiCall( endpoint: '/api/users', method: 'GET', responseCode: 200, responseTime: 0.125 ); // Test passes if no exceptions are thrown expect(true)->toBeTrue(); }); }); describe('edge cases', function () { it('handles zero sampling rate', function () { // Create new Random mock for this test $randomMock = Mockery::mock(RandomGenerator::class); // Random float will be called and return value > 0.0 (will fail sampling) $randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.1); // Create collector with 0% sampling (no tracking) $noSamplingCollector = new AnalyticsCollector( performanceCollector: $this->performanceCollector, storage: $this->storage, random: $randomMock, serverEnvironment: $this->serverEnvironment, enabled: true, samplingRate: 0.0 ); // Storage should NOT be called $this->storage->shouldNotReceive('storeRawData'); // Track action (should be sampled out) $noSamplingCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR); }); it('handles full sampling rate', function () { // With 100% sampling, float() should NOT be called (early return) // Expect storage to be called $this->storage ->shouldReceive('storeRawData') ->once() ->with(Mockery::type('array'), 1.0); // Track action (should be tracked) $this->collector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR); }); }); });