Testing Workflows

Stable 1.0: Package tests and dbflow-demo integration suites are authoritative when prose docs lag. When in doubt, read demo tests and Core validation classes.

Workflow logic touches concurrency, branching, assignments, and audit logs. PHPUnit coverage is the safest way to ship DBFlow in a real Laravel app — especially while APIs are still evolving.

What to test

Behaviour Why it matters
Workflow starts Definition resolves; instance row is created
Duplicate active workflow blocked active_key prevents double submit
Pending task created Approval nodes surface assignable work
Approve advances graph Transitions fire; next node or completion
Reject applies strategy RejectStrategy routing matches product rules
Hooks update business state Host columns stay aligned with instance status
Logs written dbflow_workflow_logs captures audit events

Validate definitions first

Catch malformed graphs before runtime tests:

use DbflowLabs\Core\Validation\WorkflowDefinitionValidator;

app(WorkflowDefinitionValidator::class)->validateOrFail($definition);

Run this against provider output, seeder arrays, and exported JSON in CI.

Core test traits

Host apps and dbflow-demo compose PHPUnit traits from dbflowlabs/core test support:

Trait Namespace
BuildsMinimalPublishedWorkflow DbflowLabs\Core\Tests\Concerns\BuildsMinimalPublishedWorkflow
LoadsBlueprintFixtures DbflowLabs\Core\Tests\Concerns\LoadsBlueprintFixtures
RegistersEngineTestResources DbflowLabs\Core\Tests\Concerns\RegistersEngineTestResources

Mirror usage from dbflow-demo when wiring your own suite. Register providers and run php artisan dbflow:sync in test setUp() only in DBFlow-focused tests before calling DBFlow::start().

Isolate DBFlow in the host test suite

Most host applications should disable DBFlow by default in PHPUnit so unrelated feature tests do not hit workflow runtime or require synced definitions:

// tests/TestCase.php — setUp() after parent::setUp()
config([
    'dbflow.enabled' => false,
    'dbflow-filament.enabled' => false,
]);

Or in phpunit.xml / .env.testing:

<env name="DBFLOW_ENABLED" value="false"/>
<env name="DBFLOW_FILAMENT_ENABLED" value="false"/>

In dedicated DBFlow integration tests, re-enable the flags in setUp(), register providers (or rely on the host booted() provider), run dbflow:sync, and seed drafts when testing Pro canvas or definition editing.

Example: start and duplicate guard

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Exceptions\WorkflowAlreadyRunningException;

public function test_start_creates_running_instance(): void
{
    $dispute = RefundDispute::factory()->create();

    DBFlow::start('refund_dispute_approval', $dispute, $this->user);

    $this->assertTrue($dispute->hasRunningWorkflow('refund_dispute_approval'));
}

public function test_duplicate_start_is_blocked(): void
{
    $dispute = RefundDispute::factory()->create();

    DBFlow::start('refund_dispute_approval', $dispute, $this->user);

    $this->expectException(WorkflowAlreadyRunningException::class);

    DBFlow::start('refund_dispute_approval', $dispute, $this->user);
}

Example: approve advances the graph

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Enums\WorkflowTaskStatus;

public function test_support_lead_approval_creates_next_task(): void
{
    $dispute = $this->startRefundDisputeUnderReview();
    $task = $dispute->runningWorkflowInstance('refund_dispute_approval')
        ?->tasks()
        ->where('status', WorkflowTaskStatus::Pending)
        ->first();

    DBFlow::approve($task, $this->supportLead, 'Approved.');

    $this->assertDatabaseHas('dbflow_workflow_logs', [
        'workflow_instance_id' => $task->workflow_instance_id,
        'event' => 'task_approved',
    ]);
}

Adapt factory helpers to your app. The dbflow-demo project centralises bootstrapping in integration test base classes.

Example: reject strategy

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Enums\RejectStrategy;

public function test_reject_with_previous_node_returns_to_prior_approval(): void
{
    $task = $this->pendingFinanceTask();

    DBFlow::reject(
        $task,
        $this->financeUser,
        'Need manager re-approval.',
        RejectStrategy::PreviousNode,
    );

    $instance = $task->workflowInstance()->fresh();
    $this->assertSame('procurement_manager_review', $instance->current_node_key);
}

Example: hooks and host status

public function test_approval_hook_sets_host_status(): void
{
    $request = $this->approveProcurementRequestThroughWorkflow();

    $this->assertSame(
        ProcurementRequestStatus::Approved,
        $request->fresh()->status,
    );
}

Register real hook classes in test setUp() the same way as production service providers.

Demo integration tests (reference)

Study these files in dbflow-demo:

Test Coverage
tests/Feature/RefundDisputeDbflowIntegrationTest.php End-to-end refund approval
tests/Feature/RefundDisputeAmountBranchingTest.php refund_amount >= 500 condition branches
tests/Feature/ProcurementRequestDbflowIntegrationTest.php Procurement flow and hook mapping

They demonstrate published definitions, actor users, task resolution, and log assertions against real tables (dbflow_workflow_instances, dbflow_workflow_tasks, dbflow_workflow_logs).

Practical tips

  • Freeze workflow keys — use constants (RefundDisputeWorkflow::KEY) in tests and app code.
  • Seed or publish in setUp — instances need an active published version in dbflow_workflow_versions (use php artisan dbflow:sync or BuildsMinimalPublishedWorkflow).
  • Use actingAs() — pass the same user model class configured in config('dbflow.auth.model').
  • Assert logs, not only status — log rows prove the runtime path, not just final columns.
  • Run package tests on upgrade — after bumping dbflowlabs/core, run composer test in the package checkout or your CI matrix.

What's next

Something wrong? Open an issue on GitHub