url => сколько раз спросили */ public array $htmlCalls = []; /** @var array url => сколько раз спросили в пачке */ public array $batchCalls = []; /** @param array $responses url => ответ */ public function __construct(private array $responses = []) {} public function html(string $url): string { $this->htmlCalls[$url] = ($this->htmlCalls[$url] ?? 0) + 1; return $this->responses[$url] ?? ''; } public function htmlBatch(array $urls): array { $out = []; foreach ($urls as $url) { $this->batchCalls[$url] = ($this->batchCalls[$url] ?? 0) + 1; $out[$url] = $this->responses[$url] ?? ''; } return $out; } } beforeEach(function () { config()->set('cache.default', 'array'); Cache::store('array')->flush(); }); function caching(SpyBatchFetcher $inner, int $ttlDays = 5): CachingPageFetcher { return new CachingPageFetcher($inner, ttlDays: $ttlDays); } it('реализует оба интерфейса', function () { $c = caching(new SpyBatchFetcher); expect($c)->toBeInstanceOf(PageFetcher::class) ->and($c)->toBeInstanceOf(BatchPageFetcher::class); }); it('карточка 2ГИС: второй вызов — из кэша (inner дёрнут один раз)', function () { $url = 'https://2gis.ru/krasnoyarsk/firm/70000001045067758'; $inner = new SpyBatchFetcher([$url => '{"state":1}']); $c = caching($inner); expect($c->html($url))->toBe('{"state":1}') ->and($c->html($url))->toBe('{"state":1}'); expect($inner->htmlCalls[$url])->toBe(1); }); it('карточка Яндекс /maps/org/ тоже кэшируется', function () { $url = 'https://yandex.ru/maps/org/metropolis/1234567890'; $inner = new SpyBatchFetcher([$url => 'ya']); $c = caching($inner); expect($c->html($url))->toBe('ya') ->and($c->html($url))->toBe('ya'); expect($inner->htmlCalls[$url])->toBe(1); }); it('поиск /search/ — всегда мимо кэша (inner каждый раз)', function () { $url = 'https://2gis.ru/krasnoyarsk/search/ломбард'; $inner = new SpyBatchFetcher([$url => 'SEARCH']); $c = caching($inner); expect($c->html($url))->toBe('SEARCH') ->and($c->html($url))->toBe('SEARCH'); expect($inner->htmlCalls[$url])->toBe(2); }); it('пустой ответ карточки НЕ кэшируется — inner дёрнут снова', function () { $url = 'https://2gis.ru/krasnoyarsk/firm/999'; $inner = new SpyBatchFetcher([]); // всегда пусто $c = caching($inner); expect($c->html($url))->toBe('') ->and($c->html($url))->toBe(''); expect($inner->htmlCalls[$url])->toBe(2); }); it('htmlBatch: часть из кэша, часть добирается', function () { $cached = 'https://2gis.ru/krasnoyarsk/firm/111'; $fresh = 'https://2gis.ru/krasnoyarsk/firm/222'; $inner = new SpyBatchFetcher([$cached => 'A', $fresh => 'B']); $c = caching($inner); // Первый заход — прогреть кэш только для $cached. expect($c->html($cached))->toBe('A'); expect($inner->htmlCalls[$cached])->toBe(1); // Пачка: $cached уже в кэше (inner НЕ спрашивают), $fresh добирается. $out = $c->htmlBatch([$cached, $fresh]); expect($out)->toBe([$cached => 'A', $fresh => 'B']); expect($inner->batchCalls[$cached] ?? 0)->toBe(0) ->and($inner->batchCalls[$fresh])->toBe(1); }); it('htmlBatch: /search/ всегда добирается, карточка после первого раза — из кэша', function () { $search = 'https://2gis.ru/krasnoyarsk/search/шины'; $card = 'https://2gis.ru/krasnoyarsk/firm/333'; $inner = new SpyBatchFetcher([$search => 'S', $card => 'C']); $c = caching($inner); $c->htmlBatch([$search, $card]); // прогрев: card в кэш, search — нет $out = $c->htmlBatch([$search, $card]); // Порядок в карте url→контент не гарантирован — сверяем по ключам. expect($out[$search])->toBe('S')->and($out[$card])->toBe('C'); // search добирался оба раза, card — только в первый. expect($inner->batchCalls[$search])->toBe(2) ->and($inner->batchCalls[$card])->toBe(1); }); it('htmlBatch: пустой ответ карточки не кэшируется', function () { $card = 'https://2gis.ru/krasnoyarsk/firm/444'; $inner = new SpyBatchFetcher([]); // пусто $c = caching($inner); $c->htmlBatch([$card]); $c->htmlBatch([$card]); expect($inner->batchCalls[$card])->toBe(2); });