Product Decisions This Supports
- Build vs. buy for a journey-testing harness. The expensive part of order-randomised testing is not the shuffler — it is the seeded, position-independent randomness, the deterministic replay artifact, and the delta-debugging shrinker that turns a 23-step failure into a 5-step one. That is weeks of engineering to build well and easy to build badly (flaky seeds, irreproducible CI failures). Adopting MIT-licensed code for this is a clear "buy" unless you have a dedicated test-infrastructure team with nothing better to do.
- Where to spend QA and test-automation capacity. Fixed-path feature tests encode the order you thought of. This package lets one journey definition produce 25+ seeded permutations plus invariant checks after every step, so the marginal cost of the 26th scenario is near zero. That shifts the question from "how many tests can we write?" to "which invariants can we state?" — a far cheaper thing to scale.
- Release confidence for stateful domains. The highest-value targets are the ones where ordering bugs actually reach customers: billing and entitlement changes, subscription upgrade/downgrade/proration, quota and credit consumption, inventory and reservation flows, approval workflows, cart/checkout, and multi-tenant permission checks. The bundled invariant vocabulary (
legalTransitions, uniqueBy, quotaBalances with per-row starting allowances) maps directly onto the guarantees product already promises in these areas.
- Prioritising which domain gets hardened first. Because it is a test-only trait with no service provider and nothing to publish, you can pilot on a single high-risk domain (payments/entitlements is the obvious candidate) and expand only if it earns its keep. That is a reversible, low-blast-radius roadmap decision rather than a platform commitment.
- Raising the "definition of done" for refactors. When a team is rewriting a state machine or splitting a monolith module, this converts "we think the behaviour is preserved" into a demonstrable check. Useful input to whether a risky refactor ships behind a flag or straight out.
- Incident prevention framing for the roadmap. Bugs that only appear when users do things in an unanticipated order are exactly the ones that escape unit, feature, and browser tests. If post-mortems keep surfacing this class of defect, this package is a targeted countermeasure rather than a generic "more testing" investment.
- Cross-actor and multi-tenant correctness. Interleaved journeys and per-actor sessions let you model two users (or two tenants) acting against the same data, which is where permission leaks and tenancy bleed live — a genuinely hard class of bug to cover any other way short of manual exploratory testing.
When to Consider This Package
Adopt (or pilot) when most of these hold:
- Your product has stateful, order-sensitive domains — state machines, quotas/credits, entitlements, bookings, carts, approvals — where "what if the user did X before Y" is a real support-ticket category.
- You can state your invariants in a sentence ("a closed deal is never reopened", "one leaderboard period per tenant/start", "credits never go negative"). If you cannot, fix that first; the tool will not invent them for you.
- The team already has a healthy Laravel test suite with factories, database resets, and HTTP-level tests — the package plugs into
TestCase as a trait and reuses that machinery rather than replacing it.
- You are on PHP 8.3+ and Laravel 11/12/13. There is no backport path.
- You want deterministic CI and reproducible bug reports (
RUNABOUT_TRAIL artifacts, seeds, shrunk counterexamples) rather than "it failed once on main, we re-ran it, it passed".
- You have someone who will own it. The engineering signals are strong — PHPStan level 9, mutation testing, Rector, Pint, OpenSSF Scorecard, coverage, benchmark tracking, real docs and a changelog — but it is 0 stars, 0 dependents, pre-1.0, and effectively single-maintainer. Treat it as a promising internal-quality dependency, not a vendor.
Look elsewhere or defer when:
- Your surface is mostly CRUD or read-only, or your invariants are trivially covered by unique constraints and existing feature tests. You will pay the learning curve for little signal.
- You need real concurrency detection. Runabout explores interleaved sequential orderings deterministically; it is not a race detector or a load/parallelism harness.
- You need UI/browser-level coverage — that is Dusk/Playwright territory. This drives the app through HTTP actors and middleware, not a browser.
- You are on PHP <8.3 or Laravel ≤10, or you have a policy requiring vendor support, SLAs, or versioned LTS guarantees. A 0.2.0 package that already re-keyed its seed schema once (deliberately, while it had no external users) will churn; if you adopt, pin the version and wrap it behind your own base test case and journey helpers so upgrades are a contained change.
- Your team does not yet write tests consistently. This amplifies existing discipline; it does not substitute for it.
- CI runtime is already the bottleneck and cannot absorb
shuffles(25) × invariant checks. Scope it to one or two journeys and use exhaustive/repeatHeavy selectively rather than blanket-applying it.
Suggested adoption shape: pilot (one domain, one sprint) → measure defects found and CI cost → pin and widen. Keep it a require --dev dependency; there is zero production footprint, which makes the downside of trying it almost entirely the engineer-hours.
How to Pitch It (Stakeholders)
For executives (one paragraph): A meaningful share of our hardest production bugs are not "feature X is broken" but "feature X breaks when a user does things in a different order than we tested, or when two users act at once." Our current tests all walk a single, hand-written happy path, so they are structurally blind to that class. laravel-runabout lets us define a user journey once and replay it in many seeded random orders, checking our business rules after every step. Because runs are seeded and replayable, it does not add flaky tests — any failure is reproduced exactly and automatically reduced to its smallest form, which shortens triage. It is a dev-only, MIT-licensed dependency with no production footprint and no infrastructure cost, installs in minutes, and can be trialled on one high-risk domain (billing/entitlements) before any broader commitment. Main caveat: it is young and single-maintainer, so we would pin the version and pilot rather than standardise immediately.
For engineering leads: It is a RunsJourneys trait you mix into an existing TestCase — no service provider, nothing to publish, compatible with your current factories and database resets. You declare a Journey of steps ($ctx->as('manager')->postJson(...)), declare invariants (legalTransitions, uniqueBy, quotaBalances, or your own), and call ->shuffles(25)->run(). Determinism comes from a seed schema that makes each step's random draws a pure function of which execution it is, so surviving executions reproduce their draws verbatim after reordering or thinning — that is what makes replay and shrinking sound. Failures carry a seed and a RUNABOUT_TRAIL artifact you can replay locally or in CI, plus a shrunk reproduction and the drawn values that pinned it. Cost is CI time proportional to shuffles × invariant checks; start at one journey, one domain.
For the sceptical reviewer: The maturity objection is fair and should be answered directly — 0 stars and 0 dependents means no social proof, and 0.2.0 reorganised src/ and re-keyed existing seeds. Against that: MIT license, PHPStan level 9, mutation testing with a live dashboard, Rector and Pint clean, an OpenSSF Scorecard badge, published coverage and benchmark tracking, a real documentation site, and a changelog that explains why each change was made (including the shrinking algorithm and the same-failure oracle). That is the profile of a carefully built tool that lacks distribution, not a weekend hack. Mitigate by pinning the version, keeping journeys behind our own base classes, and treating upgrades as deliberate work — at which point the worst case is that we delete a dev dependency and keep the invariants, which were the valuable part anyway.