NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky

Workflows::fake() vymení manažéra za Testing\WorkflowsFake za fasádou aj v kontajneri a vráti ho:

use RoundlyConsulting\Workflows\Facades\Workflows;

$fake = Workflows::fake();   // swaps the facade root AND the container binding, so injected managers are faked

$this->post('/purchase-requests', [...]);

$fake->assertStarted('purchase-approval');
$fake->assertStarted('purchase-approval', fn (StartData $data) => $data->subject?->is($request), times: 1);
$fake->assertEmitted('purchase_request.submitted', fn (EmitData $data) => $data->payload['amount'] === '1200');
$fake->assertEdited('purchase-approval', fn (array $graph) => count($graph['nodes']) === 5);
$fake->assertFlagResolved('purchase-approval', node: 'notify', path: '/recipients');
$fake->assertTouched($request);
$fake->assertWoken(times: 2);
$fake->assertNothingRecorded();

$fake->recorded(WorkflowOperation::Signal);   // list<RecordedOperation{operation, context}>
  • Len zaznamenáva: každé volanie, ktoré niečo mení — start, emit, signal, webhook, create, edit, discard, publish, retire, activate, restore, vyriešenie príznaku, obnova webhooku, import, sync, retry, cancel, touch, wake, prune — sa zaznamená a nespustí. Zaznamenajú sa aj volania cez trait HasWorkflows (touch po aktualizácii, zrušenie po zmazaní).
  • Čítanie prechádza do skutočného úložiska: definície, katalóg, subjekty, validácia, porovnania, prehľady behov, exporty, skúšobné behy a zostavovanie grafu (Workflows::graph()).
  • Rozoslanie emitu sa nespustí: overujte emit, nie spustenia, ktoré by vyvolal. Kľúč udalosti sa však najprv overí rovnakou kontrolou ako pri skutočnom emite: interný kľúč s @ alebo chybný kľúč (Order Placed) vyhodí InvalidEventKeyException a nič sa nezaznamená.
  • Náhrady, nikdy null: pod fake metóda, ktorá niečo mení, vráti neuloženú náhradu (exists === false) vytvorenú cez nastavené triedy modelov — definíciu s kľúčom, názvom a rozsahom vlastníka z volania, koncept verzie s grafom, ImportResult s neuloženou definíciou a verziou. retry a cancel vrátia inštanciu, ktorú dostali. Spustenie vráti StartResult{instance: null, created: false, skipped: 'faked'}; emity, wake, prune a sync vrátia prázdne reporty; touch() a signal() vrátia 0. Overujte cez fake, nie cez databázu.

->passthrough() zaznamenáva a zároveň spúšťa:

$fake = Workflows::fake()->passthrough();   // record AND run
$fake->passthrough(false);                   // back to record-only

Asercie

Každá zaznamenaná operácia má pozitívnu aj negatívnu aserciu (signály a skúšobné behy, ktoré sú interné alebo sa nezaznamenávajú, ich nemajú). Každá asercia je uvedená na fasáde a chybové správy uvádzajú kľúč alebo ULID inštancie:

OperáciaPrejde, keďNegatívna
startassertStarted(string $key, ?Closure $where = null, ?int $times = null)assertNotStarted($key), assertNothingStarted()
emitassertEmitted(string $event, ?Closure $where = null)assertNotEmitted($event), assertNothingEmitted()
createassertCreated(string $key)assertNotCreated($key)
editassertEdited(string $key, ?Closure $where = null)assertNotEdited($key)
publishassertPublished(string $key)assertNotPublished($key)
retireassertRetired(string $key)assertNotRetired($key)
activateassertActivated(string $key)assertNotActivated($key)
restoreassertRestored(string $key)assertNotRestored($key)
discard draftassertDraftDiscarded(string $key)assertDraftNotDiscarded($key)
resolve flagassertFlagResolved(string $key, ?string $node = null, ?string $path = null)assertNoFlagResolved($key)
rotate webhookassertWebhookRotated(string $key)assertWebhookNotRotated($key)
importassertImported(?string $key = null)assertNothingImported()
syncassertSynced(?string $path = null, ?bool $publish = null)assertNotSynced()
inbound webhookassertWebhookReceived(string $hookId)assertWebhookNotReceived($hookId)
retryassertRetried(WorkflowInstance $instance)assertNotRetried($instance)
cancelassertCancelled(WorkflowInstance $instance)assertNotCancelled($instance)
touchassertTouched(?Model $subject = null)assertNotTouched(?Model $subject = null)
wakeassertWoken(?int $times = null)assertNotWoken()
pruneassertPruned()assertNotPruned()
anything—assertNothingRecorded()

Zaznamenané operácie

$fake->recorded(?WorkflowOperation $operation = null) vráti list<RecordedOperation> (operation, context) od najstaršej, pre vlastné asercie:

