DEV Community

Cover image for Pest 5 TIA: Run Only the Tests Your Code Change Touched
Hafiz
Hafiz

Posted on • Originally published at hafiz.dev

Pest 5 TIA: Run Only the Tests Your Code Change Touched

Originally published at hafiz.dev


Nuno Maduro announced Pest 5 on stage at Laracon US yesterday, and one feature is worth your attention today: the Tia Engine. TIA stands for Test Impact Analysis, and the pitch is simple enough to sound too good to be true. A Laravel suite that took 10 minutes now finishes in around 4 seconds, because Pest reruns only the tests your latest change could actually break and replays cached results for everything else.

This shipped yesterday, so this is a walkthrough of how it works from the docs and the release, not a battle-tested war story. I'll be clear about what's proven and what you'll want to verify on your own suite. But the mechanism is well documented, the numbers come from Pest directly, and the setup really is a one-flag start. Here's what TIA does, how to turn it on, the CI trick that makes it fast for your whole team, and the catch nobody mentions in the announcement tweets.

What Test Impact Analysis Actually Does

The idea behind TIA isn't new (large monorepos at Google and Meta have done impact analysis for years), but it's new to have it built into a PHP testing framework you already use. The concept: most of your test suite is irrelevant to any given change. You edit one model, and 765 of your 774 tests couldn't possibly behave differently. Running all 774 is wasted time. TIA figures out which handful actually depend on what you touched, runs those, and reconstructs the rest from cache.

The first run builds a baseline: Pest records a dependency graph of which tests exercise which files. Every run after that is a replay. Pest diffs your working tree against the baseline and reruns only the affected tests. The output tells you exactly what happened:

Tests:    774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)
Duration: 3.92s
Enter fullscreen mode Exit fullscreen mode

Read that line: 7 tests actually ran because their dependencies changed, 2 ran because no cached result existed yet, and 765 were served from cache. Four seconds instead of ten minutes.

The part I found most reassuring in the docs: a replay isn't a shortcut that skips coverage. Pest stores what each test covered, down to lines and branches, so a replayed run reports the same coverage as a full run. Your --coverage report, coverage thresholds, and --min all behave as if every test executed. You get the speed without lying to your coverage gate.

Turning It On

One flag on any Pest invocation:

./vendor/bin/pest --parallel --tia
Enter fullscreen mode Exit fullscreen mode

There's one hard requirement, and it's the thing that'll trip people up: TIA needs a code coverage driver, either PCOV or Xdebug, installed and enabled. Pest uses it to record which files each test touches while building the baseline. No coverage driver, no dependency graph, no TIA. If you already run coverage in CI you have this; if you've never installed PCOV locally, that's your first step.

