// docs / tia

Tia Engine

The Tia Engine — short for Test Impact Analysis — is a convenient way to reduce the time it takes to run your test suite, re-running only the tests affected by your latest changes. The first time you run with --tia, the engine records a graph of which tests depend on which files. On every run after that, the engine looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.

A typical Laravel suite that used to take 10 minutes now replays in around 4 seconds. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.

To get started, you may add the --tia flag to any Pest invocation:

1./vendor/bin/pest --parallel --tia

Warning: The Tia Engine requires a code coverage driver — either PCOV or Xdebug — to be installed and enabled. The engine uses it to record which files each test touches while building the baseline. Without a coverage driver available, Pest cannot record the dependency graph, and TIA will not run.

The first run is the baseline — the engine enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. You may expect a small overhead on this run only.

Note: You don't have to pay this baseline cost on every machine. You may have CI record the baseline once and have every developer download it from GitHub Actions, so their very first --tia run replays immediately. See Sharing The Baseline From CI to set this up.

Every subsequent run is a replay. The engine compares your working tree against the baseline and re-runs only the tests affected by your changes:

1Tests: 774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)
2Duration: 3.92s

In this example, affected is the set of tests Pest re-ran because their dependencies changed. uncached means Pest had to execute a test because no cached result existed yet. Finally, replayed is the set whose results were served from cache.

A replay isn't a shortcut that skips work — it's a faithful reconstruction of the real run. When the engine caches a test, it stores not just the pass or fail result but everything that test produced, including the exact lines and branches it covered. So a replayed run reports the same code coverage as a full run, and everything that depends on it keeps working — coverage thresholds, the --coverage report, and --min all behave exactly as if every test had executed from scratch. You get the speed of replaying with none of the fidelity lost.

How The Engine Decides What To Run

For each file you have changed, the engine looks for the tests that depend on it:

  • PHP source files — your app/ classes, controllers, models, helpers — are tracked through the coverage driver. A change to app/Models/User.php re-runs only the tests that touched User.
  • Migrations are intersected with the tables each test queried during the baseline. A column rename in create_users_table.php re-runs only the tests that queried the users table.
  • Inertia pages under resources/js/Pages re-run only the tests that server-side-rendered them.
  • Shared JS components under resources/js/Components, Layouts, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.
  • Frontend runtime files like resources/js/App.jsx, resources/js/bootstrap.js, resources/js/echo.js, and resources/js/favicon.js re-run tests that rendered Inertia components, because they can affect the whole client runtime.
  • Blade templates re-run only the tests that rendered them, including renders triggered by browser tests.
  • Arch tests re-run for project PHP source changes, because Arch expectations inspect files by namespace and path instead of executing those files.
  • Browser assets such as CSS, public build files, static public assets, and public/hot re-run browser tests only.
  • Anything else — config files, route files, fixture data, files outside the recorded graph — falls through to a broader pattern. Editing config/app.php re-runs every test, because Pest cannot statically prove which tests depend on it.

Some files change the shape of the graph itself rather than a single test result. Pest rebuilds the graph when structural inputs drift, including composer.lock, phpunit.xml / phpunit.xml.dist, vite.config.*, Node lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, bun.lockb), and tsconfig / jsconfig files. Upgrading your PHP version invalidates the cached results and re-executes the suite while keeping the graph intact.

Cosmetic Edits Don't Run Anything

Pest normalizes file content before comparing, so cosmetic changes will not trigger any tests. PHP files have their whitespace, line comments, and docblocks stripped before hashing. Blade strips {{-- … --}} comments. JS, TS, Vue, and Svelte files have their line and block comments removed too.

As a result, a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.

Built-in Environments

Pest ships with watch defaults for the most common PHP stacks, and applies them automatically when their packages are installed:

  • PHP — always-on rules that re-run the whole suite when .env files (.env, .env.testing, .env.local, .env.*.local), docker-compose.yml / docker-compose.yaml, phpunit.xml*, your test fixtures (tests/Fixtures/** and nested Fixtures directories), or snapshot files (tests/.pest/snapshots/**/*.snap) change.
  • Laravel — non-PHP files under app/, database/migrations/, storage/fixtures/, resources/views/, lang/ and resources/lang/, plus build configs such as vite.config.*, webpack.mix.*, tailwind.config.*, and postcss.config.*.
  • Symfonyconfig/, migrations/, src/Migrations/, templates/, translations/, config/doctrine/, assets/, webpack.config.js, and importmap.php.
  • Livewireresources/views/livewire/, resources/views/components/, resources/views/pages/, plus JS/TS under resources/js/.
  • Inertia — server-side-rendered pages under resources/js/Pages and the Vite module graph for Components, Layouts, and runtime entry files.
  • Browser — CSS, public build files, static public assets, and public/hot re-run browser tests only.

There is no need to configure anything to opt in to these — Pest detects each framework via Composer and merges the relevant rules for you. To extend or override them, see Custom Watch Patterns.

Modes

Pest supports a few flags alongside --tia:

