The recording fake
Contacts::fake() swaps the manager for ContactsFake and returns it — constructor-injected managers get it too. Nothing is written and no event fires: added contacts come back unsaved (a structured address is rendered into the value, never stored). Every write is recorded — through the facade, an injected manager, a contact book, verification(), the HasContacts trait or $contact->requestVerification() / confirmVerification(). Reads still hit the database:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Facades\Contacts;
$fake = Contacts::fake();
$user->addEmail('[email protected]');
$contact->requestVerification();
$fake->assertAdded(fn (ContactData $data, Model $owner) => $data->value === '[email protected]');
$fake->assertVerificationRequested($contact); // the Contact argument is optional
$fake->assertNothingDeleted();| Records | Assert | Assert none |
|---|---|---|
for()->…->add(), for()->add(), trait add*() | assertAdded(?Closure(ContactData, Model)) | assertNothingAdded() |
for()->sync() | assertSynced(?ContactType, ?Closure(list<ContactData>, Model)) | assertNothingSynced() |
update() | assertUpdated(?Contact, ?Closure(ContactData)) | assertNothingUpdated() |
delete() | assertDeleted(?Contact) | assertNothingDeleted() |
setPrimary() | assertPrimarySet(?Contact) | assertNothingPrimarySet() |
verification()->markVerified() | assertVerified(?Contact) | assertNothingVerified() |
verification()->request(), $contact->requestVerification() | assertVerificationRequested(?Contact) | assertNoVerificationRequested() |
verification()->confirm(), $contact->confirmVerification() | assertVerificationConfirmed(?Contact) | assertNoVerificationConfirmed() |
- Every Contact argument is optional — pass one to match a specific contact.
- Closures receive the recorded ContactData (before normalization) and the owner; assertUpdated() passes the ContactData only.
- verification()->request() returns fake-token under the fake.
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Enums\ContactType;
use RoundlyConsulting\Contacts\Facades\Contacts;
use RoundlyConsulting\Contacts\Models\Contact;
it('stores the signup email as a contact', function () {
$fake = Contacts::fake();
$user = User::factory()->create();
$user->addEmail('[email protected]', label: 'Work');
$fake->assertAdded(fn ($data) => $data->value === '[email protected]');
});
it('requests verification and promotes the email', function () {
$fake = Contacts::fake();
$contact = Contact::factory()->email()->create();
Contacts::verification()->request($contact); // the fake returns 'fake-token'
Contacts::setPrimary($contact);
$fake->assertVerificationRequested($contact);
$fake->assertPrimarySet($contact);
$fake->assertNothingAdded();
});
it('replaces the phone list', function () {
$fake = Contacts::fake();
$user = User::factory()->create();
Contacts::for($user)->sync(ContactType::Phone, [
new ContactData(ContactType::Phone, '+421900000000', isPrimary: true),
]);
$fake->assertSynced(ContactType::Phone, fn (array $items, Model $owner) => count($items) === 1);
$fake->assertNothingUpdated();
});Factories
The model factory ships states for ergonomic setup — email(), phone(), url(), address(), primary(), verified(), pendingVerification(), forOwner($model) and ofType($type):
use RoundlyConsulting\Contacts\Enums\ContactType;
use RoundlyConsulting\Contacts\Models\Contact;
Contact::factory()->email()->primary()->verified()->forOwner($user)->create();
Contact::factory()->phone()->forOwner($user)->create();
Contact::factory()->url()->create();
Contact::factory()->address()->create();
Contact::factory()->ofType(ContactType::Social)->create();
$pending = Contact::factory()->email()->pendingVerification()->create();
$pending->confirmVerification('123456'); // the state stores the hash of '123456'Pest expectations
Register the package’s custom Pest matchers from your app’s tests/Pest.php:
// tests/Pest.php
use RoundlyConsulting\Contacts\Testing\ContactExpectations;
ContactExpectations::register();Then assert against any owner model. Expected values are normalized for their kind before comparison:
use RoundlyConsulting\Contacts\Enums\ContactType;
expect($company)
->toHaveContactOfType(ContactType::Email)
->toHavePrimaryEmail('[email protected]')
->toHavePrimaryContact(ContactType::Phone, '+421900000000')
->toHaveVerifiedContact('[email protected]');The package’s own suite
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.