skip to content

Your team maintains a large PHPUnit 13 suite; how would you decide whether to adopt Pest 5, and how would you roll it out?

level: principalimportance: should knowfreq 22%

answer

  1. Pest runs on PHPUnit
  2. class-based tests keep running
  3. PHP 8.4 floor, PHPUnit pinned
  4. arch, mutate, type coverage first
  5. --drift converts files gradually

basics

~20 s

Pest 5 runs on PHPUnit 13, so existing TestCase classes keep running and phpunit.xml still applies; adopt it for what it adds (arch rules, built-in mutation, type coverage, Tia) against its costs: a PHP 8.4 floor and PHPUnit upgrades tied to Pest releases.

solid answer

~50 s

I would treat it as a tooling decision, not a rewrite. Pest 5 is a layer over PHPUnit 13: its runner executes existing `TestCase` classes alongside closure tests and reads the same `phpunit.xml`, so the migration can be incremental. The gains are concrete: `arch()` rules and presets, mutation testing through `--mutate`, type coverage, Tia for local feedback, `--parallel`, browser tests via `visit()`. The costs are also concrete: Pest 5 requires PHP 8.4, and its composer.json pins `phpunit/phpunit` to a narrow range (Pest 5.2.1 conflicts with anything above 13.3.4), so PHPUnit upgrades wait for Pest releases. Static analysis needs `pest-plugin-phpstan` to understand test closures. The rollout: install Pest, keep all existing tests running, add arch rules first because they need no rewrite, then mutation on one critical module, write new tests in Pest, and convert old files with `--drift` only when they are touched anyway.

code

bash · 9 lines
bash
composer require pestphp/pest --dev --with-all-dependencies
./vendor/bin/pest --init

# existing PHPUnit TestCase classes run unchanged
./vendor/bin/pest --parallel

# convert one folder when it is being worked on anyway
composer require pestphp/pest-plugin-drift --dev
./vendor/bin/pest --drift tests/Unit/Billing

go deeper

for a junior

Know that Pest is built on PHPUnit, so existing PHPUnit test classes still run when Pest is installed.

for a middle

List what Pest adds that PHPUnit alone does not provide: arch rules, built-in mutation testing, type coverage and Tia.

for a senior

Plan the migration so the suite stays green at every step, and set conventions for mixed class and closure tests.

for a principal

Own the trade-off: the value of the added capabilities against the PHP 8.4 floor, the PHPUnit version coupling, tooling and team cost, and say when not to adopt.

## Frame the decision correctly Adopting Pest is not a choice between two test engines. **Pest 5 runs on PHPUnit 13**: its runner is built on PHPUnit's, its assertions, configuration file and coverage come from PHPUnit, and a class that extends `PHPUnit\Framework\TestCase` still runs under `./vendor/bin/pest`. The real question is whether the layer Pest adds is worth its costs for this team and codebase. ## What Pest adds on top of the suite you already have | Capability | How it arrives | Needs a rewrite of old tests | |---|---|---| | Architecture rules | `arch()` with `toUse()`, `toOnlyBeUsedIn()`, presets `php`, `security`, `strict` | no | | Mutation testing | `--mutate` with `covers()` / `mutates()` per file, `--min` gate | only a `covers()` line per file | | Type coverage | `pestphp/pest-plugin-type-coverage`, `--type-coverage --min` | no | | Parallel runs | `--parallel` through ParaTest | no, but tests must be isolated | | Local test impact analysis | `--tia` (Pest 5), needs PCOV or Xdebug | no | | Browser tests | `pestphp/pest-plugin-browser`, `visit()` driving Playwright | new tests only | Several of the strongest gains need **no change to existing tests**, which is the core of the business case. ## What it costs 1. **Runtime floor.** Pest 5 requires PHP 8.4. A service still on 8.3 cannot adopt it without upgrading PHP first. 2. **Version coupling.** Pest's composer.json pins PHPUnit tightly: Pest 5.2.1 requires `phpunit/phpunit` `^13.3.4` and declares a conflict with anything above 13.3.4. A PHPUnit bug fix or feature therefore waits for a matching Pest release. 3. **Two styles in one suite.** During the migration, reviewers read both class-based tests and closure tests. A written convention ("new tests in Pest, old ones converted when touched") keeps this manageable. 4. **Tooling.** Closure tests use `$this` bound at runtime, which static analysers do not understand out of the box; `pestphp/pest-plugin-phpstan` teaches PHPStan about `it()`, `expect()` and `$this` in tests. 5. **Team familiarity.** Most PHP developers know PHPUnit; Pest's expectation API is quick to learn, but it is a learning cost. ## A rollout that keeps the suite green at every step 1. `composer require pestphp/pest --dev --with-all-dependencies` and `./vendor/bin/pest --init`, then run the unchanged suite under Pest and compare counts with the PHPUnit run. 2. Switch CI to `./vendor/bin/pest`, optionally with `--parallel` once isolation problems are fixed. 3. Add `arch()` rules for the boundaries the team already agrees on, plus the `php` and `security` presets. 4. Pick one critical domain module, add `covers()` to its test files, and introduce `--mutate --min` at the current score. 5. Write new tests in Pest; convert existing files with the drift plugin (`./vendor/bin/pest --drift tests/Unit/Billing`) when a file is being changed anyway, reviewing the output. ## What to measure during the pilot A decision like this is easier to defend with evidence from a small pilot than with opinions: - **Parity.** The number of tests and assertions under `./vendor/bin/pest` should match the PHPUnit run before anything else changes. - **Wall-clock time** for the full suite serially and with `--parallel`, and for a typical local change with `--tia`. - **Findings.** How many boundary violations the first `arch()` rules and presets report, and how many untested mutations `--mutate` finds in the pilot module. - **Developer feedback** after a few weeks of writing new tests in Pest style. If the arch and mutation findings are real bugs or real debt, the case makes itself; if the suite was already fast and the rules find nothing, the migration is cost without payoff. ## Signals for "not now" - The codebase cannot move to PHP 8.4 in the planning horizon. - The team depends on a PHPUnit extension or version that Pest's pin blocks. - The suite is small and stable, and none of the added capabilities addresses a real pain. There is no single right answer. A strong answer names the incremental path, the concrete wins that need no rewrite, and the version coupling that a lead has to own.

  • Do you have to convert every PHPUnit test class before switching CI to Pest?
    No. Pest's runner executes classes that extend PHPUnit's TestCase alongside closure tests, and it reads the same phpunit.xml. You can switch the CI command first and convert files gradually, for example with the drift plugin, as they are touched.
  • What risk does Pest's PHPUnit version pin create for a platform team?
    PHPUnit upgrades become gated on Pest releases: Pest 5.2.1 conflicts with PHPUnit above 13.3.4, so a needed PHPUnit fix cannot be installed until Pest widens its range. The team has to track both release streams and accept that lag.

saying these in an interview costs you the question

  • Adopting Pest means rewriting every PHPUnit test before anything runs
  • Pest replaces PHPUnit with its own runner and assertion engine
  • phpunit.xml is ignored once Pest is installed
  • Pest 5 runs on PHP 8.2 like earlier releases
  • Pest lets you upgrade PHPUnit independently at any time