ichinya / laratesto
Laravel bridge for the Testo testing framework.
Requires
- php: ^8.3
- laravel/framework: ^13.0
- testo/testo: ^0.10.42
README
Native Testo plugin that boots Laravel around every test — without PHPUnit.
The bridge is a standalone Composer package. It does not require any changes to Testo or to your application: it registers a TestRunInterceptor through the standard plugin API (the same mechanism testo/bridge-mockery uses).
The
testovendor namespace on Packagist belongs to the framework author, so this package is published asichinya/laratesto. If the bridge is ever adopted upstream, it can move totesto/bridge-laravelfollowing the ecosystem convention.
Features
- Boots the framework the same way
artisandoes:bootstrap/app.php+ Console kernel bootstrap. - A fresh application per test with guaranteed cleanup of Laravel static state afterwards.
.env.testingsupport via the standardAPP_ENVmechanism.- Service container, facades, global
app()helper, Artisan. - HTTP requests through the HTTP kernel (
get,post,postJson, …) with a Testo-nativeLaravelResponsewrapper (assertOk,assertStatus,assertJson, … — no PHPUnit). #[RefreshDatabase]and#[DatabaseTransactions]attributes.
Requirements
- PHP 8.3+
- Laravel 13
- Testo
^0.10.42
Installation
composer require --dev ichinya/laratesto
Register the plugin for a suite in testo.php:
<?php declare(strict_types=1); use Testo\Application\Config\ApplicationConfig; use Testo\Application\Config\SuiteConfig; use Laratesto\Config\LaravelConfig; use Laratesto\LaravelPlugin; use Testo\Convention\NamingConventionPlugin; return new ApplicationConfig( suites: [ new SuiteConfig( name: 'Unit', location: ['tests/Unit'], ), new SuiteConfig( name: 'Laravel', location: ['tests/Feature'], plugins: [ new NamingConventionPlugin(), new LaravelPlugin( new LaravelConfig( basePath: __DIR__, ), ), ], ), ], );
NamingConventionPlugin is optional; with it, *Test.php classes and test*() methods are discovered automatically and no #[Test] attributes are needed.
Run with:
vendor/bin/testo run --suite=Laravel
Writing tests
Base class
use Laratesto\Testing\LaravelTestCase; final class UserControllerTest extends LaravelTestCase { public function testIndexReturnsUsers(): void { $this->get('/users') ->assertOk() ->assertJson(['users' => []]); } }
Trait (no inheritance)
use Laratesto\Testing\InteractsWithLaravel; final class UserServiceTest { use InteractsWithLaravel; public function testCreatesUser(): void { $service = $this->make(UserService::class); $service->create(...); // ... } }
Function style
After the framework is booted, global helpers work in plain test functions too:
use Testo\Assert; function testResolvesUserService(): void { Assert::instanceOf(app(UserService::class), UserService::class); }
Configuration
LaravelConfig options:
| Option | Default | Description |
|---|---|---|
basePath |
— | Laravel project root; bootstrap/app.php is loaded from it. |
environment |
testing |
Forced into APP_ENV before boot, so .env.testing is loaded when present. |
config |
[] |
Config overrides applied after boot (config()->set($key, $value)). |
Database attributes
use Laratesto\Attribute\DatabaseTransactions; use Laratesto\Attribute\RefreshDatabase; final class BillingTest { use InteractsWithLaravel; #[RefreshDatabase] // migrate:fresh before the test (seed: true to run seeders) public function testInvoicesAreCreated(): void { /* ... */ } #[DatabaseTransactions] // wrap the test in a transaction, roll back afterwards public function testPaymentIsProcessed(): void { /* ... */ } }
Both attributes can be combined; migrations run first, then the wrapping transaction. The interceptors resolve the application booted by the plugin, so they require the LaravelPlugin in the suite.
HTTP helpers
Available via LaravelTestCase or the InteractsWithLaravel trait:
| Method | Description |
|---|---|
$this->app() |
The application booted for current test. |
$this->make($abstract) |
Resolve a service. |
$this->get($uri, $headers) |
GET request. |
$this->post($uri, $parameters, $headers) |
POST with form parameters. |
$this->postJson($uri, $payload, $headers) |
POST with a JSON body. |
$this->sendRequest($method, $uri, ...) |
Arbitrary method. |
$this->artisan($command, $parameters) |
Run an Artisan command, returns exit code. |
LaravelResponse methods: status(), header(), headers(), body(), json(), assertOk(), assertStatus(), assertHeader(), assertJson(), assertSee(), response().
How it works
LaravelTestInterceptorruns before attribute interceptors for every test: it forcesAPP_ENV, requiresbootstrap/app.php, bootstraps the Console kernel and applies config overrides.- The booted application is injected into test cases that use
LaravelTestCaseor theInteractsWithLaraveltrait. - After the test — in a
finallyblock — database connections are closed, the container is flushed, facades are cleared and the static-state resets from the framework's own test teardown (tearDownTheTestEnvironment) are applied. - A new application instance is created for every test, which is what makes the cleanup sufficient: container-level state dies with the old application.
Limitations
- This is not a runner for existing PHPUnit/Pest Laravel tests. Laravel's
TestCaseextends PHPUnit, and Laravel assertions (Queue::assertPushed, etc.) call PHPUnit under the hood. Tests must be written against Testo assertions (Testo\Assert) — hence the PHPUnit-freeLaravelResponsewrapper. - Laravel keeps a lot of state in process-global statics, so a Laravel suite must run tests sequentially. Do not enable fiber-based concurrency for it.
PluginConfigurator::configure()currently receives Testo's internal container (Internal\Container\Container). The supportedtesto/testorange is pinned incomposer.jsonand tested in CI against the released versions.
Development
composer update
composer test
The package's own test suite runs under Testo: tests/Unit covers the response
wrapper, tests/Integration boots the fixture application in tests/Fixture/laravel
(SQLite in-memory) and exercises HTTP requests, the database attributes and
cross-test isolation.
License
BSD 3-Clause. See LICENSE.