draw/tester
Tools for testing PHP apps: DataTester wraps PHPUnit assertions with a fluent, path-based API for arrays/objects. Includes PHPUnit extensions like CarbonReset to reset Carbon state between tests and SetUpAutowire to autowire test properties via attributes.
Installation:
composer require draw/tester
Add to composer.json if not auto-loaded:
"autoload": {
"psr-4": {
"Draw\\Component\\Tester\\": "vendor/draw/tester/src"
}
}
Run composer dump-autoload.
First Test Case:
Create a test class extending PHPUnit\Framework\TestCase and use DataTester for fluent assertions:
use Draw\Component\Tester\DataTester;
class UserTest extends TestCase {
public function testUserData() {
$user = ['name' => 'John', 'age' => 30];
DataTester::from($user)
->assertIsArray()
->assertCount(2)
->path('name')->assertSame('John')
->path('age')->assertGreaterThan(25);
}
}
PHPUnit Extensions:
Enable extensions in phpunit.xml:
<extensions>
<bootstrap class="Draw\Component\Tester\PHPUnit\Extension\CarbonReset\CarbonResetExtension"/>
<bootstrap class="Draw\Component\Tester\PHPUnit\Extension\SetUpAutowire\SetUpAutowireExtension"/>
</extensions>
Fluent Assertions: Chain assertions for nested data:
DataTester::from($response)
->assertIsArray()
->assertCount(1)
->path('[0].id')->assertIsInt()
->path('[0].name')->assertMatches('/^[A-Z]/');
Dynamic Paths: Use variables for dynamic paths:
$key = 'user';
DataTester::from($data)
->path($key)->assertExists()
->path($key . '.email')->assertIsString();
Custom Assertions:
Extend DataTester for domain-specific rules:
class UserDataTester extends DataTester {
public function assertValidEmail() {
$this->assertMatches('/^[^\s@]+@[^\s@]+\.[^\s@]+$/');
return $this;
}
}
Carbon Reset: Reset Carbon between tests to avoid timestamp pollution:
// No manual setup needed; extension handles it globally.
Autowiring Services:
#[AutowireMock] for test doubles:
#[AutowireMock]
private UserRepository $userRepo;
#[AutowireMockProperty('userRepo')]
private UserService $userService;
AutowireRandomInt).Lifecycle Hooks:
Use AutowiredCompletionAwareInterface for post-autowire setup:
public function postAutowire() {
$this->userRepo->method('find')->willReturn($mockUser);
}
Symfony Kernel Tests:
Combine with draw/tester-bundle for service autowiring:
#[AutowireService('app.user_repository')]
private UserRepository $userRepo;
Legacy Code:
Use DataTester to validate legacy data structures without modifying assertions.
CI/CD:
Add to phpunit.xml for consistent test environments:
<extensions>
<bootstrap class="Draw\Component\Tester\PHPUnit\Extension\CarbonReset\CarbonResetExtension"/>
</extensions>
Extension Registration:
phpunit.xml renders them useless.<extensions> block exists and paths are correct.Autowire Order:
getPriority() run first. Use this to resolve dependencies.Static Properties:
CarbonReset Scope:
DataTester State:
// ❌ Avoid (state may leak)
$tester->assertIsArray()->assertCount(2)->assertIsArray(); // Fails
// ✅ Correct
DataTester::from($data)->assertIsArray();
DataTester::from($data)->assertCount(2);
Autowire Failures:
-vvv for extension debug logs.Path Assertions:
assertPathExists() to validate paths before assertions:$tester->path('[user.profile]')->assertPathExists()->assertIsArray();
Custom Attributes:
#[AutowireRandomInt(1, 10)]
private int $value;
public function testAutowire() {
$this->assertGreaterThan(0, $this->value);
}
Custom Assertions:
Extend DataTester for domain logic:
class ApiResponseTester extends DataTester {
public function assertSuccess() {
$this->assertPathExists('data')->assertPathExists('meta.status')->assertSame('success');
return $this;
}
}
Autowire Providers: Create a service provider for complex autowiring (e.g., database fixtures):
class FixtureAutowire implements AutowireInterface {
public static function getPriority(): int { return 100; }
public function autowire(TestCase $testCase, ReflectionProperty $property) {
$property->setValue($testCase, FixtureFactory::create());
}
}
CarbonReset Customization: Override the reset logic in a subclass:
class CustomCarbonResetExtension extends CarbonResetExtension {
protected function resetCarbon() {
Carbon::setTestNow(Carbon::now()->addHour());
}
}
Combine with Laravel:
Use DataTester in DatabaseMigrationsTestCase to validate seed data:
DataTester::from($users)->assertCount(3)->path('[0].email')->assertMatches('/@example\.com/');
Performance:
Cache DataTester instances for large datasets:
private static $tester;
public function setUp(): void {
self::$tester = DataTester::from($this->data);
}
Documentation: Add PHPDoc to custom attributes for IDE support:
#[AutowireRandomInt(1, 10)]
/** @var int Random value between 1 and 10 */
private int $randomValue;
How can I help you explore Laravel packages today?