phpstan/phpstan-phpunit
PHPStan extension for PHPUnit: improves type inference for mocks (intersection types for createMock/getMock), understands Foo|MockObject phpDocs, adds early-terminating methods to avoid undefined vars, and refines assert() types. Optional strict rules catch improper assertion usage.
Installation:
composer require --dev phpstan/phpstan-phpunit
Use phpstan/extension-installer to auto-include the extension in your phpstan.neon config.
Basic Configuration (if not using extension-installer):
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon # For framework-specific rules
First Use Case: Run PHPStan on your test suite:
vendor/bin/phpstan analyse tests
The extension will now:
Foo&\PHPUnit\Framework\MockObject\MockObject).assert* method usage (e.g., assertTrue(true) will flag as redundant).Pattern: Use intersection types for mocks to enable static analysis of both mock methods (expects(), method()) and mocked class methods.
// Correct: Intersection type (PHP 8+)
private function createMock(): User&\PHPUnit\Framework\MockObject\MockObject {
return $this->createMock(User::class);
}
// Fallback (PHP < 8 or PHPDoc)
/**
* @return User&\PHPUnit\Framework\MockObject\MockObject
*/
private function createMock(): User {
return $this->createMock(User::class);
}
Workflow:
willReturn(), expects()).private User $user).Pattern: Leverage PHPStan’s level 4 to catch redundant or incorrect assertions.
// Flags as redundant (use `assertTrue()` instead)
$this->assertSame(true, true);
// Flags as incorrect (use `assertNull()` instead)
$this->assertSame(null, null);
Integration Tips:
phpstan.neon:
level: 4
rules:
PHPUnit\Framework\TestCase:
assertSameBooleanExpectedRule: true
assertSameNullExpectedRule: true
assertEquals()/assertNotEquals() sparingly (prefer assertSame() for identical types).Pattern: Annotate data provider methods with @phpstan-data-provider to avoid false positives.
/**
* @phpstan-data-provider provideTestData
*/
public function testWithData(array $input, string $expected): void {
$this->assertEquals($expected, $this->service->process($input));
}
/**
* @return array<int, array{0: array<int, mixed>, 1: string}>
*/
public static function provideTestData(): array {
return [[[1, 2], "1,2"], [[3], "3"]];
}
Workflow:
array<int, array{...}>).Iterator, Generator, or IteratorAggregate types for dynamic providers.Pattern: Ignore dynamic calls to assert* methods when needed (e.g., in factories or dynamic test builders).
// In phpstan.neon
services:
DynamicCallToAssertionIgnoreExtension:
ignoredMethods:
- 'buildAssertion'
Mock Configuration Timing:
expects()).Class&\PHPUnit\Framework\MockObject\MockObject for active mocks.Data Provider Return Types:
array<int, mixed> vs. array<int, array{...}>).@return array<int, array{...}> or use @phpstan-data-provider.Strict Rules Overhead:
assertSameBooleanExpectedRule) may flag legitimate use cases.phpstan.neon:
rules:
PHPUnit\Framework\TestCase:
assertSameBooleanExpectedRule: false
PHPUnit Version Mismatches:
False Positives:
@var or @phpstan-ignore-next-line to suppress specific errors:
/** @var User&\PHPUnit\Framework\MockObject\MockObject */
$mock = $this->createMock(User::class);
Performance:
analysePaths:
- tests/Unit/
- !tests/Integration/SlowTests.php
Auto-Fixes:
vendor/bin/phpstan analyse --generate-baseline tests --level 4
vendor/bin/phpstan analyse tests --fix
Custom Assertions:
DynamicCallToAssertionIgnoreExtension to ignore custom assertion methods:
services:
DynamicCallToAssertionIgnoreExtension:
ignoredMethods:
- 'customAssert*'
Mock Builders:
createMockForIntersectionOfInterfaces (PHPUnit 9.5+):
$mock = $this->createMockForIntersectionOfInterfaces([User::class, Logger::class]);
PHPDoc Overrides:
@method to add mock methods to PHPDoc:
/**
* @method void expects()
* @method $this method(string $name, callable $mock)
*/
private function createMock(): User&\PHPUnit\Framework\MockObject\MockObject { ... }
How can I help you explore Laravel packages today?