Skip to main content

Testing

Workflow::fake() lets you assert that your application triggers the right workflows, without needing to inspect internals or stub anything by hand.

use Qanna\WorkflowEngine\Facades\Workflow;

Workflow::fake();

// ... code under test that calls Workflow::run() / dispatch() / resume() ...

Workflow::assertRan('send-welcome-email');

What faking does (and doesn't) change

Workflow::fake() only fakes execution storage — swapping it for an in-memory driver so tests don't touch disk or your real database, and so state doesn't leak between tests. It does not stub out the engine: run()/dispatch()/resume() still execute your real workflow, through the real node handlers, exactly as in production. This means assertions like assertExecutionOutput() reflect what the workflow actually produced, not a canned response.

Workflow definitions aren't faked — persist a real one first (e.g. via the repository contract or a small factory in your test suite), then fake execution storage before running it:

use Qanna\WorkflowEngine\Storage\Contracts\WorkflowRepositoryContract;

app(WorkflowRepositoryContract::class)->create($workflow);

Workflow::fake();

$execution = Workflow::run($workflow->id, ['email' => 'ada@example.com']);

dispatch() behaves differently under fake() too: instead of queuing a job, it runs the workflow immediately (so there's an execution to assert on right away), while run()/dispatch()/resume() calls are still recorded separately so you can tell which one your code actually used.

Anything a workflow suspends on (a Wait node's delay, a Call workflow node's child run) resolves inline and instantly under fake(), rather than sleeping or touching a real queue — so a workflow that waits or calls another workflow still runs to completion synchronously in your test.

Assertions

AssertionChecks
assertRan(string $workflowId)run() was called for this workflow.
assertNotRan(string $workflowId)It wasn't.
assertDispatched(string $workflowId)dispatch() was called for this workflow.
assertResumed(string $executionId)resume() was called for this execution.
assertNothingRan()No workflow ran at all.
assertExecutionCount(int $count)Total distinct executions recorded across run()/dispatch()/resume().
assertWorkflowRanTimes(string $workflowId, int $times)This workflow ran exactly $times times.
assertCompleted(string $executionId)The execution's status is Succeeded.
assertSuspended(string $executionId)The execution's status is Suspended.
assertFailed(string $executionId)The execution's status is Failed.
assertExecutionStatus(string $executionId, ExecutionStatus $status)Any specific status.
assertExecutionOutput(string $executionId, mixed $expected)The execution's final output matches exactly.

Inspecting recorded executions directly

When an assertion helper doesn't cover what you need, drop down to the recorded WorkflowExecution objects directly (see Execution for the model's shape):

Workflow::runs(); // WorkflowExecution[] — every run() call
Workflow::dispatches(); // WorkflowExecution[] — every dispatch() call
Workflow::resumes(); // WorkflowExecution[] — every resume() call
Workflow::executions(); // WorkflowExecution[] — every distinct execution, deduplicated by id
Workflow::findExecution($executionId); // ?WorkflowExecution

Testing custom nodes directly

A node's handle() has no framework dependency beyond WorkflowContext and array $config — you can unit test it without going through a workflow at all:

use Qanna\WorkflowEngine\Engine\Context\WorkflowContext;

$result = (new SendSlackMessageNode())->handle(
WorkflowContext::make(['trigger' => ['channel' => '#general']]),
['channel' => '#general', 'message' => 'Hello'],
);

$this->assertTrue($result->ok());

Next