# phpunit-watcher: rerunning PHPUnit when your source changes

> phpunit-watcher is a small command line tool that runs your PHPUnit suite and reruns it whenever a file in your source or test directory changes, with configuration for watched directories, the PHPUnit binary, arguments and desktop notifications. Its most interesting limitation is the one it documents on Windows, where the interactive commands do not work at all.

**spatie/phpunit-watcher** — A tool to automatically rerun PHPUnit tests when source code changes

- Repository: https://github.com/spatie/phpunit-watcher
- Website: https://spatie.be/opensource/php
- Stars: 875 · Forks: 75
- Language: PHP
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-phpunit-watcher

## The loop it gives you

The package does one thing and the README asks the question directly: wouldn't it be great if your PHPUnit tests were automatically rerun whenever you change some code. With the package installed the command is short.

```bash
phpunit-watcher watch
```

That runs the tests and reruns them whenever a file in the app, src or tests directory is modified. Passing arguments to PHPUnit is a matter of appending them, so a single test can be targeted.

```bash
phpunit-watcher watch --filter=it_can_run_a_single_test
```

What makes the tool more than a shell loop is that the rerun happens in the same process with the same terminal, so the output of the failing test appears where you are already looking rather than in a separate pane. The README includes a screenshot of the watcher in action and links a talk from Laracon EU 2017 in which Amo Chohan demonstrates it, which is a reasonable indication of how old and how settled the idea is.

## Global install or local dev dependency

There are two installation routes and they change how you invoke the tool. Globally, the package is installed with Composer and the command becomes available in any directory on your system.

```bash
composer global require spatie/phpunit-watcher
```

Locally, it goes in as a development dependency of the project.

```bash
composer require spatie/phpunit-watcher --dev
```

In that case you run it as vendor/bin/phpunit-watcher watch, and the README says that all the examples assume a global installation, so if you chose the local route you prepend vendor/bin/ everywhere the command is mentioned. The global route suits a developer who works across several projects and wants the same watcher everywhere, while the local route pins the version to the project and keeps it in the lockfile, which is the safer choice when the team wants the behaviour to be identical on every machine.

## The configuration file and where it is looked up

Behaviour is controlled by a YAML file that may be named .phpunit-watcher.yml, phpunit-watcher.yml or phpunit-watcher.yml.dist, and the tool looks for them in that order. If no config file exists in the project directory, the tool checks the parent directories of the project directory. The lookup order and the parent directory fallback together mean that a monorepo can hold one configuration for the whole tree, and a package inside it can override it locally, which is the arrangement most teams arrive at eventually. The example configuration shows the shape of the file.

```yaml
watch:
  directories:
    - src
    - tests
  fileMask: '*.php'
notifications:
  passingTests: false
  failingTests: false
phpunit:
  binaryPath: vendor/bin/phpunit
  arguments: '--stop-on-failure'
  timeout: 180
```

The three top-level keys are watch, notifications and phpunit, and each has its own options documented further down.

## Watched directories and the performance escape hatch

The watch section is where the practical tuning happens, and the README is candid that a large repository will be slow without help. The directories list says what to watch and the exclude list says what to leave out, the fileMask restricts the file types, and there are three ignore switches: ignoreDotFiles, ignoreVCS and ignoreVCSIgnored.

```yaml
watch:
  directories:
    - src
    - tests
  exclude:
    - lib
  fileMask: '*.php'
  ignoreDotFiles: true
  ignoreVCS: true
  ignoreVCSIgnored: false
```

The guidance is that if you see performance delays with large repositories you should add exclude entries for large subdirectories you do not need to watch, that enabling the ignore options can also help, and that you should make sure you are using the '*.php' file mask. That last point matters more than it looks: without a file mask the watcher reacts to every write in the tree, including the editor swap files and the cache directories that a test run itself creates, which is how a watcher ends up triggering itself. The exclude and ignore options work through the Symfony Finder, and the README links to the Finder documentation for the details.

## Desktop notifications, the help line and hiding both

By default the tool displays a desktop notification whenever the tests pass or fail, and it shows a helper for keyboard actions after each run. Both are switchable. The notifications section takes a passingTests and a failingTests key, each set to false to disable that notification.

```yaml
notifications:
  passingTests: false
  failingTests: false
```

The keyboard helper is hidden with a hideManual key.

```yaml
hideManual: true
```

The notifications exist because the watcher is expected to be running in a background window while you work in the editor, so the failure notification is how you find out something broke. Turning both off is a matter of taste, and a team with a shared screen will probably want the failing notification off for the same reason the manual is.

## Pointing it at a different PHPUnit

The phpunit section controls the runner itself. The binaryPath option defaults to vendor/bin/phpunit, and exists for people whose binary lives elsewhere, such as a project that installs PHPUnit as a phar or keeps it in a non-standard path.

