---
title: "Testing — Forms for Laravel | Roundly"
description: "Assert form activity with Forms::fake() — a recording fake that still performs — plus InteractsWithForms helpers, Pest matchers and factories."
url: https://roundly-consulting.com/open-source/docs/forms-for-laravel/testing
language: en
---

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

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

# Testing

Forms::fake() swaps the manager for FormsFake — a recording FormsManager subtype, so injected managers get it too — and returns it as your handle. It still performs against the database: the rows you’d expect are really created, events fire, and a review still opens its approvals request. The fake records every call on top — through the facade, an injected manager, the builders, Forms::submission(), the HasForms trait and forms:sync:

```php
use RoundlyConsulting\Approvals\Models\ApprovalRequest;
use RoundlyConsulting\Forms\Facades\Forms;
use RoundlyConsulting\Forms\Models\FormSubmission;

$fake = Forms::fake();

// ... exercise your application code that submits the form ...

$fake->assertSubmitted($form);
$fake->assertSubmitted($form, fn ($result, $form) => $result->fieldCount === 3);
$fake->assertSubmittedCount(1);
$fake->assertNotSubmitted($otherForm);
$fake->assertReviewOpened(fn (ApprovalRequest $request, FormSubmission $submission) => $request->quorum === 2);
```

## Every assertion

| Records | Assert | Assert none |
| --- | --- | --- |
| submit(), $user->submitTo() | `assertSubmitted(?Form, ?callable)`, `assertSubmittedCount(int)`, `assertNotSubmitted(?Form)` | `assertNothingSubmitted()` |
| draft(), $user->draftTo() | `assertDrafted(?Form, ?callable)` | `assertNothingDrafted()` |
| finalize(), submission()->finalize() | `assertFinalized(?string $uuid)` | `assertNothingFinalized()` |
| define() | `assertFormDefined(string $key)` | `assertNoFormDefined()` |
| create(), define()->create() | `assertFormCreated(?callable)` | `assertNoFormCreated()` |
| update()->save() | `assertFormUpdated(string $key)` | `assertNoFormUpdated()` |
| createSubmission() | `assertSubmissionCreated(?callable)` | `assertNoSubmissionCreated()` |
| sync(), forms:sync | `assertSynced()` | `assertNothingSynced()` |
| review()->…->open() | `assertReviewOpened(?callable)` | `assertNothingReviewed()` |

```php
use RoundlyConsulting\Approvals\Models\ApprovalRequest;
use RoundlyConsulting\Forms\DataTransferObjects\SubmissionResult;
use RoundlyConsulting\Forms\Models\Form;
use RoundlyConsulting\Forms\Models\FormSubmission;
use RoundlyConsulting\Forms\Models\Submission;

// submit(), $user->submitTo()
$fake->assertSubmitted($form, fn (SubmissionResult $result, Form $form) => $result->fieldCount === 3);
$fake->assertSubmittedCount(1);
$fake->assertNotSubmitted($otherForm);
$fake->assertNothingSubmitted();

// draft(), $user->draftTo()
$fake->assertDrafted($form, fn (SubmissionResult $result, Form $form) => $result->fieldCount > 0);
$fake->assertNothingDrafted();

// finalize(), Forms::submission($uuid)->finalize()
$fake->assertFinalized($draft->uuid);
$fake->assertNothingFinalized();

// define(), create(), update()->save()
$fake->assertFormDefined('contact');
$fake->assertNoFormDefined();
$fake->assertFormCreated(fn (Form $form) => $form->key === 'contact');
$fake->assertNoFormCreated();
$fake->assertFormUpdated('contact');     // only once save() has run
$fake->assertNoFormUpdated();

// createSubmission()
$fake->assertSubmissionCreated(fn (Submission $row) => $row->uuid === $uuid);
$fake->assertNoSubmissionCreated();

// sync(), php artisan forms:sync
$fake->assertSynced();
$fake->assertNothingSynced();

// review()->…->open()
$fake->assertReviewOpened(fn (ApprovalRequest $request, FormSubmission $submission) => $request->quorum === 2);
$fake->assertNothingReviewed();
```

- assertSubmitted() and assertDrafted() match by form key; their callbacks receive the SubmissionResult and the Form and must return true for one recorded call.
- assertFormCreated() sees forms stored through Forms::create() and define()->create(); forms created inside sync() satisfy assertSynced() only.
- assertFormDefined($key) passes when define($key, …) ran or a form with that key was created.
- An update is recorded when save() runs — Forms::update($key) alone records nothing, so assertFormUpdated() passes only after save().
- assertReviewOpened() callbacks receive the ApprovalRequest and the FormSubmission.

## Test helpers

The InteractsWithForms trait adds fakeForms(), submitForm($form, $values, $sender) and draftForm(...). Values are nested per group or flat by group\_key.field\_key (one style per group) — the helper wraps them in the form key for you:

```php
uses(RoundlyConsulting\Forms\Testing\InteractsWithForms::class);

$fake = $this->fakeForms();
$this->submitForm($form, ['details' => ['name' => 'Ann']]);
$this->submitForm($form, ['details.name' => 'Ann']);   // the same submission, flat
$fake->assertSubmitted($form);
```

## Pest matchers

Opt into the expectation matchers from your tests/Pest.php:

```php
RoundlyConsulting\Forms\Testing\FormExpectations::register();

expect($form)->toBeAcceptingSubmissions();
expect($expiredForm)->toBeExpired();
```

## A full example

```php
use RoundlyConsulting\Forms\Facades\Forms;
use RoundlyConsulting\Forms\GroupBuilder;

it('stores a contact submission', function () {
    $fake = Forms::fake();

    $form = Forms::define('contact', 'Contact us')
        ->public()
        ->group('details', 'Details', function (GroupBuilder $g): void {
            $g->field('name', 'Name')->required();
        })
        ->create();

    $this->post('/contact', ['contact' => ['details' => ['name' => 'Ann']]])
        ->assertRedirect();

    $fake->assertSubmitted($form, fn ($result) => $result->fieldCount === 1);
    expect(Forms::submissions($form)->first()->value('name'))->toBe('Ann');
});
```

## Factories

Every model has a factory, with states for the common cases:

```php
use RoundlyConsulting\Forms\Models\Form;
use RoundlyConsulting\Forms\Models\FormSubmission;
use RoundlyConsulting\Forms\Models\Submission;

Form::factory()->public()->create();
Form::factory()->withExpiration()->create();

Submission::factory()->draft()->create();
FormSubmission::factory()->pending()->create();
```

Run your suite with the forms migrations (and the dependency packages’ migrations) loaded, e.g. via RefreshDatabase. To run the package’s own tests:

```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/forms-for-laravel.md)
