SmartTest

PHP (preview)

SmartTest selects and runs the tests of PHP projects that use PHPUnit, Pest or php artisan test, Laravel applications included. PHP is a preview: it never hides a failure, but its precision is coarser than for TypeScript. SmartTest reads the PHP sources in TypeScript, without running PHP, and runs your own test runner as a subprocess.

Installing beside PHP #

SmartTest is an npm package. A PHP repository needs Node (22.12 or later) on the machine, not a package.json:

npx --yes @tkmremi/smarttest init      # writes smarttest.config.json and the Git hook
npx --yes @tkmremi/smarttest run       # runs the tests the uncommitted change can affect

In a repository without package.json, init writes the configuration as smarttest.config.json, which loads without installing the package, and the hook runs SmartTest through npx --yes @tkmremi/smarttest. If the repository also has a package.json (a Laravel app with Vite), you can instead npm install -D @tkmremi/smarttest; the hook then runs the installed copy.

PHP itself must be installed with the project's Composer dependencies (composer install): SmartTest runs vendor/bin/phpunit, vendor/bin/pest or php artisan test. The PHP binary is, in this order: SMARTTEST_PHP, php.binary in the config, then php on PATH. A bare command (php8.3) is looked up on PATH; a relative path is relative to the project.

{
  "php": { "enabled": true, "binary": "php8.3" }
}

"php": { "enabled": false } turns the preview off: .php files are no longer analysed and PHPUnit is no longer run.

Supported setups #

SetupHow SmartTest finds and runs the tests
PHPUnit 10 or 11 (phpunit.xml, phpunit.xml.dist)--list-tests-xml lists the test classes PHPUnit collects from the configuration: its test suites, folders, suffixes, exclude entries and excluded groups. A selection runs with the files as arguments; results come from --log-junit
php artisan test (a Composer script runs it)The same, through php artisan test, which runs PHPUnit or Pest with Laravel's own options
Pest (vendor/bin/pest)The same, through Pest

A PHP project is any folder with a composer.json (outside vendor/) whose PHPUnit configuration or *Test.php files show tests; smarttest doctor shows phpunit (. · phpunit.xml) collects 1998 test file(s). SmartTest turns off the project's own PHPUnit logging and result cache for its runs (--no-logging, --do-not-cache-result): it writes nothing in the project. Xdebug is off unless you set XDEBUG_MODE.

A fatal error, a selected file PHPUnit runs no test from, and "No tests executed" are failures, never passes. When PHPUnit fails a run without a failing test (a risky test with failOnRisky, a warning with failOnWarning), the tests its event log names fail; if it names none, every file of that run fails.

What is understood #

What selects a test #

You changeSelected
A methodThe tests that reach it through one of the edges above
A constructor, a property, a constant, an enum case, the class's declarationEvery test that reaches the class
A traitEvery test that reaches a class using it
A class a test or a factory builds (new X, return new self)Every test that builds or receives it: any of its members can be called on the object
A controller, or what one usesThe tests that request one of its routes
A command's handle(), or what it usesThe tests that run that command
A route file, a config file, a provider, a migration, a translation, a view, tests/TestCase.phpEvery test that boots the application
What the application boot reaches, when the tests run through php artisan testEvery test: Artisan boots the application before PHPUnit starts, plain test cases too
phpunit.xml, its bootstrap file, .env.testing, tests/Pest.phpEvery test of that PHPUnit configuration
composer.json, composer.lockEvery test in that folder and below
A test fileThat test

What is uncertainty #

A name SmartTest cannot read is uncertainty: the tests that reach it are selected in BALANCED and SAFE, whatever changed. That covers:

When you know what such a line can load, say so in a comment on that line or the line above, with globs relative to the project root; those files become its dependencies instead of uncertainty:

// smarttest-loads: app/Models/*.php
$model = $class::query()->find($id);

Name every place it can load from: the comment replaces the uncertainty.

Known limits #

Edit this page on GitHub