PHPUnit & Pest Test Explorer for VS Code

繁體中文
Run PHPUnit and Pest tests directly in VS Code using the native Test Explorer UI.

Quick Start
- Install the extension from the VS Code Marketplace
- Open a PHP project that contains a
phpunit.xml or phpunit.xml.dist
- Tests appear automatically in the Test Explorer sidebar — click to run
PHPUnit and Pest are auto-detected from vendor/bin. No configuration needed in most projects.
Features
- Test Explorer integration — discover, run, and debug tests from the sidebar
- PHPUnit 7 – 12 & Pest 1 – 4 — broad version support
- Auto-detect binary — reads
composer.json to use vendor/bin/pest for Pest projects automatically
- Auto-reload — reloads all tests when
phpunit.xml or composer.lock changes
- Colored output — syntax-highlighted results with embedded PHP source snippets
- Clickable stack traces — jump to file:line directly from error output
- Remote environments — Docker, SSH, Laravel Sail, DDEV via custom commands
- Multi-workspace Docker — single shared container for multiple workspace folders
- Parallel execution — ParaTest support
- Xdebug debugging — step-through debugging with one click
- Continuous runs — auto-run tests on file changes

Settings
Add to .vscode/settings.json. All settings use the phpunit.* prefix.
{
// Path to the PHP binary (default: "php")
"phpunit.php": "php",
// Path to PHPUnit or Pest binary
// Auto-detected from composer.json: uses "vendor/bin/pest" if pestphp/pest is a dependency,
// otherwise defaults to "vendor/bin/phpunit". Only set this if auto-detection is not sufficient.
"phpunit.phpunit": "vendor/bin/phpunit",
// Custom command template
// Variables: ${php}, ${phpargs}, ${phpunit}, ${phpunitargs}, ${phpunitxml}, ${cwd}
// ${workspaceFolder} — absolute path to the workspace folder
// ${workspaceFolderBasename} — folder name only (e.g. "myproject")
// ${userHome} — user home directory
// ${pathSeparator} — OS path separator ("/" or "\")
"phpunit.command": "\"${php}\" ${phpargs} \"${phpunit}\" ${phpunitargs}",
// Extra arguments passed to PHPUnit
"phpunit.args": [],
// Path mappings for remote environments { "local/path": "remote/path" }
"phpunit.paths": {},
// Environment variables set before running
"phpunit.environment": {},
// Save all open files before running tests (default: false)
"phpunit.saveBeforeTest": false,
// Output format preset: "collision" (detailed per-test), "progress" (dot-progress), or "pretty" (per-test without icons)
"phpunit.output.preset": "collision",
// Override individual format fields from the preset (see phpunit package docs)
"phpunit.output.format": {},
// Clear debug output channel before each run (default: true)
"phpunit.clearDebugOutputOnRun": true,
// Control when Test Results panel opens automatically (VS Code built-in setting):
// - "openOnTestStart" — open as soon as a run starts
// - "openOnTestFailure" — open only when a test fails (useful for seeing dd() output)
// - "neverOpen" — never open automatically
// "testing.openTesting": "openOnTestFailure"
// launch.json configuration name for debugging
"phpunit.debuggerConfig": "",
// Xdebug port, 0 = random (default: 0)
"phpunit.xdebugPort": 0
}
Configuration Examples
Local
For most local projects, zero configuration is needed. To use a different test runner:
// Pest
{ "phpunit.phpunit": "vendor/bin/pest" }
// Laravel Artisan
{ "phpunit.phpunit": "artisan test" }
// ParaTest (parallel execution)
{ "phpunit.phpunit": "vendor/bin/paratest" }
Docker
When running tests inside a Docker container, you need two things:
phpunit.command — tells the extension how to execute commands in the container
phpunit.paths — maps your local file paths to container paths so the extension can locate test files and parse error output
Important: ${workspaceFolder} may not resolve correctly on macOS or WSL. If you encounter path issues, replace it with the actual absolute path (e.g. /home/user/myproject).
docker exec (existing container):
{
"phpunit.command": "docker exec -t my_container /bin/sh -c \"${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.paths": {
"${workspaceFolder}": "/app"
}
}
docker run (ephemeral container):
{
"phpunit.command": "docker run --rm -t -v ${PWD}:/app -w /app php:latest ${php} ${phpargs} ${phpunit} ${phpunitargs}",
"phpunit.paths": {
"${workspaceFolder}": "/app"
}
}
Docker Compose:
{
"phpunit.command": "docker compose exec -t app /bin/sh -c \"${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.paths": {
"${workspaceFolder}": "/app"
}
}
If your docker-compose.yml is not in the workspace root, use the -f flag:
{
"phpunit.command": "docker compose -f /path/to/docker-compose.yml exec -t app /bin/sh -c \"${php} ${phpargs} ${phpunit} ${phpunitargs}\""
}
Docker Multi-Workspace
When using a multi-root workspace with a single shared Docker container, use ${workspaceFolderBasename} to switch directories per folder:
// .code-workspace settings
{
"folders": [
{ "path": "./project-a" },
{ "path": "./project-b" }
],
"settings": {
"phpunit.command": "docker exec -t vscode-phpunit /bin/sh -c \"cd /${workspaceFolderBasename} && ${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.paths": {
"${workspaceFolder}": "/${workspaceFolderBasename}"
}
}
}
Each project folder is mounted separately in the container under its basename:
# docker-compose.yml
services:
vscode-phpunit:
# ...
volumes:
- ./project-a:/project-a
- ./project-b:/project-b
The command cd /${workspaceFolderBasename} switches into the correct directory before running tests. Each folder should have its own phpunit.xml — the extension auto-detects it. Do not set --configuration with an absolute container path at workspace level (e.g. --configuration=/project-a/phpunit.xml), as this causes every folder to load the same config and discover duplicate tests.
Laravel Sail
{
"phpunit.command": "docker compose exec -u sail laravel.test ${php} ${phpargs} ${phpunit} ${phpunitargs}",
"phpunit.phpunit": "artisan test",
"phpunit.paths": {
"${workspaceFolder}": "/var/www/html"
}
}
SSH
{
"phpunit.command": "ssh user@host \"cd /app; ${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.paths": {
"${workspaceFolder}": "/app"
}
}
DDEV
{
"phpunit.command": "ddev exec ${php} ${phpargs} ${phpunit} ${phpunitargs}"
}
WSL + Docker
When using Docker from a WSL workspace, use the full WSL path as the local key:
{
"phpunit.command": "docker exec -t my_container /bin/sh -c \"${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.paths": {
"//wsl.localhost/Ubuntu/var/www/myproject": "/var/www/myproject"
}
}
Debugging with Xdebug
Add a launch configuration to .vscode/launch.json:
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/app": "${workspaceFolder}"
}
}
Set the configuration name in settings:
{
"phpunit.debuggerConfig": "Listen for Xdebug"
}
Click the Debug Test button in the Test Explorer.
Using xdebug.start_with_request=trigger in Docker:
{
"phpunit.command": "docker compose exec -e XDEBUG_TRIGGER=VSCODE app bash -c \"${php} ${phpargs} ${phpunit} ${phpunitargs}\"",
"phpunit.debuggerConfig": "Listen for Xdebug"
}
Breakpoints not hit? Check that:
- Xdebug is configured with
xdebug.mode=debug and xdebug.start_with_request=yes (or trigger)
phpunit.debuggerConfig matches the exact name in launch.json
pathMappings in launch.json correctly maps container paths to local paths
- The Xdebug port is not blocked by a firewall
Commands
| Command |
Description |
Keybinding |
phpunit.reload |
Reload tests |
— |
phpunit.run-all |
Run all tests |
Cmd+T Cmd+S |
phpunit.run-file |
Run tests in current file |
Cmd+T Cmd+F |
phpunit.run-test-at-cursor |
Run test at cursor |
Cmd+T Cmd+T |
phpunit.run-by-group |
Run tests by group |
— |
phpunit.rerun |
Repeat last test run |
Cmd+T Cmd+L |
Troubleshooting
${workspaceFolder} resolves to /
On some systems (macOS, WSL), ${workspaceFolder} may not resolve correctly. Replace it with the actual absolute path in phpunit.paths:
{
"phpunit.paths": {
"/home/user/myproject": "/app"
}
}
spawn ${php} ENOENT
Usually caused by another extension (e.g. DEVSENSE PHP Tools) injecting ${php} as a literal variable. Fix:
{
"phpunit.command": ""
}
If that doesn't help, disable conflicting PHP extensions, then re-enable PHPUnit Test Explorer first.
Paths with spaces cause errors
Ensure your phpunit.command template quotes the variables (this is the default):
{
"phpunit.command": "\"${php}\" ${phpargs} \"${phpunit}\" ${phpunitargs}"
}
Test Results panel does not open automatically after a failure
By default VS Code does not automatically reveal the Test Results panel. To open it whenever a test fails (useful for inspecting dd() output or error messages):
{
"testing.openTesting": "openOnTestFailure"
}
Other values: "openOnTestStart" (open as soon as a run starts), "neverOpen" (never open automatically).
Note: dd() output appears in the Test Results panel as raw output, since it causes the PHP process to exit before producing any structured test results.
Duplicate tests in multi-root workspace
If the same tests appear under multiple workspace folders, check your phpunit.args setting. A --configuration flag pointing to an absolute container path (e.g. --configuration=/var/www/project-a/phpunit.xml) at workspace level causes every folder to load the same phpunit.xml.
Fix: Remove --configuration from phpunit.args and let each folder auto-detect its own phpunit.xml. See the Docker Multi-Workspace section for the correct setup.
Contributing
Found a bug? Have an idea? We welcome contributions!
License
MIT