Testing Workflows

Alpha notice: During alpha, package and demo tests are often more current than prose docs. When in doubt, read dbflow-demo integration 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 SyncWorkflowDefinitions in test setUp() before calling DBFlow::start().

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 SyncWorkflowDefinitions 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