Advertisements::fake() swaps in RoundlyConsulting\Advertisements\Testing\AdvertisementsFake — a subtype of AdvertisementManager, installed behind the facade and in the container, so constructor-injected managers and the model methods ($ad->publish(), $ad->delete(), …) hit it too. It returns the fake as your assertion handle:
use RoundlyConsulting\Advertisements\Facades\Advertisements;
use RoundlyConsulting\Advertisements\Models\Advertisement;
use RoundlyConsulting\Advertisements\Models\Placement;
use RoundlyConsulting\Advertisements\Testing\RecordedEvent;
use RoundlyConsulting\Advertisements\Testing\RecordedPlacements;
it('launches an ad in the sidebar and counts an impression', function () {
$fake = Advertisements::fake();
Placement::factory()->create(['slug' => 'sidebar']);
$ad = Advertisement::factory()->create();
Advertisements::for($ad)->placements()->attach(['sidebar']); // still runs: track() needs it
$ad->publish(); // model sugar — recorded too
Advertisements::for($ad)->track('sidebar')->impression(); // in memory only, returns null
$fake->assertPlacementsAttached(fn (RecordedPlacements $c) => $c->includes('sidebar'));
$fake->assertPublished($ad);
$fake->assertImpressionRecorded(fn (RecordedEvent $e) => $e->ad->is($ad) && $e->placement === 'sidebar');
$fake->assertNothingDeleted();
});
it('tracks nothing for a page without ads', function () {
$fake = Advertisements::fake();
// … render a page without placements …
$fake->assertNothingRecorded();
});- Lifecycle and placement changes still run against the database — reads and events behave normally — and are recorded.
- Tracking is recorded in memory only — no row, no counter, no job — and returns null. The live-ad and placement checks of track() still apply, so publish the ad and attach the placement before you track in it.
- Calling fake() again returns the fake already installed.
- The fake replaces the manager, not the actions — an action class you resolve directly (RecordImpression) still writes rows.
Every assertion
| Recorded | Assert | Negative |
|---|---|---|
create | assertCreated(…) | assertNothingCreated() |
update | assertUpdated(…) | assertNothingUpdated() |
publish / $ad->publish() | assertPublished(…) | assertNothingPublished() |
unpublish / $ad->unpublish() | assertUnpublished(…) | assertNothingUnpublished() |
expire / $ad->expire() | assertExpired(…) | assertNothingExpired() |
archive / $ad->archive() | assertArchived(…) | assertNothingArchived() |
delete / $ad->delete() | assertDeleted(…) | assertNothingDeleted() |
for($ad)->placements()->attach() | assertPlacementsAttached(…) | assertNothingAttached() |
for($ad)->placements()->detach() | assertPlacementsDetached(…) | assertNothingDetached() |
for($ad)->placements()->sync() | assertPlacementsSynced(…) | assertNothingSynced() |
for($ad)->track()->impression() | assertImpressionRecorded(…) | assertNothingRecorded() |
for($ad)->track()->click() | assertClickRecorded(…) | assertNothingRecorded() |
Each assertion takes an Advertisement, a Closure or nothing. Lifecycle closures receive the Advertisement; placement closures a RecordedPlacements (ad, placements as given, includes($placement) matching by model, id or slug); tracking closures a RecordedEvent (ad, type, placement as passed, data):
// Lifecycle — each takes an Advertisement, a Closure(Advertisement) or nothing
$fake->assertCreated(fn (Advertisement $ad) => $ad->name === 'Vintage road bike');
$fake->assertUpdated($ad);
$fake->assertPublished($ad);
$fake->assertUnpublished($ad);
$fake->assertExpired($ad);
$fake->assertArchived($ad);
$fake->assertDeleted($ad);
$fake->assertNothingCreated(); // … and assertNothingUpdated / Published / Unpublished /
$fake->assertNothingArchived(); // Expired / Archived / Deleted
// Placements — an Advertisement, a Closure(RecordedPlacements) or nothing
$fake->assertPlacementsAttached(fn (RecordedPlacements $c) => $c->includes('sidebar'));
$fake->assertPlacementsDetached($ad);
$fake->assertPlacementsSynced($ad);
$fake->assertNothingAttached();
$fake->assertNothingDetached();
$fake->assertNothingSynced();
// Tracking — an Advertisement, a Closure(RecordedEvent) or nothing
$fake->assertImpressionRecorded(fn (RecordedEvent $e) => $e->placement === 'sidebar');
$fake->assertClickRecorded($ad);
$fake->assertNothingRecorded(); // no impression and no clickFactories
Factories ship for testing and seeding. The advertisement factory has published(), scheduled(), expired() and archived() states; the event factory has impression() and click():
use RoundlyConsulting\Advertisements\Models\Advertisement;
use RoundlyConsulting\Advertisements\Models\AdvertisementEvent;
use RoundlyConsulting\Advertisements\Models\Category;
use RoundlyConsulting\Advertisements\Models\Placement;
Advertisement::factory()->create(); // a draft with a random EUR price
Advertisement::factory()->published()->create();
Advertisement::factory()->scheduled()->create();
Advertisement::factory()->expired()->create();
Advertisement::factory()->archived()->create();
Placement::factory()->create(['slug' => 'sidebar', 'width' => 300, 'height' => 250]);
Category::factory()->create(['slug' => 'vehicles', 'name' => ['en' => 'Vehicles']]);
AdvertisementEvent::factory()->impression()->create(['advertisement_id' => $ad->id]);
AdvertisementEvent::factory()->click()->create(['advertisement_id' => $ad->id]);Event factory rows don’t bump the ad’s counters — track through Advertisements::for($ad)->track() without the fake when a test needs impressions_count or clicks_count.
Faking geolocation
Seed geolocation-for-laravel’s fake with canned locations — no HTTP, no provider:
use RoundlyConsulting\Advertisements\DataTransferObjects\ImpressionData;
use RoundlyConsulting\Advertisements\Facades\Advertisements;
use RoundlyConsulting\Advertisements\Models\Advertisement;
use RoundlyConsulting\Geolocation\DataTransferObjects\Location;
use RoundlyConsulting\Geolocation\Enum\GeolocationType;
use RoundlyConsulting\Geolocation\Facades\Geolocation;
it('reports impressions per country', function () {
Geolocation::fake([
'203.0.113.7' => new Location(
humanReadable: 'Bratislava SK',
street: '',
city: 'Bratislava',
countryIsoCode: 'SK',
latitude: 48.1486,
longitude: 17.1077,
type: GeolocationType::Ip,
),
]);
$ad = Advertisement::factory()->published()->create();
Advertisements::for($ad)->track()->impression(new ImpressionData(ip: '203.0.113.7'));
expect($ad->impressionsByCountry())->toBe(['SK' => 1]);
});Lifecycle events
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Advertisements\Events\AdvertisementPublished;
use RoundlyConsulting\Advertisements\Facades\Advertisements;
use RoundlyConsulting\Advertisements\Models\Advertisement;
it('announces a publish', function () {
Event::fake([AdvertisementPublished::class]);
$ad = Advertisement::factory()->create();
Advertisements::publish($ad);
Event::assertDispatched(AdvertisementPublished::class, fn ($e) => $e->advertisement->is($ad));
expect($ad->isPublished())->toBeTrue();
});Run the published migrations in your test database (RefreshDatabase) — plus media-library’s whenever a test renders an ad, and Storage::fake() with UploadedFile::fake()->image() when you test creatives.
Show 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.