›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-approval
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

byrcsc/laravel-approval · 1.x

Testing.

Test approval mutations with a fake and persisted states with factories.

Approval::fake() records every public mutation instead of performing it, and provides an assertion for each:

$fake = Approval::fake();

$order->submitForApproval('purchase-order', ['amount' => 10_000]);

$fake->assertSubmitted(
    $order,
    fn ($recorded) => $recorded->metadata['new_attributes'] === ['amount' => 10_000],
);

$fake->restore();

While the fake is active, the package writes nothing to the approval tables. Each faked command returns an unsaved model, or an empty result, in the shape the real operation returns. Code under test that reads the returned request keeps working.

What is faked, and what is not

Mutations are faked. Submission, decisions, bulk decisions, withdrawal, cancellation, delegation, standing delegations, reassignment, resynchronization, conflict resolution, resubmission, attachment, guard bypass, definition sync, and overdue processing.

Queries are not. awaiting(), pendingFor(), requestsFor(), overdue(), workflow(), and verifyTrail() run against the database as usual, so a test can seed real requests and still fake the decisions taken on them.

Requester resolution is not faked either. A faked submit() with no requester finds one the same way the real manager does, through approvalRequester() and then the authenticated user. A test that forgets to authenticate fails the same way production does.

Assertions

AssertionCovers
assertSubmitted($approvable, $callback?)Submission
assertNotSubmitted($approvable)none
assertSubmittedTimes($count, $approvable?)none
assertApproved($request, $callback?)An approval decision
assertRejected($request, $callback?)A rejection
assertReturnedForRevision($request, $callback?)A return for revision
assertNotApproved($request)none
assertApprovedTimes($count, $request?)none
assertRejectedTimes($count, $request?)none
assertApprovedMany($callback?)A bulk approval
assertRejectedMany($callback?)A bulk rejection
assertWithdrawn($request, $callback?)Withdrawal
assertCancelled($request, $callback?)Administrative cancellation
assertDelegated($request, $callback?)Delegation
assertDelegationGranted($delegator, $callback?)A standing delegation grant
assertDelegationRevoked($delegation, $callback?)A standing delegation revoke
assertDelegationsApplied($callback?)The delegation sweep
assertReassigned($request, $callback?)Reassignment
assertResynced($request, $callback?)Resynchronization
assertConflictResolved($request, $callback?)Conflict resolution
assertResubmitted($request, $callback?)Revise and resubmit
assertAttached($request, $callback?)Attachment
assertGuardBypassed($approvable, $callback?)A guard-mode bypass
assertOverdueProcessed($callback?)The overdue sweep
assertDefinitionsSynced($callback?)Definition sync
assertNothingRecorded()All of them

When assertNothingRecorded() fails, it names the interaction types that were recorded. The names usually show what happened without a debugger.

Inspecting an interaction

The optional callback receives an ApprovalInteraction and returns a boolean:

$fake->assertDelegated($request, fn ($recorded) =>
    $recorded->recipient->is($deputy)
    && $recorded->comment === 'On leave this week');
PropertyMeaning
typeAn InteractionType
targetThe request, record, delegator, delegation, or list of requests it acted on
actorWho performed it
workflowThe workflow argument, for submission and resubmission
verdictThe Verdict, for decisions
commentThe comment, trimmed
metadataOperation-specific input
recipientThe approver an assignment moved to, or the delegate a standing delegation names

target is null for the operations that act on no single subject: overdue processing, delegation processing, and definition sync.

A faked grantDelegation() records the window in metadata as starts_at and ends_at, and returns an unsaved ApprovalDelegation.

Read them all with $fake->interactions() when an assertion helper does not fit.

Restoring

$fake->restore();

Puts the real manager back in the container and on the facade. Calling fake() again on an active fake clears its recorded interactions instead of nesting.

Testing without the fake

Integration tests that use the real engine need only a database. Everything the package does is local. It makes no HTTP calls, uses no queue, and calls no external service:

it('applies the draft after the final approval', function () {
    $order = PurchaseOrder::factory()->create(['amount' => 4_000]);
    $request = $order->submitForApproval('purchase-order', ['amount' => 10_000]);

    Approval::approve($request, $financeUser);

    expect($order->fresh()->amount)->toBe(10_000)
        ->and($request->fresh()->state)->toBe(RequestState::Approved);
});

Factories

Every model ships a factory under ByRcsc\LaravelApproval\Database\Factories:

use ByRcsc\LaravelApproval\Models\ApprovalWorkflow;

$workflow = ApprovalWorkflow::factory()
    ->has(ApprovalWorkflowStage::factory()->count(2), 'stages')
    ->create(['slug' => 'purchase-order']);

Factories build an unusual request state directly, such as a conflicted request or a stranded stage. Reaching those states through the engine takes several steps.

Asserting events

Approval events are ordinary Laravel events:

Event::fake([ApprovalCompleted::class]);

Approval::approve($request, $financeUser);

Event::assertDispatched(ApprovalCompleted::class);

Faking events also stops the notification listener, because the package registers the listener against those events.

Asserting notifications

config(['approval.notifications.enabled' => true]);

Notification::fake();

$order->submitForApproval('purchase-order');

Notification::assertSentTo($financeUser, ApprovalRequestedNotification::class);

The listener defers delivery with DB::afterCommit(). Outside a transaction, the callback runs immediately. A test that does not wrap the call in a transaction sees the notification without extra work.

Contributing to the package

The package's own suite needs no external database, .env, or service:

composer install
composer test
composer analyse
vendor/bin/pint --test

What to read next

  • Events and listeners to choose between fake interactions and real event assertions.
  • Eligibility and authorization to test refusal reasons and policies.
  • Troubleshooting for states that require persisted fixtures.
PreviousConsole commandsNextTroubleshooting
View source

On this page

  1. What is faked, and what is not
  2. Assertions
  3. Inspecting an interaction
  4. Restoring
  5. Testing without the fake
  6. Factories
  7. Asserting events
  8. Asserting notifications
  9. Contributing to the package
  10. What to read next