Start with Greenlight
Greenlight is an attribute-based test framework for PHP 8.4 and later. It runs tests in parallel by default.
This guide starts with an empty project and creates a successful test run.
Requirements and installation
Greenlight requires PHP 8.4 or later. It does not require a PHP extension.
The parallel runner uses core stream sockets and proc_open. Coverage requires
ext-pcov or Xdebug in coverage mode.
If PHP disables a process or stream function that the parallel runner requires, Greenlight uses an in-process sequential run.
An in-process run cannot give #[Isolated] tests a dedicated process.
Install Greenlight as a development dependency:
composer require --dev greenlight/greenlight
Configure the Composer autoloader
Add these keys to the project composer.json file:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}
Create the source and test directories. Then update the Composer autoloader:
mkdir -p src tests
composer dump-autoload
Create the configuration file
Greenlight reads greenlight.php from the project root. This file returns a
typed builder.
Create this minimum configuration:
<?php
declare(strict_types=1);
use Greenlight\Config\GreenlightConfig;
return GreenlightConfig::create()
->paths(['tests'])
->workers(count: 'auto');
paths() specifies the directories that Greenlight scans for tests.
workers('auto') calculates the worker count from the CPU count.
These values are defaults. Thus, this configuration has the same result:
return GreenlightConfig::create();
The longer form can make a new project easier to understand. See the configuration reference for the complete builder API.
Create the first test
Tests are PHP classes. Add #[Test] to each test method.
Greenlight scans the configured paths for files whose names end in Test.php.
The class short name must match the file name. Composer must be able to
autoload the class from its namespace.
For example, tests/GreeterTest.php must declare the class
App\Tests\GreeterTest with the mappings in this guide.
Greenlight does not require a TestCase base class or a test method name
pattern. Start each expectation with Expect::value().
Constructor injection supplies stateful test services when a test requests them.
Create this small class:
<?php
declare(strict_types=1);
namespace App;
final class Greeter
{
public function greet(string $name): string
{
$name = \trim($name);
if ($name === '') {
throw new \InvalidArgumentException('Name cannot be empty.');
}
return \sprintf('Hello, %s!', $name);
}
}
Save the file as src/Greeter.php.
Create a test for both results of the public method:
<?php
declare(strict_types=1);
namespace App\Tests;
use App\Greeter;
use Greenlight\Attribute\Test;
use Greenlight\Expect\Expect;
final class GreeterTest
{
#[Test]
public function greetsByName(): void
{
$greeter = new Greeter();
Expect::value($greeter->greet('Ada'))->toBe('Hello, Ada!');
}
#[Test]
public function rejectsEmptyNames(): void
{
$greeter = new Greeter();
Expect::calling(
static fn (): string => $greeter->greet(''),
)->toThrow(\InvalidArgumentException::class, matching: '/empty/');
}
}
Save the file as tests/GreeterTest.php.
Expect::value() starts a matcher chain for a value. A failed matcher throws
immediately and includes a clear difference when applicable.
The expectations reference describes each matcher, negation, polls, exception checks, and explicit failures.
Run tests
Run the suite:
vendor/bin/greenlight run
run is the default command. This command has the same result:
vendor/bin/greenlight
Use these commands for common tasks:
vendor/bin/greenlight list-testsprints each discovered test ID.vendor/bin/greenlight run --dry-runprints the resolved run-settings summary.vendor/bin/greenlight run --workers=1uses one in-process worker.vendor/bin/greenlight run --group=slowselects tests with#[Group('slow')].vendor/bin/greenlight run --exclude-group=slowexcludes that group.vendor/bin/greenlight run --list-testsprints the selected tests.vendor/bin/greenlight run --bailstops new work after the first failed or errored test.
With --bail, active assignments can finish after the limit. The final failure
count can therefore exceed one.
The --exclude-class, --exclude-method, and --exclude-path flags also
exclude tests. Exclusion rules take priority over inclusion rules.
The --list-groups and --list-suites flags print the discovered groups and
configured suites.
Repeat one plan to find an intermittent failure:
vendor/bin/greenlight run --filter=CheckoutTest --repeat=20
vendor/bin/greenlight run --filter=CheckoutTest --repeat-until-failure
Each iteration reports its number. The summary identifies each failed iteration.
The command returns a nonzero exit code if an iteration fails.
--repeat-until-failure stops after the first failed iteration.
Without --repeat=N, this mode stops after 100 iterations. Add --repeat=N
to specify a different limit.
Repeat modes do not support JUnit output or enabled coverage. Run a separate command for each required report.
Read the output
Greenlight uses the tty reporter on an interactive terminal. This reporter
shows live progress with ANSI color. At the end of the run, it prints problem
details and differences before the summary.
Greenlight uses the plain reporter when standard output is not a TTY. This
reporter prints the run start, one line for each completed test, problem
details, and the final summary. It does not print escape codes.
Select a reporter with --reporter:
vendor/bin/greenlight run --reporter=plain
vendor/bin/greenlight run --reporter=junit
Repeat the flag to select more than one reporter:
vendor/bin/greenlight run --reporter=tty --reporter=junit
Add a file to keep a reporter separate from standard output:
vendor/bin/greenlight run --reporter=tty --reporter=junit=reports/junit.xml
Greenlight creates missing parent directories. It replaces an existing file.
Plugins can register custom reporter factories. See ReporterProvider.
Tests can retain diagnostic data without output to standard output. Inject
Greenlight\Artifact\Attachments.
Call value(), text(), bytes(), or file() to add an attachment.
Greenlight prints retained paths and stores files below
build/greenlight-artifacts by default.
See test attachments for more information.
Use watch mode
Start watch mode:
vendor/bin/greenlight run --watch
Watch mode runs all selected tests at startup and after each file change. It watches effective test paths and coverage include paths.
Classes that failed in the previous watch iteration run first.
Press Enter to rerun all selected tests. Press q to stop watch mode with exit
code 0, regardless of the last iteration result.
Watch mode does not print coverage totals or write coverage exports.
Watch mode combines rapid save events. The default delay is 200 ms.
Use the watch() configuration builder to change this delay.
The builder can also watch non-PHP inputs:
->watch(fn ($watch) => $watch
->paths('templates', 'config')
->include('**/*.twig', '**/*.yaml')
->exclude('build/**', 'coverage/**'))
Relative paths and patterns use the command working directory. Exclusions have precedence and can prevent runs that generated artifacts would cause.
Configure workers
Tests run in parallel worker processes by default.
--workers=auto uses one worker for each detected CPU core. This value is the
default. If CPU detection fails, Greenlight uses four workers.
--workers=4 specifies four workers. --workers=1 uses one in-process runner.
The last mode is usually the simplest choice for debug work.
Do not use --workers=1 when a test depends on #[Isolated] for process-global
state cleanup.
A worker remains active until it has no more tests or the worker fails. This behavior makes memory growth and state leaks visible in the suite.
Parallel suites need separate resources for workers or a limit for one shared dependency. Greenlight supports both methods.
Use a channel when each worker has a separate external resource. The channel number is from 1 through the worker count.
Each worker receives its channel through Greenlight\Test\TestChannel and
the GREENLIGHT_CHANNEL environment variable.
A plugin can also provision real infrastructure in the orchestrator and expose
typed connection data through Greenlight\IntegrationFixture\IntegrationResources.
That keeps setup and teardown active when a worker crashes.
See Writing plugins.
final class OrderRepositoryTest
{
public function __construct(
private readonly TestChannel $channel,
) {}
#[Test]
public function persistsAnOrder(): void
{
$pdo = new \PDO('mysql:host=127.0.0.1;dbname=app_test_' . $this->channel->number, 'app', 'secret');
// ...
}
}
Concurrent tests within one run never share a channel. Thus, databases such as
app_test_1 and app_test_2 do not conflict within that run.
Separate runs and CI shards reuse channel numbers. Add a separate resource prefix for each concurrent run that uses the same database server.
Use #[RequiresResource] when several workers use one dependency with limited
capacity:
#[RequiresResource('payments-sandbox')]
final class PaymentGatewayTest { ... }
return GreenlightConfig::create()
->workers(8)
->resourceLimit('payments-sandbox', 2);
Other workers can run tests that do not require payments-sandbox. Without a
configured limit, only one assignment can use the resource at a time.
A resource limit controls capacity. It does not select a sandbox, database, or account for a test.
Use a channel when each worker has one resource instance. Use an application-owned allocator for fewer resource instances.
A Greenlight resource limit can restrict the tests that enter that allocator.
A test can use both methods. Its channel can select a database while
#[RequiresResource] controls access to a shared service.
Resource limits apply to one Greenlight run. Other worktrees and CI shards have separate counts.
Use external coordination when different processes share the dependency. See the configuration reference for all resource limit rules.
Use built-in sandboxes
The harness supplies per-test sandboxes from Greenlight\Sandbox. Constructor
injection supplies each sandbox.
Greenlight disposes each sandbox after its test. Thus, temporary state does not leak to other tests or workers.
TemporaryDirectorycreates a unique temporary directory on first use. It removes the directory after the test.EnvironmentVariableschanges environment variables ingetenv,$_ENV, and$_SERVER. It restores the original values after the test.Autoloadersregisters PHP autoloaders. It unregisters them in reverse registration order after the test.StreamWrappersregisters PHP stream wrappers. It unregisters them in reverse registration order after the test.
use Greenlight\Sandbox\EnvironmentVariables;
use Greenlight\Sandbox\TemporaryDirectory;
final class ExporterTest
{
public function __construct(
private readonly TemporaryDirectory $tmp,
private readonly EnvironmentVariables $env,
) {}
#[Test]
public function writesTheExportFile(): void
{
$this->env->set('EXPORT_DIR', $this->tmp->path());
new Exporter()->run();
Expect::value(\file_exists($this->tmp->path() . '/export.csv'))->toBeTrue();
}
}
Each test receives separate sandbox instances. Each TemporaryDirectory
instance has a unique directory.
Defer test cleanup
Ask for Greenlight\Test\Cleanup through constructor injection. Call
defer() immediately after the test acquires a resource.
use Greenlight\Attribute\Test;
use Greenlight\Test\Cleanup;
final readonly class ServerTest
{
public function __construct(private Cleanup $cleanup) {}
#[Test]
public function acceptsAConnection(): void
{
$server = TestServer::start();
$this->cleanup->defer(static fn() => $server->stop());
// Test the server.
}
}
Greenlight runs cleanup callbacks once in reverse registration order. It runs
them after After hooks and before per-test sandbox disposal.
A callback failure does not prevent the remaining callbacks. For a passed or
skipped test, the first cleanup failure determines the new outcome. An
ExpectationFailed changes the outcome to failed. Another throwable changes
it to errored. An earlier test failure or error remains primary.
Exit codes
Greenlight uses these exit codes:
0means that the run succeeded.1means that the run failed or found no tests.64means that the command has a usage error.128 + signal numbermeans that a signal interrupted the run.
For example, SIGINT returns 130 and SIGTERM returns 143. See
interruption for the graceful shutdown rules.
Exit code 1 includes test failures, test errors, invalid configuration,
discovery errors, coverage gate failures, coverage export errors, and detected
leaks.
Greenlight treats a run without tests as a configuration problem.