testo/test
Testo plugin that adds the #[Test] attribute and discovery locator. Automatically finds attribute-marked test classes and methods for canonical attribute-driven test detection. Install with composer require --dev testo/test.
Install the package in a Laravel project:
composer require --dev testo/test
Configure Testo (if not already set up):
testo/testo to your composer.json dev dependencies.testo.php config file in your project root (or extend Testo’s defaults).return [
'locators' => [
Testo\Locator\AttributeLocator::class, // Enables #[Test] attribute discovery
],
];
Write your first attribute-driven test:
use Testo\Test;
#[Test]
class ExampleTest {
#[Test]
public function it_passes(): void {
assertThat(true)->isTrue();
}
#[Test]
private function private_method_can_be_tested(): void {
assertThat('test')->isNotEmpty();
}
}
Run tests via:
./vendor/bin/testo
Or integrate with Laravel’s Artisan (requires custom command setup).
Replace a PHPUnit test class:
// Before (PHPUnit)
class UserTest extends TestCase {
public function test_user_can_login() { ... }
}
// After (Testo + #[Test])
#[Test]
class UserTest {
#[Test]
public function user_can_login(): void { ... }
}
AttributeLocator scans for classes/methods marked with #[Test].testo.php:
'locators' => [
Testo\Locator\AttributeLocator::class,
App\CustomLocator::class, // Additional locators
],
// app/Providers/TestoServiceProvider.php
public function register(): void {
$this->app->bind(TestoTestCase::class, function () {
return new TestoTestCase();
});
$this->commands([
new TestoCommand(), // Custom Artisan command
]);
}
Testo\TestCase for Laravel-specific setup:
use Testo\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TestCase extends BaseTestCase {
use RefreshDatabase;
protected function setUp(): void {
parent::setUp();
$this->artisan('migrate'); // Laravel-specific setup
}
}
// Testo tests
namespace Tests\Testo;
#[Test]
class TestoTest { ... }
// PHPUnit tests
namespace Tests\PHPUnit;
class PHPUnitTest extends \Tests\TestCase { ... }
php artisan testo:run
#[Test]
class PrivateMethodTest {
#[Test]
private function internal_logic_should_work(): void {
assertThat($this->internalMethod())->isTrue();
}
private function internalMethod(): bool { return true; }
}
assertThat($value)->isEqualTo($expected)).DatabaseMigrations via Testo’s beforeEach/afterEach:
#[Test]
class UserTest {
protected function beforeEach(): void {
$this->migrate(); // Custom method to run migrations
}
}
MockFacade:
use Testo\Mock;
#[Test]
class MockTest {
#[Test]
public function it_mocks_a_service(): void {
$mock = Mock::mock(Logger::class);
$mock->shouldReceive('log')->once();
}
}
Attribute Conflicts:
#[Test] or Pest’s #[Test], conflicts may arise.use Testo\Test;
#[Test\Test] // Explicit namespace
Laravel-Specific Features Missing:
RefreshDatabase, QueueTesting, or NotificationTesting.trait LaravelRefreshDatabase {
protected function refreshDatabase(): void {
$this->artisan('migrate:fresh');
}
}
Configuration Overrides:
testo.php:
return array_merge(
require __DIR__.'/../../vendor/testo/testo/config/testo.php',
[
'paths' => [
'tests' => base_path('tests/Testo'), // Custom test path
],
]
);
Private Method Limitations:
@method) or document tests clearly.Performance Overhead:
'locators' => [
Testo\Locator\FileLocator::class, // Faster for large suites
Testo\Locator\AttributeLocator::class,
],
Test Not Discovered:
#[Test] attribute is correctly imported (use Testo\Test).testo.php for locator configuration../vendor/bin/testo --verbose
Attribute Errors:
composer dump-autoload
Laravel Integration Issues:
TestCase extends the correct base class.Custom Attributes:
Testo\Attribute\Test:
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class CustomTest extends Test {
public function __construct(public string $priority = 'medium') {}
}
Locator Extensions:
Testo\Locator\LocatorInterface for custom discovery logic:
class NamespaceLocator implements LocatorInterface {
public function locate(TestRunner $runner): void {
$runner->addTestClass(MyTestClass::class);
}
}
Testo Events:
TestStarting, TestFailed) via listeners:
$runner->on(TestStarting::class, function (TestStarting $event) {
// Custom logic before test runs
});
Laravel Artisan Integration:
class TestoCommand extends Command {
protected $signature = 'testo {--group= : Filter test group}';
protected $description = 'Run Testo tests';
public function handle(): int {
$exitCode = (new TestRunner())->run();
return $exitCode;
}
}
#[Test] attributes.#[Attribute(Attribute::TARGET_CLASS)]
class TestGroup {
public function __construct(public string $group) {}
}
#[TestGroup('unit')]
#[Test]
class UnitTest { ... }
#[Test]
#[PHPUnit\Test] // Dual annotation for gradual migration
class HybridTest { ... }
composer dump-autoload --optimize
./vendor/bin/testo
How can I help you explore Laravel packages today?