testo/inline
Inline test plugin for Testo: mark methods as tests via PHP attributes, without separate test classes. Ideal for quick checks near production code and self-documenting examples. Install with composer require --dev testo/inline.
composer require --dev testo/inline
composer require --dev testo/testo
testo.php config file in your project root (mirror Laravel’s phpunit.xml structure where possible):
return [
'bootstrap' => [
__DIR__.'/tests/bootstrap.php',
],
'suites' => [
'default' => [
'paths' => [__DIR__.'/tests'],
'plugins' => ['testo/inline'],
],
],
];
// app/Services/UserService.php
use Testo\Inline\Test;
class UserService {
#[Test]
public function find_by_id_returns_user() {
$user = $this->findById(1);
$this->assertInstanceOf(User::class, $user);
}
public function findById(int $id) { /* ... */ }
}
vendor/bin/testo
Use inline tests for self-contained assertions near production code, such as:
Example:
// app/Helpers/StringHelper.php
#[Test]
public function truncate_removes_excess_chars() {
$result = StringHelper::truncate("Hello World", 5);
$this->assertEquals("Hello...", $result);
}
Colocate Tests:
Place inline tests in the same file as the production code they test (e.g., User.php).
// app/Models/User.php
#[Test]
public function email_is_valid() {
$user = new User(['email' => 'test@example.com']);
$this->assertTrue(Str::isValidEmail($user->email));
}
Group Tests with TestSuite:
Register inline test classes in a TestSuite for organized execution:
// tests/TestSuite/InlineSuite.php
use Testo\TestSuite;
return new TestSuite([
User::class,
UserService::class,
]);
Leverage Testo’s Assertions: Use Testo’s assertion methods (similar to PHPUnit but lighter):
#[Test]
public function calculate_total_returns_float() {
$total = $this->calculateTotal([10, 20, 30]);
$this->assertFloat($total, 60.0);
}
Hybrid Testing: Combine inline tests with Laravel’s PHPUnit tests:
# Run all tests
composer test
// composer.json
{
"scripts": {
"test": "phpunit && vendor/bin/testo",
"test:unit": "vendor/bin/testo",
"test:feature": "phpunit"
}
}
Avoid Stateful Tests: Inline tests share the same scope as production code. Avoid:
app() bindings).Mocking Dependencies:
Manually inject mocks or use Testo’s Mock plugin (if available):
#[Test]
public function create_user_calls_repository() {
$mockRepo = $this->mock(Repository::class);
$mockRepo->shouldReceive('create')->once();
$this->createUser(['name' => 'John']);
}
Test Discovery:
Ensure your testo.php config includes the correct paths:
'suites' => [
'default' => [
'paths' => [__DIR__.'/app', __DIR__.'/tests'], // Include app/ for inline tests
],
],
CI/CD Pipeline: Add Testo to your CI workflow (e.g., GitHub Actions):
- name: Run Testo
run: vendor/bin/testo
Test Discovery Issues:
TestSuite.testo.php:
'suites' => [
'default' => [
'classes' => [User::class, UserService::class], // Manual registration
],
],
Shared State:
#[Test]
public function test_with_fresh_instance() {
$service = new UserService(); // New instance per test
$this->assertEmpty($service->getCache());
}
Laravel-Specific Gaps:
refreshDatabase(), actingAs()).Attribute Conflicts:
Performance Overhead:
TestSuite.-v for detailed output:
vendor/bin/testo -v
#[Before] and #[After] hooks to reset state:
#[Before]
public function reset_state() {
Cache::clear();
}
try-catch for granular debugging:
#[Test]
public function debug_failing_test() {
try {
$this->expectException(InvalidArgumentException::class);
$this->doSomethingRisky();
} catch (Exception $e) {
$this->assertStringContainsString('Expected error', $e->getMessage());
}
}
Custom Assertions: Extend Testo’s assertion system by creating a custom plugin:
// app/Plugins/CustomAssertions.php
use Testo\Plugin;
class CustomAssertions extends Plugin {
public function assertJsonEquals($expected, $actual) {
$this->assertEquals(json_encode($expected, JSON_PRETTY_PRINT), json_encode($actual, JSON_PRETTY_PRINT));
}
}
Register in testo.php:
'plugins' => ['app/Plugins/CustomAssertions'],
Test Lifecycle Hooks:
Use #[Before] and #[After] for setup/teardown:
#[Before]
public function setup_database() {
DB::connection()->transaction(function () {
// Setup test data
});
}
Parallel Testing:
If Testo supports parallelization, configure it in testo.php:
'parallel' => [
'workers' => 4,
],
#[Test]
public function test_with_manual_injection() {
$repo = new UserRepository();
$service = new UserService($repo);
// Test logic
}
artisan test command. Use custom scripts:
# package.json
"scripts": {
"test:inline": "php vendor/bin/testo"
}
#[Test]
public function test_with_transaction() {
DB::beginTransaction();
try {
// Test logic
DB::commit();
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
How can I help you explore Laravel packages today?