---
title: "Testing — Onboarding for Laravel | Roundly"
description: "Onboarding::fake() captures events, records dismissals and seeds an in-memory store — or test progress straight from model data."
url: https://roundly-consulting.com/open-source/docs/onboarding-for-laravel/testing
language: en
---

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

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

# Testing

Onboarding::fake() swaps the manager for an OnboardingFake — behind the facade and in the container, so injected managers and the GetsOnboarded trait use it too — and returns it. The fake:

- keeps every flow and the resolver registered before it, so flows resolve normally;
- captures the package’s StepCompleted and FlowCompleted events, like Bus::fake() — unless events are already faked: an existing Event::fake() is kept, with what it recorded, so call it before or after Onboarding::fake(). An earlier partial Event::fake(\[...\]) must list both events, or they dispatch for real and the fake can’t see them;
- records every dismissal — Onboarding::for($user)->dismiss(), a flow’s dismiss() and $user->dismissOnboardingStep();
- replaces the store with an in-memory one you can seed; useStore() is ignored under the fake.

```php
use RoundlyConsulting\Onboarding\Facades\Onboarding;

$fake = Onboarding::fake();

$fake->seedCompleted($user, 'verify-email');   // completedAt() reports it; record() skips it
$fake->seedDismissed($user, 'add-bio');        // the step drops out of $user's flow

$user->onboarding()->record('photo');
$user->dismissOnboardingStep('tour');

$fake->assertStepCompleted('photo')
     ->assertFlowCompleted()           // optionally pass a flow title to filter
     ->assertStepNotCompleted('bio')
     ->assertDismissed('tour', $user)  // subject optional
     ->assertNotDismissed('add-bio');  // seeding is not a dismissal

Onboarding::fake()->assertNothingRecorded()->assertNothingDismissed();
```

## Assertions

| Method | Purpose |
| --- | --- |
| `assertStepCompleted(string $key, ?callable $callback = null)` | A StepCompleted was dispatched for the key; the optional callback receives the event and returns bool. |
| `assertStepNotCompleted(string $key)` | No StepCompleted was dispatched for the key. |
| `assertFlowCompleted(?string $flowKey = null)` | A FlowCompleted was dispatched; the optional argument filters by the flow’s title. |
| `assertNothingRecorded()` | Neither event was dispatched. |
| `assertDismissed(string $step, $subject = null)` | The step was dismissed — for that subject, or for any. A saved model matches by key, another authenticatable by class and auth identifier, anything else by object. |
| `assertNotDismissed(string $step, $subject = null)` | The step was not dismissed. Seeding with seedDismissed() doesn’t count. |
| `assertNothingDismissed()` | No dismissal was recorded. |

Every assertion returns the fake for chaining, and each is also callable statically — Onboarding::assertDismissed(…). assertFlowCompleted() filters by the flow’s title, not its registry key — the event carries the Flow itself.

## Seeding the store

| Method | Purpose |
| --- | --- |
| `seedCompleted($subject, string ...$steps)` | completedAt() reports the steps as completed now, so record() won’t announce them. |
| `seedDismissed($subject, string ...$steps)` | The steps count as dismissed — hidden when they are optional and dismissible. |

## A Pest example

```php
use App\Models\User;
use RoundlyConsulting\Onboarding\Facades\Onboarding;

it('announces the photo step for the uploading user', function () {
    $fake = Onboarding::fake();

    $user = User::factory()->create(['avatar_path' => 'avatars/jane-doe.jpg']);

    $user->onboarding()?->record('photo');

    $fake->assertStepCompleted('photo', fn ($event) => $event->for === $user)
         ->assertStepNotCompleted('bio');
});

it('hides the bio step once the user says not now', function () {
    Onboarding::fake();

    $user = User::factory()->create();

    Onboarding::for($user)?->dismiss('add-bio');     // 'add-bio' is optional()->dismissible()

    Onboarding::assertDismissed('add-bio', $user);   // static calls work too
    expect(Onboarding::for($user)?->hasStep('add-bio'))->toBeFalse();
});
```

## Testing progress directly

Because progress is derived from data, most tests need no fake at all — set up the model and read the flow:

```php
it('sends a fresh account to the photo step', function () {
    $user = User::factory()->create(['avatar_path' => null]);

    expect($user->hasCompletedOnboarding())->toBeFalse()
        ->and($user->onboardingProgress())->toBe(0.0)
        ->and($user->nextOnboardingStep()?->stepKey())->toBe('photo');
});
```

The manager is a container singleton, so each test’s fresh application starts from the flows your service providers register. Call Onboarding::flush() or forget() to start clean when a test registers its own flows.

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