Testing
Workflow::fake() swaps the execution manager for a recording wrapper. Workflows
still run for real — through the real engine, nodes, and resume logic — but in
Test mode: waits don't sleep, child workflows resolve inline, nothing touches a
real queue. Every run is recorded so you can assert on it.
use Illuminate\Foundation\Testing\RefreshDatabase;
use Qanna\WorkflowEngine\Engine\Nodes\Action\LogNode;
use Qanna\WorkflowEngine\Engine\Triggers\ManualTrigger;
use Qanna\WorkflowEngine\Facades\Workflow;
use Qanna\WorkflowEngine\Models\Workflow as WorkflowModel;
class SignupTest extends TestCase
{
use RefreshDatabase;
public function test_signup_sends_a_welcome_email(): void
{
WorkflowModel::create([
'id' => 'send-welcome-email',
'name' => 'Send Welcome Email',
'trigger' => ['type' => ManualTrigger::type(), 'config' => []],
'nodes' => [
['id' => 'greet', 'type' => LogNode::type(), 'config' => ['message' => 'Welcome {{ trigger.email }}']],
],
'edges' => [
['from' => 'trigger', 'to' => 'greet', 'branch' => 'main'],
],
])->publish();
Workflow::fake();
$execution = Workflow::run('send-welcome-email', ['email' => 'ada@example.com']);
Workflow::assertRan('send-welcome-email');
Workflow::assertCompleted($execution->id);
}
}
Workflow::fake()still reads workflows from, and writes execution records to, the database — your tests need the package's migration (for example viaRefreshDatabase).Workflow::run()only runs published workflows, so publish the ones your test creates, or useWorkflow::runDraft().
Assertions
| Assertion | Passes when |
|---|---|
assertRan($workflowId) | The workflow ran at least once via run() or runDraft(). |
assertNotRan($workflowId) | It never ran. |
assertNothingRan() | No workflow ran. |
assertDispatched($workflowId) | It was started via dispatch(). |
assertResumed($executionId) | That execution was resumed. |
assertWorkflowRanTimes($workflowId, $n) | It ran exactly $n times. |
assertExecutionCount($n) | $n distinct executions were recorded. |
assertCompleted($executionId) | Final status Succeeded. |
assertSuspended($executionId) | Final status Suspended. |
assertFailed($executionId) | Final status Failed. |
assertExecutionStatus($executionId, ExecutionStatus::…) | Exact status. |
assertExecutionOutput($executionId, $expected) | Output identical to $expected. |
Accessors
$fake = Workflow::fake();
// ... run some workflows ...
$fake->runs(); // WorkflowExecution[] — one per run() / runDraft() call
$fake->dispatches(); // one per dispatch() call
$fake->resumes(); // one per resume() call
$fake->executions(); // deduped by execution id, latest state
$fake->findExecution($executionId);
Testing suspend / resume
Because child workflows and waits resolve inline in test mode, a full suspend → resume cycle runs in one test:
Workflow::fake();
$suspended = Workflow::run('needs-approval');
Workflow::assertSuspended($suspended->id);
$resumed = Workflow::resume($suspended->id, ['approved' => true]);
Workflow::assertCompleted($suspended->id);
$this->assertSame(['approved' => true], $resumed->output);
A child workflow started by a Call workflow node
is recorded too, so Workflow::assertRan('the-child') works without extra wiring.
Registering a fixture node
If your test needs a bespoke node (e.g. one that suspends on demand), register it
on the registry in setUp():
$this->app->make(\Qanna\WorkflowEngine\Engine\NodeRegistry::class)
->register(MyFakeApprovalNode::class);