pestphp/pest-plugin-expectations
An extension for the Pest PHP testing framework that adds extra expectation APIs, letting you write fluent, readable assertions with expect(). It supports custom expectation creation and ships additional built-in expectations for cleaner, more expressive test suites.
Framing. The TPM decision is not "adopt or reject a testing library." The capability this package provides — chainable, domain-readable assertions like expect($order)->toBePaid() — is native to Pest 2.x/3.x via expect() and expect()->extend(). The real decisions are: (1) how much engineering capacity we spend confirming that, (2) what we do with the removal, and (3) whether the repo becomes a reusable decision template for the next "community plugin vs. framework-native feature" debate. The deliverable of this evaluation is a decision record, a scoped removal ticket, and a guardrail — not an integration plan.
Architecture fit. Dev-only tooling with a narrow blast radius by construction: no runtime code path, no production service provider, no database, queue, or HTTP surface. It plugs into Pest's expectation registry and delegates the actual assertion to PHPUnit. That is a clean fit with the plugin contract — which is exactly why it became redundant, since Pest absorbed both the expect() entry point and custom-expectation authoring into core. On any current Laravel/Pest stack the package is at best a no-op and at worst a duplicate-registration hazard at test bootstrap. The dangerous failure mode is not a crash; it is green tests that assert less than reviewers believe.
Integration feasibility. Mechanically trivial (composer require --dev, autodiscovery, zero configuration); practically gated by dependency resolution. A final release dated 2021-05-19 implies PHP 7.x/8.0-era and PHPUnit 8/9-era constraints. On PHP 8.2+ with PHPUnit 10/11, Composer will either refuse resolution outright or force downgrades that destabilize the rest of the test toolchain — a cost far larger than the convenience. Diligence flag: any metadata claiming compatibility with Pest 2.x/3.x is unverified and inconsistent with the archive date and the absence of a release after May 2021. The only admissible evidence is a throwaway-branch install — and even a clean install does not create a maintenance path, so a successful spike is not an approval signal.
Technical risk. High and asymmetric. The downside is not that the plugin breaks; it is that the plugin prevents everything else from moving.
| Risk | Likelihood | Impact | TPM read |
|---|---|---|---|
| Abandonment / no security response path | Certain (archived, 0 dependents) | Medium | No upstream to inherit fixes from; 100% of burden transfers to us |
Duplicate expect() registration / shadowed assertions |
Medium on Pest 2.x+ | High (silent under-testing) | The dangerous case is a passing suite that verifies less than the review process assumes |
| Toolchain ceiling on PHP / PHPUnit / Pest | High over 12–18 months | High | Converts a dev convenience into a blocker on every future upgrade window |
| Autoload / plugin-discovery conflict with other Pest plugins | Low–Medium | Medium | Manifests as non-obvious bootstrap failures under incident pressure |
Key questions for the owning team.
expect() and expect()->extend()?Stack fit. Laravel + Pest + PHPUnit, require-dev only. If it survives at all, the correct posture is a single dev dependency, no config publishing, no provider registration — and explicitly not alongside or in place of core expectations. Anything more is duplicated surface area with two owners and no clear source of truth.
Migration path. The recommended path is off this package, not onto it. Sequence:
expect() call and bespoke expectation across the suite; tag each as deletable, rewrite as core expect()->extend(), or replace with a plain PHPUnit assertion. This same inventory doubles as the sizing input for any adjacent PHPUnit → Pest migration workstream, because custom assertion surface area is exactly what has to be rewritten or retired during the move.composer.json.composer.json, run composer update --dev, execute the full suite plus a coverage diff to catch silently dropped assertions.For a genuinely legacy Pest v0 codebase, the path inverts: treat the plugin as a knowingly frozen stopgap, isolate it behind the test bootstrap, and track the Pest major upgrade as the remediation work item — not the plugin. The unit of work is the framework upgrade; the plugin is just a passenger.
Compatibility. Assume PHP ≤8.0 and PHPUnit ≤9 until proven otherwise by a real install in a disposable branch. Do not infer support from the MIT license, from 2021-era green builds, or from package metadata. If validation is required, verify with composer why-not rather than reasoning about it.
Sequencing. Spike → decide → change, and never bundle. Timebox the spike (install, run suite, capture resolution conflicts and deprecation noise), then choose remove (default), pin and freeze (legacy only, with a dated exit), or replace with core. Keep this off the same change window as a PHP or framework upgrade: dependency-resolution interactions are the hardest failures to debug, and they deserve isolated scope and a named owner.
Maintenance. Zero upstream maintenance and no viable fork strategy at 21 stars and 0 dependents. The artifact is frozen, so 100% of the maintenance burden transfers to us, and the only available maintenance action is eventual removal. Budget for the exit, not for stewardship — a stewardship line item against an archived repo is a budget that can never pay off.
Support. No vendor, no maintainer, no issue triage, no CVE channel. Internal support means one or two engineers reverse-engineering Pest's plugin registration under incident pressure. The dependency's purpose and its removal ticket should be written down at the moment of adoption, or the next team will treat it as load-bearing infrastructure.
Scaling. Runtime cost is nil — dev-only, no effect on latency, throughput, or infrastructure spend. The scaling risk is organizational: the version ceiling propagates to every developer laptop and every CI runner, so its cost grows with headcount and release cadence rather than with traffic. It is a tax that compounds quietly until an upgrade is blocked and nobody can identify the cause.
Failure modes to watch.
Ramp-up. Low for engineers — the API is idiomatic Pest and requires no training. The real ramp-up cost is institutional: new joiners reasonably assume anything in composer.json is current and supported. Budget a one-paragraph ADR stating that the package is archived, why it exists if it still does, what replaces it, and who owns the exit.
Recommendation. Do not adopt; do not budget. Own this as a small removal workstream — confirm core expectations cover the suite, delete the dependency, publish the ADR, add the CI guard — and use it as the worked example for a standing rule: test-tooling extensions live in-repo on framework-native extension points, and single-maintainer plugins are permitted only with a named owner and a dated exit. The repo's highest value to us is as a decision template — a clean worked example of capability being real, a community package supplying it, and the framework then absorbing it into core — applied to the next build-vs-buy-vs-use-the-native-feature debate on developer tooling.
How can I help you explore Laravel packages today?