```yaml
phpunit:
  binaryPath: ./vendor/phpunit/phpunit/phpunit
```

The arguments option passes the same arguments to PHPUnit every time the watcher starts.

```yaml
phpunit:
  arguments: '--stop-on-failure'
```

The precedence rule is worth noting because it decides which arguments actually run. Arguments supplied on the command line when you start the watcher, such as phpunit-watcher watch --filter=my_favourite_test, are used instead of the ones in the config file. So the config file is the default and the command line is the override. There is also a timeout option, 180 in the example, which bounds how long a single run may take, and a stop-on-failure argument is a sensible default for a watch loop because it stops at the first failure instead of running the rest of the suite.

## The Composer script caveat and the Windows gap

Two operational notes sit between the tool and real use. The first is Composer scripts: when you run phpunit-watcher from a Composer script you may need to redirect input for the interactive commands to work, and to disable the default Composer process timeout. The README gives the recipe.

```json
{
    "scripts": {
        "test:watch": [
            "Composer\\Config::disableProcessTimeout",
            "phpunit-watcher watch < /dev/tty"
        ]
    }
}
```

Both halves are needed. The redirect gives the watcher the terminal it needs for its keyboard commands, and disableProcessTimeout stops Composer from killing the watcher as a long-running process. The second note is the harder limitation. On Windows, TTY is not being supported, so any interaction has been disabled, and while watching for changes works, any arguments for PHPUnit have to be provided when initially calling phpunit-watcher. So on Windows the tool becomes a non-interactive watcher with a fixed argument set, which is workable but loses the interactive parts.

## Where phpunit-watcher is the wrong choice

Four limits apply. It reruns the whole suite rather than selecting the tests affected by your change, so on a large project the loop is only as fast as your suite, and a suite that takes a minute is a minute-long loop no matter how good the watcher is. It watches directories rather than understanding your code, so the file mask and the exclude list are the only controls and there is no dependency graph to narrow the run. It is tied to PHPUnit as the runner, so a project on Pest or another runner gets nothing from it. And the Windows gap removes the interactive behaviour there entirely. The counter-argument is that none of this is the tool's job, and the fact that it does one narrow thing well, with a config file that covers the cases that matter, is a reasonable thing to want from a tool you will look at every day.

## Conclusion

Use phpunit-watcher when you want a tight edit-and-test loop without wiring up a file watcher and a test runner yourself, since it handles the change detection, the rerun and the notification in one command. Do not expect it to narrow the run to the affected tests, because the tool reruns the suite rather than selecting tests for you, so a slow suite stays slow. Verify first that your project uses the directory names it watches by default, that the PHPUnit binary path in your config file is right if you keep phpunit somewhere other than vendor/bin, and on Windows that you accept that the interactive commands are disabled and every argument has to be supplied at startup.

## FAQ

### How do I install phpunit-watcher and run it?

Install it globally with composer global require spatie/phpunit-watcher, or locally as a dev dependency with composer require spatie/phpunit-watcher --dev and run vendor/bin/phpunit-watcher watch. The watch command runs the tests and reruns them whenever a file in the app, src or tests directory is modified.

### Where does phpunit-watcher look for its config file?

It looks for .phpunit-watcher.yml, phpunit-watcher.yml or phpunit-watcher.yml.dist, in that order. If no config file exists in the project directory, it checks the parent directories of the project directory, which lets a monorepo share one configuration.

### Can I pass arguments to PHPUnit through phpunit-watcher?

Yes. You can tack them onto the watch command, for example phpunit-watcher watch --filter=it_can_run_a_single_test, and those arguments take precedence over the arguments option in the config file. The config file also has a binaryPath option for a PHPUnit binary somewhere other than vendor/bin/phpunit.

### Does phpunit-watcher work on Windows?

Watching for changes works, but TTY is not supported, so any interaction has been disabled and any arguments for PHPUnit have to be provided when initially calling phpunit-watcher. When running it from a Composer script you also need to redirect input and disable the Composer process timeout.

### How do I stop phpunit-watcher from being slow on a large repository?

Add exclude entries for large subdirectories you do not need to watch, enable the ignoreDotFiles, ignoreVCS and ignoreVCSIgnored options, and make sure the fileMask is set to '*.php'. The directory scanning uses the Symfony Finder, whose documentation is linked from the README.

## Sources

- [License: MIT](https://github.com/spatie/phpunit-watcher/blob/main/LICENSE)
- [Project website](https://spatie.be/opensource/php)
- [README](https://github.com/spatie/phpunit-watcher/blob/main/README.md)
- [Releases](https://github.com/spatie/phpunit-watcher/releases)
- [spatie/phpunit-watcher on GitHub](https://github.com/spatie/phpunit-watcher)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-phpunit-watcher
