---
title: "Testing — Advertisements for Laravel | Roundly"
description: "Advertisements::fake() records every lifecycle, placement and tracking call with an assertion for each, plus factories and geolocation fakes."
url: https://roundly-consulting.com/open-source/docs/advertisements-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Advertisements for Laravel](https://roundly-consulting.com/open-source/docs/advertisements-for-laravel.md)

# Testing

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:

```php
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):

```php
// 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 click
```

## Factories

Factories ship for testing and seeding. The advertisement factory has published(), scheduled(), expired() and archived() states; the event factory has impression() and click():

```php
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:

```php
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

```php
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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/advertisements-for-laravel.md)
