psalm/plugin-phpunit
Psalm plugin that teaches Psalm about PHPUnit tests. Adds better type inference and assertions for PHPUnit APIs so your test suite is analyzed more accurately. Requires Psalm v4+. Install via Composer and enable with psalm-plugin.
composer require --dev psalm/plugin-phpunit
vendor/bin/psalm-plugin enable psalm/plugin-phpunit
psalm.xml to include your test directory:
<file-list>
<dir>tests/</dir>
</file-list>
./vendor/bin/psalm --init
Run Psalm on your test files to immediately see improvements:
./vendor/bin/psalm --no-cache
Example Output: Psalm will now understand PHPUnit assertions (e.g., assertSame(), assertInstanceOf()) and validate their types, reducing false positives in your tests.
Type Validation for Assertions
Psalm will analyze assertion methods (e.g., assertEquals(), assertInstanceOf()) and validate their arguments. For example:
public function testUserIdIsInteger(): void
{
$user = User::factory()->create();
$this->assertSame(1, $user->id); // Psalm checks if `$user->id` is an int
}
assertSame()/assertEquals() with typed arguments to leverage Psalm’s type inference.Data Provider Analysis
Psalm validates @dataProvider methods and their return types:
#[DataProvider('integerProvider')]
public function testIntegerHandling(int $value): void
{
$this->assertIsInt($value);
}
public function integerProvider(): iterable
{
yield [1];
yield [2];
}
iterable or array types. Psalm will flag mismatches (e.g., returning a scalar).Attribute Support
Psalm understands PHPUnit 9+ attributes (#[Test], #[DataProvider], #[Before]):
#[Test]
public function testAttributeSupport(): void
{
$this->assertTrue(true);
}
@test) to attributes for full Psalm support.Mocking and Stubs
Psalm validates mock interactions (e.g., createMock(), getMockBuilder()):
$mock = $this->createMock(UserRepository::class);
$mock->method('find')->willReturn(new User());
createMock() with generic types (e.g., createMock(UserRepository::class)) for better type hints.Laravel-Specific Use Cases:
Collection vs. Builder):
$users = User::query()->where('active', true)->get();
$this->assertInstanceOf(Collection::class, $users);
$response = $this->get('/api/users');
$this->assertJsonStructure([['id', 'name']]);
$this->app->bind(UserRepository::class, fn () => new UserRepository());
$this->assertTrue($this->app->has(UserRepository::class));
CI/CD Pipeline:
Add Psalm to your test suite in phpunit.xml:
<listeners>
<listener class="Psalm\Plugin\PHPUnit\Listener" />
</listeners>
Run Psalm before PHPUnit to fail fast on type errors:
./vendor/bin/psalm --no-cache && ./vendor/bin/phpunit
Configuration:
Customize Psalm’s behavior in psalm.xml:
<plugin_class>Psalm\Plugin\PHPUnit\Plugin</plugin_class>
<param name="checkForThrowsDocblock">true</param> <!-- Validate `@throws` annotations -->
False Positives with Dynamic Types:
Psalm may flag assertions involving dynamic types (e.g., mixed or array) as errors. Use @psalm-suppress sparingly:
#[psalm-suppress MixedArgument]
$this->assertEquals($dynamicValue, 'expected');
Data Provider Mismatches: If a data provider returns a tuple but the test expects separate arguments, Psalm will report an error. Ensure consistency:
// Correct: Data provider returns iterable of arrays
public function provider(): iterable
{
yield [1, 'one'];
yield [2, 'two'];
}
Legacy PHPUnit Annotations:
The plugin prioritizes attributes (#[Test]) over annotations (@test). Update old tests to avoid warnings.
Mock Method Signatures:
Psalm may not infer mock method return types if they’re not explicitly set. Use willReturn() with typed values:
$mock->method('getName')->willReturn('John'); // Psalm infers `string`
Performance Overhead:
Analyzing large test suites may slow down Psalm. Use --no-cache in CI and cache results locally:
./vendor/bin/psalm --init && ./vendor/bin/psalm --no-cache
-v to debug plugin behavior:
./vendor/bin/psalm -v
./vendor/bin/psalm tests/Feature/UserTest.php
composer require psalm/psalm:^7.0 --dev
Custom Assertions:
Extend the plugin to support custom assertions by defining stubs in psalm.xml:
<stub_files>
<file>stubs/CustomAssertions.php</file>
</stub_files>
Example stub:
namespace PHPUnit\Framework\Assert {
function assertCustomType(mixed $actual, string $expectedType): void { /* ... */ }
}
Data Provider Enhancements:
Add support for complex data providers (e.g., generators) by extending the plugin’s ProviderAnalyzer class.
Laravel-Specific Stubs:
Create stubs for Laravel’s testing helpers (e.g., createMock(), JsonResponse) to improve type inference:
namespace Illuminate\Testing {
class JsonResponse {
public function assertJsonStructure(array $structure): void { /* ... */ }
}
}
<exclude-files> in psalm.xml to skip problematic test files:
<exclude-files>
<file>tests/Integration/SlowTests.php</file>
</exclude-files>
strict_types=1 in test files to catch more type errors:
<?php declare(strict_types=1);
psalm/plugin-phpunit is loaded after core Psalm plugins in psalm.xml:
<plugins>
<plugin_class>Psalm\Plugin\PHPUnit\Plugin</plugin_class>
</plugins>
How can I help you explore Laravel packages today?