nette/tester
Nette Tester is a lightweight, enjoyable PHP testing framework with expressive assertions, mocks, code coverage, and parallel test execution. It requires no annotations or complex setup, making it a fast, practical choice for testing PHP projects.
Installation:
composer require nette/tester --dev
Add to composer.json under require-dev:
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
First Test File:
Create tests/MyTest.php:
<?php
use Tester\Assert;
use Tester\TestCase;
class MyTest extends TestCase
{
public function testBasicAssertion()
{
Assert::true(1 + 1 === 2);
}
}
(new MyTest())->run();
Run Tests:
vendor/bin/tester tests/MyTest.php
public function testLaravelSpecific()
{
$response = new \Illuminate\Http\Response('Hello');
Assert::same('Hello', $response->getContent());
Assert::same(200, $response->getStatusCode());
Assert::exception(function () {
throw new \RuntimeException('Expected error');
}, \RuntimeException::class);
}
Note (v2.6.2):
TestCase::run()now runs every test method even after one fails and displays all failures together—results first, then details. This gives you a complete picture in a single run.
Laravel Integration Pattern:
class UserTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->app = require __DIR__.'/../bootstrap/app.php';
$this->app->make(\Illuminate\Contracts\Http\Kernel::class)->bootstrap();
}
public function testUserCreation()
{
$user = \App\Models\User::factory()->create();
Assert::true($user->exists);
Assert::notNull($user->email);
}
}
Use the @dataProvider annotation:
/**
* @dataProvider providerUserRoles
*/
public function testUserRoles($role, $description)
{
$user = \App\Models\User::factory()->create(['role' => $role]);
Assert::same($role, $user->role);
Assert::same($description, $user->description);
}
public function providerUserRoles()
{
return [
['admin', 'Administrator'],
['user', 'Regular User'],
];
}
Since Tester lacks Laravel’s $this->get(), manually create and handle a request:
public function testGetUser()
{
$request = \Illuminate\Http\Request::create('/api/user/1', 'GET');
$response = $this->app->make(\Illuminate\Contracts\Http\Kernel::class)->handle($request);
Assert::same(200, $response->getStatusCode());
Assert::same(1, json_decode($response->getContent(), true)['id']);
}
Manually wrap tests in a transaction:
protected function setUp(): void
{
parent::setUp();
$this->app = require __DIR__.'/../bootstrap/app.php';
$this->app->make(\Illuminate\Contracts\Http\Kernel::class)->bootstrap();
$this->app->make('db')->beginTransaction();
}
protected function tearDown(): void
{
$this->app->make('db')->rollBack();
parent::tearDown();
}
vendor/bin/tester tests/ --parallel
New in v2.6.2: Stack traces now include call arguments (e.g.,
redirect('https://…', 302)), making it immediately clear which call failed.
System php.ini Usage:
php.ini by default. Use -C to override:
vendor/bin/tester tests/ -C /path/to/custom/php.ini
Test Lifecycle:
tearDown() runs even when tests fail (since v2.6.0). Avoid side effects in assertions.Assertion Order:
Assert::equal() with $matchOrder = true can cause false positives if array order matters.Parallel Execution:
@phpVersion Boundary Behaviour (v2.6.2):
@phpVersion 8.4.3 runs on PHP 8.4.3, and < 8.0 no longer runs on 8.0. Common forms like @phpVersion 8.1 behave as before.Assert::exception() / Assert::error() Message Check (v2.6.2):
'' or '0'. To skip the message check, pass null as the third argument. This can break tests that relied on empty‑string messages being ignored.JUnit Report failures Attribute (v2.6.2):
failures attribute instead of errors. Update your CI scripts if they read only errors.--stop-on-fail with Parallel Jobs (v2.6.2):
Environment::lock() on Windows (v2.6.2):
HttpAssert with follow: true (v2.6.2):
Code Coverage Exit Code (v2.6.2):
tester now exits with code 1 when the requested report cannot be generated. The summary line no longer mixes into TAP or JUnit output on stdout.Robustness Fixes (v2.6.2):
Assert::match() masks (UTF-8, %f%), FileMock (append, rb+, touch()), DomQuery selectors on PHP < 8.4, Dumper with references, and JUnit report with invalid UTF-8. @multiple with an invalid value and Helpers::purge() on a path resolving to the root directory are now reported as errors.Verbose Output:
vendor/bin/tester tests/ -v
Console Lines Mode:
vendor/bin/tester tests/ --console-lines
Dumper Integration:
\Tester\Dumper::dump($this->app->make(\App\Models\User::class));
Error Output Capture:
Assert::exception(function () {
// Code that might fail
}, \RuntimeException::class, 'Expected error message');
Custom Assertions: Create helper functions:
function assertDatabaseHas(string $table, array $data): void
{
Assert::true(
app('db')->table($table)->where($data)->exists()
);
}
Test Templates:
Extend TestCase for shared setup:
abstract class LaravelTestCase extends \Tester\TestCase
{
protected $app;
protected function setUp(): void
{
$this->app = require __DIR__.'/../../bootstrap/app.php';
$this->app->make(\Illuminate\Contracts\Http\Kernel::class)->bootstrap();
}
}
Code Coverage:
vendor/bin/tester tests/ --coverage
Custom Output Formats:
vendor/bin/tester tests/ --format=junit
Service Container:
app() helper or $this->app->make() instead of direct instantiation.Artisan Commands:
$exitCode = \Illuminate\Support\Facades\Artisan::call('migrate:fresh');
Assert::same(0, $exitCode);
Event Testing:
\Illuminate\Support\Facades\Event::fake();
// ... trigger event ...
\Illuminate\Support\Facades\Event::assertDispatched('user.created');
Queue Testing:
\Illuminate\Support\Facades\Queue::fake();
// ... dispatch job ...
\Illuminate\Support\Facades\Queue::assertPushed(\App\Jobs\SendEmail::class);
PHP 8.6 support was added in v2.6.2, so you can run the suite on the latest PHP version.
How can I help you explore Laravel packages today?