GooglePlaces::fake() swaps the live client for a recording GooglePlacesFake and returns it — no Http::fake(), no live (paid) calls. Nothing reaches Google: every call is recorded and answered from what the with*() seeders queued:
use RoundlyConsulting\GooglePlaces\DataTransferObjects\AutocompletePrediction;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\AutocompleteQuery;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\Place;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
$fake = GooglePlaces::fake();
$fake->withAutocomplete([new AutocompletePrediction('Café Roma', 'p1', ['cafe'])]);
$fake->withDetails(new Place('Café Roma'));
// ...exercise code that calls the facade...
$fake->assertAutocompleted(fn (AutocompleteQuery $q) => $q->input === 'Café');
$fake->assertDetailsRequested('p1');
$fake->assertNothingGeocoded();The fake is a PlacesClient installed behind the facade and the container binding, so constructor-injected clients, session(), matrix(), photo() (including save(), which writes the seeded bytes), the paginated search helpers, the google-places:check command and the google_places geolocation driver all hit it.
Seeding results
| Seeder | Feeds | When not seeded |
|---|---|---|
withAutocomplete(array $predictions) | autocomplete() | Empty collection. |
withDetails(?Place $place) | details() | null. |
withGeocode(array $results) | geocode() | Empty collection. |
withGeocodeAddress(array $results) | geocodeAddress() | Empty collection. |
withTextSearch(array $places) | textSearch(), textSearchPaginated() | Empty; the paginator returns one page. |
withNearbySearch(array $places) | nearbySearch(), nearbySearchPaginated() | Empty; the paginator returns one page. |
withFindPlace(?Place $place) | findPlace() | null. |
withDistance(Distance|MultipleDistances $distance) | distance() | A zero Distance. |
withMatrix(DistanceMatrix $matrix) | computeMatrix(), matrix()->…() | An empty matrix sized to the query. |
withPhoto(string $bytes, ?string $uri = null) | photoContents(), photoUri(), photo()->… | Empty bytes; a fake key-free URL built from the photo name. |
withPhotoUrl(string $url) | photoUrl() | A media URL built from the photo name. |
withCheck(array $results) | check(), google-places:check | Three healthy results. |
Assertions
Each call is recorded as its query object — scalars are normalised to the query class first. Without a callback an assertion passes if the call happened at all; with one, at least one recorded query must match:
| Assertion | Passes when |
|---|---|
assertAutocompleted(fn (AutocompleteQuery $q) => $q->input === 'Café') | An autocomplete call (matching the callback). |
assertDetailsRequested('p1') | A details call; a string matches the place id, a callback gets the DetailsQuery. |
assertGeocoded(fn (ReverseGeocodingQuery $q) => $q->location->latitude === 48.1486) | A reverse geocode. |
assertAddressGeocoded('Hlavné námestie 1, Bratislava') | A forward geocode; a string matches the address. |
assertTextSearched(fn (TextSearchQuery $q) => $q->textQuery === 'pizza') | A text search, paginated included. |
assertNearbySearched(fn (NearbySearchQuery $q) => $q->radius === 1500) | A nearby search, paginated included. |
assertFindPlaceRequested(fn (string $text) => $text === 'Eiffel Tower') | A findPlace() — the callback gets the search text only, not the bias. |
assertDistanceRequested(fn (DistanceQuery $q) => $q->type === TravelMode::Driving) | A distance() call. |
assertMatrixComputed(fn (MatrixQuery $q) => count($q->origins) === 2) | A matrix, via computeMatrix() or matrix()->…(). |
assertPhotoRequested('places/p1/photos/a') | A photo’s bytes or key-free URL; a callback gets the PhotoQuery (contents is false for a URL). |
assertChecked() | check() ran — directly or through google-places:check. |
assertNothingAutocompleted() | No autocomplete call. |
assertNothingGeocoded() | No reverse-geocode call. |
assertNothingRequested() | No call at all, except photoUrl() and building the handles. |
session(), matrix() and photo() themselves are not recorded — the calls the handles make are. Session calls land as ordinary autocomplete and details calls with the session token set on the query, so assert on $q->sessionToken. The fake never throws.
Building fake results
Results are plain DTOs, so you can construct them directly:
use RoundlyConsulting\GooglePlaces\DataTransferObjects\Distance;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\DistanceQuery;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\Geometry;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\Location;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\ReverseGeocodingResult;
use RoundlyConsulting\GooglePlaces\Enums\TravelMode;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
it('quotes a delivery time for a typed address', function () {
$fake = GooglePlaces::fake();
$point = new Location(48.1435, 17.1089);
$fake->withGeocodeAddress([
new ReverseGeocodingResult(
address: 'Hlavné námestie 1, 811 01 Bratislava, Slovakia',
placeId: 'p1',
geometry: new Geometry($point, $point, $point),
types: ['street_address'],
),
]);
$fake->withDistance(new Distance('4.2 km', 4200, '12m', 720, TravelMode::Driving));
// ...call the code under test...
$fake->assertAddressGeocoded('Hlavné námestie 1, Bratislava');
$fake->assertDistanceRequested(fn (DistanceQuery $q) => $q->type === TravelMode::Driving);
$fake->assertNothingAutocompleted();
});Guarding against calls
use RoundlyConsulting\GooglePlaces\DataTransferObjects\Place;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\TextSearchQuery;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
it('renders the cached page without calling Google', function () {
$fake = GooglePlaces::fake();
$this->get('/places/cached')->assertOk();
$fake->assertNothingRequested(); // no lookup, photo request or check at all
});
it('searches by text and never geocodes', function () {
$fake = GooglePlaces::fake()->withTextSearch([new Place('Pizza Roma')]);
$this->get('/search?q=pizza')->assertOk();
$fake->assertTextSearched(fn (TextSearchQuery $q) => $q->textQuery === 'pizza');
$fake->assertNothingAutocompleted();
$fake->assertNothingGeocoded();
});Photos
Seed the bytes with withPhoto() and pair the fake with Storage::fake():
use Illuminate\Support\Facades\Storage;
use RoundlyConsulting\GooglePlaces\DataTransferObjects\PhotoQuery;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
it('stores the hero photo', function () {
Storage::fake('public');
$fake = GooglePlaces::fake()->withPhoto('JPEG-BYTES');
GooglePlaces::photo('places/p1/photos/a', 1200, 800)->save('public', 'p1.jpg');
Storage::disk('public')->assertExists('p1.jpg');
$fake->assertPhotoRequested(fn (PhotoQuery $q) => $q->name === 'places/p1/photos/a' && $q->maxWidth === 1200);
});The health check
use RoundlyConsulting\GooglePlaces\DataTransferObjects\ApiCheckResult;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
config(['google-places.key' => 'test-key']); // the command refuses to run without a key
$fake = GooglePlaces::fake()->withCheck([
new ApiCheckResult('Places API (New)', true, 'Reachable and authorized.'),
new ApiCheckResult('Routes API', false, 'Unreachable: timeout'),
new ApiCheckResult('Geocoding API', true, 'Reachable and authorized.'),
]);
$this->artisan('google-places:check')->assertFailed();
$fake->assertChecked();Testing against the real client
To exercise request building, error classification, caching or rate limiting, keep the real client and fake the HTTP layer instead:
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\GooglePlaces\Exceptions\PlacesException;
use RoundlyConsulting\GooglePlaces\Facades\GooglePlaces;
config(['google-places.key' => 'test-key', 'google-places.rate_limits.places.enabled' => false]);
Http::fake([
'places.googleapis.com/v1/places:searchText' => Http::response([
'error' => [
'status' => 'INVALID_ARGUMENT',
'message' => 'API key not valid.',
'details' => [['@type' => 'type.googleapis.com/google.rpc.ErrorInfo', 'reason' => 'API_KEY_INVALID']],
],
], 400),
]);
try {
GooglePlaces::textSearch('coffee');
} catch (PlacesException $e) {
expect($e->isDenied())->toBeTrue()
->and($e->isInvalidRequest())->toBeFalse()
->and($e->googleReason())->toBe('API_KEY_INVALID');
}To run the package’s own suite from its repository:
composer testShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.