Testovanie
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-onlyAsercie
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ácia | Prejde, keď | Negatívna |
|---|---|---|
| start | assertStarted(string $key, ?Closure $where = null, ?int $times = null) | assertNotStarted($key), assertNothingStarted() |
| emit | assertEmitted(string $event, ?Closure $where = null) | assertNotEmitted($event), assertNothingEmitted() |
| create | assertCreated(string $key) | assertNotCreated($key) |
| edit | assertEdited(string $key, ?Closure $where = null) | assertNotEdited($key) |
| publish | assertPublished(string $key) | assertNotPublished($key) |
| retire | assertRetired(string $key) | assertNotRetired($key) |
| activate | assertActivated(string $key) | assertNotActivated($key) |
| restore | assertRestored(string $key) | assertNotRestored($key) |
| discard draft | assertDraftDiscarded(string $key) | assertDraftNotDiscarded($key) |
| resolve flag | assertFlagResolved(string $key, ?string $node = null, ?string $path = null) | assertNoFlagResolved($key) |
| rotate webhook | assertWebhookRotated(string $key) | assertWebhookNotRotated($key) |
| import | assertImported(?string $key = null) | assertNothingImported() |
| sync | assertSynced(?string $path = null, ?bool $publish = null) | assertNotSynced() |
| inbound webhook | assertWebhookReceived(string $hookId) | assertWebhookNotReceived($hookId) |
| retry | assertRetried(WorkflowInstance $instance) | assertNotRetried($instance) |
| cancel | assertCancelled(WorkflowInstance $instance) | assertNotCancelled($instance) |
| touch | assertTouched(?Model $subject = null) | assertNotTouched(?Model $subject = null) |
| wake | assertWoken(?int $times = null) | assertNotWoken() |
| prune | assertPruned() | assertNotPruned() |
| anything | — | assertNothingRecorded() |
Zaznamenané operácie
$fake->recorded(?WorkflowOperation $operation = null) vráti list<RecordedOperation> (operation, context) od najstaršej, pre vlastné asercie:
| WorkflowOperation | Zaznamená ho | Kľúče context |
|---|---|---|
start | start()->dispatch(), startWith(), for()->start(), definition()->start() | data (StartData), key |
emit | emit(), emitWith(), for()->emit() | data (EmitData), event |
signal | prepojenia (interné) | key, subject |
webhook | triggers()->webhook(), prichádzajúca route | hook, payload |
create_definition | definitions()->create() / createWith() | data, key |
restore_definition | definitions()->restore() | key, owner |
sync | definitions()->sync() | path, owner, publish, dry_run |
edit | definition()->edit() | definition, key, graph |
discard_draft | definition()->discardDraft() | definition, key |
publish | definition()->publish() | definition, key, by |
retire / activate / rotate_webhook | definition()->retire() / activate() / rotateWebhook() | definition, key |
resolve_flag | definition()->resolveFlag() | definition, key, node, path |
import | versions()->import() | owner, key, payload |
retry | instances()->retry() | instance |
cancel | instances()->cancel(), for()->cancelAll(), hook HasWorkflows pri mazaní | instance, reason |
touch | for()->touch(), hook HasWorkflows pri aktualizácii | subject |
wake | wake() | now |
prune | prune() | — |
dry_run | nikdy 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 kryptomienOdoslaní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.