The other requirement is the bigger gate: Pest 5 requires PHP 8.4 or higher. It's built on PHP 8.4 and PHPUnit 13. If you're on an older PHP version, TIA is a reason to plan the upgrade, not something you can try this afternoon. (If you're mid-migration on the framework side, my Laravel 12 to 13 upgrade guide covers that half.)

If you'd rather not type the flag every time, configure it in tests/Pest.php:

pest()->tia()
    ->always()     // run TIA on every invocation, no --tia flag
    ->locally()    // but only on local machines, skip on CI
    ->baselined()  // fetch the shared baseline from CI when missing
    ->filtered();  // narrow PHPUnit to only affected test files
Enter fullscreen mode Exit fullscreen mode

The always()->locally() pairing is the sensible default for most teams: fast replays while you work, full runs on CI where you want every test to execute for real. An explicit --tia always wins, and --no-tia disables it for a single run.

How TIA Decides What To Rerun

This is where it stops being magic and starts being understandable, and understanding it is what tells you whether to trust it. TIA maps different file types to tests in different ways:

For PHP source files, the coverage driver does the work. Change app/Models/User.php and Pest reruns only tests that touched User. For migrations, Pest intersects the change against the tables each test queried during the baseline, so a column rename in the users migration reruns only tests that hit the users table. Blade templates rerun only the tests that rendered them. If you're on Inertia, Pest walks Vite's module graph to trace which pages import a changed component and reruns only the tests that server-side-rendered those pages.

Then there's the honest fallback. Config files, route files, fixtures, anything outside the recorded graph triggers a broader pattern. Edit config/app.php and Pest reruns the entire suite, because it can't statically prove which tests depend on it. That's the correct behavior, and it's a good sign: TIA errs toward running too much rather than missing a regression. Some changes rebuild the graph itself, like composer.lock, phpunit.xml, and Node lockfiles.

View the interactive diagram on hafiz.dev

The Detail That Sells It: Cosmetic Edits Run Nothing

Here's the feature that makes TIA feel different from a dumb file-timestamp cache. Pest normalizes file content before comparing. PHP files get whitespace, line comments, and docblocks stripped before hashing. Blade strips its {{-- --}} comments. JS, TS, Vue, and Svelte lose their comments too.

The practical result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash. The file never enters the changed set. Zero tests run. If you've ever watched your full suite grind through CI because someone reformatted a file, you understand why that matters.

Sharing the Baseline: The CI Trick

Recording the baseline takes as long as one full suite run, which on a large project is minutes. Paying that on every developer's machine, and every fresh checkout, would undercut the whole point. Pest's answer: record the baseline once in CI, and have everyone else download it.

You enable fetching with pest()->tia()->baselined() in tests/Pest.php (preferred for teams). When Pest finds no local graph, it uses the GitHub CLI to pull the pest-tia-baseline artifact from your latest successful baseline workflow, validates it against your project state, and adopts it if it matches. Every developer's first --tia run then replays immediately, paying no record cost.

The workflow that produces that artifact, dropped into .github/workflows/tia-baseline.yml:

name: TIA Baseline
on:
  push: { branches: [main] }
  schedule: [{ cron: '0 3 * * *' }]
  workflow_dispatch:
jobs:
  baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.4', coverage: xdebug }
      - run: composer install --no-interaction --prefer-dist
      - name: Run tests
        run: ./vendor/bin/pest --parallel --tia --coverage --fresh
      - name: Resolve TIA baseline path
        id: baseline
        run: echo "path=$(./vendor/bin/pest --baseline)" >> "$GITHUB_OUTPUT"
      - name: Upload TIA baseline
        uses: actions/upload-artifact@v4
        with:
          name: pest-tia-baseline
          path: ${{ steps.baseline.outputs.path }}
          include-hidden-files: true
          retention-days: 30
Enter fullscreen mode Exit fullscreen mode

Two things to know here. ./vendor/bin/pest --baseline prints the absolute path to the TIA storage directory (~/.pest/tia/<project-key>/), which is what the upload step bundles. And include-hidden-files: true is required, because that directory is dot-prefixed and Actions skips hidden files by default. Fetching relies on the GitHub CLI being installed and authenticated, so it's GitHub-only for now; on any failure Pest falls back to recording locally and tells you why. If you don't have a Pest CI pipeline yet, my GitHub Actions guide for Laravel is the place to start before wiring this in.

The Catch Nobody's Tweeting About

The announcement numbers are real, but here's what to keep in mind before you assume your suite drops to 4 seconds tomorrow.

TIA's speedup is proportional to how well-isolated your tests are. If most of your tests are unit tests touching a handful of classes, a small change reruns almost nothing and you get the headline numbers. If your suite is mostly full feature tests that boot the whole app and hit the database, more of them qualify as "affected" by any given change, and the win shrinks. The 10-minutes-to-4-seconds figure describes a well-structured suite, not a guaranteed outcome for every codebase.

There's also a trust question worth being deliberate about. TIA is deciding not to run tests. That's the entire point, and the coverage-fidelity design is reassuring, but this is day-one software. The safe pattern is exactly what the config nudges you toward: always()->locally() for fast local replays, and full untia'd runs on CI as the source of truth. Let TIA speed up your inner loop first. Trust it to gate your deploys only once you've watched it behave on your real codebase for a while. That's not skepticism about Pest; it's how you'd adopt any tool that skips work on your behalf.

What Else Shipped in Pest 5

TIA is the headline, but the release bundled several first-party plugins that matured across the Pest 4 cycle. Worth knowing they exist:

The Agent plugin gives AI coding agents a single command to verify a change actually works against your real suite, and with the Browser plugin, in a real browser too. Evals let you score LLM output and AI-generated results from inside your test suite, mixing deterministic checks with AI scorers. And PHPStan and Rector are now built in, so static analysis and automated refactoring live alongside your tests instead of as separate tooling. If you're still on Pest 4, my complete Pest 4 testing guide still applies for the fundamentals; Pest 5's testing API is the same, with these plugins layered on top.

FAQ

Do I need to change how I write tests to use Pest 5 TIA?

No. TIA works with your existing tests unchanged. You add the --tia flag (or configure it in tests/Pest.php) and Pest builds the dependency graph automatically from a normal run. Your test files, expectations, and structure stay exactly as they are.

Does TIA reduce my code coverage numbers?

No, and this is the clever part. When Pest caches a test it stores the exact lines and branches that test covered, so a replayed run reports the same coverage as a full run. Coverage thresholds, the --coverage report, and --min all behave as if every test executed from scratch.

What happens when I change a config or route file?

Pest reruns the entire suite. Files outside the recorded dependency graph, including config and route files, can't be statically traced to specific tests, so TIA falls back to running everything rather than risk missing a regression. It errs toward safety.

Can my whole team skip the slow first baseline run?

Yes, if you're on GitHub. Record the baseline once in a CI workflow, upload it as the pest-tia-baseline artifact, and enable pest()->tia()->baselined(). Each developer's first --tia run downloads that baseline and replays immediately instead of recording locally. It relies on the GitHub CLI, so it's GitHub-only for now.

Should I trust TIA to gate my production deploys?

Be cautious at first. TIA shipped at Laracon US 2026, so it's brand new. The sensible adoption path is always()->locally(): fast replays while you develop, full runs on CI as your source of truth. Once you've watched TIA behave correctly on your real suite over time, you can decide whether to lean on it further.

Does Pest 5 TIA need a specific PHP version?

Yes. Pest 5 requires PHP 8.4 or greater and is built on PHPUnit 13. TIA also needs a coverage driver (PCOV or Xdebug) to record the baseline. If you're on an older PHP version, upgrading is a prerequisite.

Wrapping Up

TIA is the kind of feature that changes a daily habit rather than adding one. The slow test suite you run less often than you should, or gate behind a coffee break, becomes something fast enough to run after every save. Add --tia, make sure you've got PHP 8.4 and a coverage driver, and let it speed up your local loop first. Wire up the CI baseline once your team wants the shared graph. And keep your CI running the full suite untia'd until you've earned trust in the replays on your own codebase.

Top comments (0)