WorkflowOperationZaznamená hoKľúče context
startstart()->dispatch(), startWith(), for()->start(), definition()->start()data (StartData), key
emitemit(), emitWith(), for()->emit()data (EmitData), event
signalprepojenia (interné)key, subject
webhooktriggers()->webhook(), prichádzajúca routehook, payload
create_definitiondefinitions()->create() / createWith()data, key
restore_definitiondefinitions()->restore()key, owner
syncdefinitions()->sync()path, owner, publish, dry_run
editdefinition()->edit()definition, key, graph
discard_draftdefinition()->discardDraft()definition, key
publishdefinition()->publish()definition, key, by
retire / activate / rotate_webhookdefinition()->retire() / activate() / rotateWebhook()definition, key
resolve_flagdefinition()->resolveFlag()definition, key, node, path
importversions()->import()owner, key, payload
retryinstances()->retry()instance
cancelinstances()->cancel(), for()->cancelAll(), hook HasWorkflows pri mazaníinstance, reason
touchfor()->touch(), hook HasWorkflows pri aktualizáciisubject
wakewake()now
pruneprune()—
dry_runnikdy sa nezaznamená—

Spúšťanie workflowov v teste

use RoundlyConsulting\Workflows\Testing\InteractsWithWorkflows;

uses(InteractsWithWorkflows::class);

it('escalates after two business days', function () {
    $this->travelTo(CarbonImmutable::parse('2026-11-06 15:00', 'Europe/Bratislava'));   // Friday
    $instance = Workflows::start('purchase-approval', $request, $team)->dispatch()->instance;

    $this->travelAndWake('P4D');    // move the clock, run workflows:wake, drain the queue

    $this->assertInstanceAt($instance, 'approve');
    $this->assertInstanceStatus($instance, InstanceStatus::Waiting);
});
PomocníkČo robí
runWorkflows(?string $connection)Spracúva front, kým nie je prázdny (≤ 1000 jobov); na sync nerobí nič.
travelAndWake($by)'P2D', 'PT1H', interval alebo absolútny okamih: posunie testovacie hodiny, prebudí a vyprázdni front.
fakeStep(string $type, StepResult|string|Closure $result, ?array $outcomes = null)Určí, čo vráti každý uzol daného typu kroku.
assertStepRan(string $type, ?int $times = null, ?Closure $where = null)Skriptovaný typ bežal (toľkokrát, vyhovujúc $where).
assertStepNotRan(string $type)Skriptovaný typ nikdy nebežal.
restoreSteps()Vráti skutočné typy (spustí sa samo na konci testu).
assertInstanceAt($instance, $node)Aktuálny uzol.
assertInstanceStatus($instance, InstanceStatus)Stav.
assertPath($instance, list<string>)Cesta dokončených uzlov.

S workflows.queue.enabled = off beží všetko priamo a runWorkflows() nemá čo robiť.

Falošné kroky

Workflow otestujete od začiatku do konca bez jeho skutočných vedľajších efektov: určíte výsledok typu kroku a skutočný engine beží ďalej — zámky, čakania, eskalácie, záznam krokov aj autorizačný hook (zamietnutie vo vašich policy krok stále ukončí, takže test ho nikdy neobíde):

uses(InteractsWithWorkflows::class);

$this->fakeStep('approval', 'rejected', outcomes: ['approved', 'rejected', 'expired']);  // approvals not installed: name its outcomes
$this->fakeStep('acme.create_task', fn (StepContext $c) => StepResult::complete(output: ['task_id' => 7]));

Workflows::start('purchase-approval', $request, $team)->dispatch();
$this->runWorkflows();

$this->assertStepRan('acme.create_task', times: 1, where: fn (StepContext $c) => $c->config()['title'] === 'Order');
  • Reťazec je výsledok; StepResult sa vráti tak, ako je; closure ho vytvorí pri každom behu.
  • Registrovaný typ si ponechá kľúč, názov, schému konfigurácie, výsledky, vedľajšie efekty, oprávnenie aj podporu eskalácií, takže validácia pri publikovaní aj autorizácia sa správajú ako v produkcii.
  • Typ, ktorý nie je registrovaný (prepojený typ, ktorého balík chýba), potrebuje $outcomes; prijme akúkoľvek konfiguráciu (aj eskaláciu reassign, ktorá nič nerobí) a nemá vedľajšie efekty.
  • Funguje so zapnutým aj vypnutým frontom. Skutočný typ sa vráti na konci testu.

Pest expectations

Registrujú sa automaticky cez Pest plugin balíka; každá najprv obnoví inštanciu z databázy:

expect($instance)->toBeWaitingAt('approve');
expect($instance)->toHaveCompletedVia(['start', 'approve', 'done']);
expect($instance)->toHaveFailedWith('no_approvers');

Skúšobné behy v testoch

Skúšobný beh je lacný spôsob, ako overiť smerovanie bez vedľajších efektov:

expect(Workflows::definition('purchase-approval', $team)->dryRun()->sample(['amount' => '60000'])->run()->nodes())
    ->toBe(['start', 'big', 'legal_cfo', 'notify_approved', 'done']);

Každý model má factory (Database\Factories\*Factory), takže definície, verzie a inštancie pripravíte aj priamo.

Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

Ďalšie spôsoby podpory vrátane kryptomien

Odoslaním daru súhlasíte s našimi podmienkami prijímania darov.

Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.