Flag Behavior
--tia Replay if a baseline graph exists, otherwise record.
--no-tia Disable TIA for this run, even if pest()->tia()->always() is configured.
--tia --fresh Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale.
--tia --refetch Discard the local graph and force a fresh CI baseline fetch, bypassing the 24-hour cooldown that otherwise applies after a fetch found no baseline.
--tia --filtered Narrow PHPUnit to only the affected test files rather than loading all tests and replaying cached results for unaffected ones. Automatically disabled when you pass an explicit test path or a --coverage report; if no tests are affected, Pest stops and tells you so.
--tia --locally Equivalent to pest()->tia()->always()->locally() — run TIA automatically on local machines but skip on CI.
--tia --baselined Opt in to fetching the shared baseline from CI when no local graph exists or the local graph drifts.
--baseline Print the absolute path of this project's TIA storage directory and exit. Designed for CI uploads — see Sharing The Baseline From CI.

Environment Variables

Each enabling flag has an environment variable equivalent, useful for CI matrices, container entry points, and shared developer configs:

Variable Equivalent flag
PEST_TIA=1 --tia
PEST_TIA_FILTERED=1 --filtered
PEST_TIA_LOCALLY=1 --locally
PEST_TIA_BASELINED=1 --baselined

Sharing The Baseline From CI

Recording the baseline locally may take minutes on large suites. Instead, you may have CI record it once per merge to main, and every developer downloads the result.

Baseline fetching is opt-in. You may enable it with --tia --baselined on the command line, the PEST_TIA_BASELINED=1 environment variable, or — preferred for teams — by calling pest()->tia()->baselined() in tests/Pest.php. Once enabled, when Pest detects no local graph (or the local graph is out of date) it uses GitHub's CLI to download the latest successful run of a tia-baseline.yml workflow's pest-tia-baseline artifact. Pest then validates the fetched graph against your project state — if it matches, it is adopted. Otherwise, it is discarded and a local rebuild proceeds.

Note: Baseline fetching relies on the GitHub CLI (gh), so it is only available for repositories hosted on GitHub, and gh must be installed and authenticated (gh auth login) on the machine doing the fetch. When a fetch cannot proceed — missing CLI, no authentication, a network or rate-limit error, or no baseline artifact yet — Pest reports the reason and falls back to recording a local baseline.

Here is a starter workflow you may drop into .github/workflows/tia-baseline.yml:

1name: TIA Baseline
2on:
3 push: { branches: [main] }
4 schedule: [{ cron: '0 3 * * *' }]
5 workflow_dispatch:
6jobs:
7 baseline:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 with: { fetch-depth: 0 }
12 - uses: shivammathur/setup-php@v2
13 with: { php-version: '8.4', coverage: xdebug }
14 - run: composer install --no-interaction --prefer-dist
15 
16 - name: Run tests
17 run: ./vendor/bin/pest --parallel --tia --coverage --fresh
18 
19 - name: Resolve TIA baseline path
20 id: baseline
21 run: echo "path=$(./vendor/bin/pest --baseline)" >> "$GITHUB_OUTPUT"
22 
23 - name: Upload TIA baseline
24 uses: actions/upload-artifact@v4
25 with:
26 name: pest-tia-baseline
27 path: ${{ steps.baseline.outputs.path }}
28 include-hidden-files: true
29 retention-days: 30

The ./vendor/bin/pest --baseline command prints the absolute path to this project's TIA storage directory (typically ~/.pest/tia/<project-key>/), which is exactly what actions/upload-artifact needs to bundle the recorded graph and coverage cache. Note that include-hidden-files: true is required because the baseline lives under a dot-prefixed directory.

After CI runs, every developer with baselined() enabled who runs ./vendor/bin/pest --tia for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.

Storage

Pest stores its state at ~/.pest/tia/<project-key>/, where the project key is derived from your normalized git remote URL — so git@github.com:foo/bar.git and https://github.com/foo/bar produce the same key. A non-git project falls back to a hash of the project's absolute path.

Sharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.

Configuration

You may configure TIA behavior in tests/Pest.php via pest()->tia():

1pest()->tia()
2 ->always() // run TIA on every invocation, no --tia flag needed
3 ->locally() // restrict always() to local environments only
4 ->baselined() // fetch the shared baseline from CI when no local graph exists
5 ->filtered(); // narrow PHPUnit to only affected test files

always() activates TIA for every pest run without requiring the --tia flag. Pair it with locally() to restrict that behavior to local machines — when you pass the --ci flag, TIA is skipped automatically. An explicit --tia on the command line always takes effect regardless, and --no-tia will disable it for a single run.

filtered() enables filtered mode, equivalent to --tia --filtered. In this mode, Pest narrows PHPUnit to only the affected test files rather than loading the full suite and replaying cached results for unaffected tests:

1pest()->tia()->filtered();

baselined() opts in to fetching the shared TIA baseline from CI when no local graph exists or the local graph drifts. See Sharing The Baseline From CI for the recommended workflow:

1pest()->tia()->baselined();

Custom Watch Patterns

Sometimes your project may have a directory layout that does not match the framework defaults. In that case, you may register custom watch patterns in tests/Pest.php:

1pest()->tia()->watch([
2 'config/billing/**/*.php' => 'tests/Feature/Billing',
3 'public/build/**/*' => 'tests/Browser',
4]);

Each glob maps to a test directory or an exact test file. Whenever a matching file changes, every test under that directory is invalidated. If a glob already exists in Pest's defaults, your target will be merged with the existing targets rather than replacing them.


Now that you've learned how to use the Tia Engine to speed up your test suite, let's explore how to automatically modernize and refactor your test code with Rector: Rector