---
title: "Testing — Addresses for Laravel | Roundly"
description: "The recording Addresses::fake(), InteractsWithAddresses assertions, the AddressFactory with billing, shipping and primary states, and a Pest example."
url: https://roundly-consulting.com/open-source/docs/addresses-for-laravel/testing
language: en
---

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

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

# Testing

## The recording fake

Addresses::fake() swaps the manager — for the facade and for anything that injects AddressManager — with a recording fake that writes nothing, and returns it. Added addresses stay in memory, unsaved, and the owner’s reads (all(), ofType(), primary()) answer from the stored rows with the fake’s adds, updates, deletes and primary changes applied — so primary() returns what the real manager would after the same calls. Like the real manager, update() fills the address you pass and returns that same instance, unsaved, so a second update() or a delete() through the same variable shows in the reads. Ownership is still enforced, and so is the refusal to promote a deleted address — both checked against the stored row, not the copy you pass. Calls through the HasAddresses trait are recorded too:

```php
use RoundlyConsulting\Addresses\DataTransferObjects\AddressData;
use RoundlyConsulting\Addresses\Facades\Addresses;

$fake = Addresses::fake();

$user->addAddress($data);                       // your code under test
Addresses::for($user)->setPrimary($address);

$fake->assertAdded($user, fn (AddressData $d) => $d->city === 'Bratislava');
$fake->assertPrimarySet($address);
$fake->assertNothingDeleted();
```

| Assertion | Negative |
| --- | --- |
| `assertAdded(?Model $to = null, ?Closure $where = null)` | `assertNothingAdded()` |
| `assertUpdated(?Address $address = null, ?Closure $where = null)` | `assertNothingUpdated()` |
| `assertDeleted(?Address $address = null)` | `assertNothingDeleted()` |
| `assertPrimarySet(?Address $address = null)` | `assertNothingPrimarySet()` |

Every argument is optional. assertAdded() filters by owner and by a closure over the recorded AddressData; assertUpdated() takes the address and a closure over its AddressData.

## Database assertions

For database-backed tests, the InteractsWithAddresses trait adds two assertions. In Pest, apply it with uses(); in PHPUnit, use it on the test class:

```php
use RoundlyConsulting\Addresses\Enums\AddressType;
use RoundlyConsulting\Addresses\Testing\InteractsWithAddresses;

uses(InteractsWithAddresses::class);

$this->assertHasAddress($payer, ['city' => 'Bratislava']);
$this->assertPrimaryAddress($payer, AddressType::Office);
```

```php
use RoundlyConsulting\Addresses\Testing\InteractsWithAddresses;
use Tests\TestCase;

final class CustomerAddressTest extends TestCase
{
    use InteractsWithAddresses;
}
```

| Assertion | Passes when |
| --- | --- |
| `assertHasAddress(Model $addressable, array $criteria = [])` | The model has an address matching every column => value pair; with no criteria, any address. |
| `assertPrimaryAddress(Model $addressable, AddressType $type)` | The model has a primary address of that type. |

assertHasAddress() matches criteria against the addresses table columns, so compare stored values — \['type' => 'billing', 'country\_iso' => 'SK'\] — not enum cases. Soft-deleted addresses do not count.

## The factory

Address::factory() generates realistic addresses. The default definition builds a plain address — is\_primary false, so ask for a primary with the primary() state — of the default type, with a faker name, city, street and postcode and an alpha-3 country\_iso:

| State | Effect |
| --- | --- |
| `primary()` | Sets is\_primary to true. |
| `ofType(AddressType $type)` | Sets type to the enum’s value. |
| `billing()` | Shortcut for ofType(AddressType::Billing). |
| `shipping()` | Shortcut for ofType(AddressType::Shipping). |

```php
use RoundlyConsulting\Addresses\Address;
use RoundlyConsulting\Addresses\Enums\AddressType;

// through the owner's relation — sets addressable_type and addressable_id for you
$customer->addresses()->save(Address::factory()->billing()->primary()->make());

// standalone, with the owner set explicitly
Address::factory()->shipping()->create([
    'addressable_type' => $customer->getMorphClass(),
    'addressable_id' => $customer->getKey(),
]);

Address::factory()->ofType(AddressType::Office)->make();
```

The factory writes rows directly — no country normalisation, no AddressCreated, no sibling demotion — and builds the model configured in addresses.model, so with a custom subclass its rows are created as that subclass, with its casts, events and observers. On PostgreSQL and SQLite the partial unique index still refuses a second live primary in one owner + type group.

## A complete test

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Addresses\Enums\AddressType;
use RoundlyConsulting\Addresses\Events\PrimaryAddressChanged;
use RoundlyConsulting\Addresses\Testing\InteractsWithAddresses;

uses(InteractsWithAddresses::class);

it('promotes a saved shipping address', function () {
    Event::fake([PrimaryAddressChanged::class]);

    $customer = Customer::factory()->create();
    $address = $customer->newAddress()
        ->type(AddressType::Shipping)
        ->in('Bratislava')->at('Somewhere 1')->postalCode('81101')->country('sk')
        ->save();

    $customer->setPrimaryAddress($address);

    $this->assertHasAddress($customer, ['country_iso' => 'SK']);
    $this->assertPrimaryAddress($customer, AddressType::Shipping);
    Event::assertDispatched(PrimaryAddressChanged::class);
});
```

Run it with your usual database setup (for example RefreshDatabase) and the package migration published. To run the package’s own suite from its repository:

```bash
composer test
```

## 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/addresses-for-laravel.md)
