PHPStan
Source code: github.com/pestphp/pest-plugin-phpstan
PHPStan is a static analysis tool that finds bugs in your code without running it. By default, however, PHPStan does not understand Pest's functional API — functions like it(), test(), expect(), and the $this available inside your test closures.
Pest's PHPStan plugin teaches PHPStan about Pest. It provides accurate type inference for your tests and expectations, and adds a set of Pest-specific rules that catch common mistakes before you run your suite.
To get started, require the plugin via Composer along with PHPStan:
1composer require pestphp/pest-plugin-phpstan --dev2composer require phpstan/phpstan --dev
If you use phpstan/extension-installer, the plugin is registered automatically. Otherwise, include the extension in your phpstan.neon configuration file:
1includes:2 - vendor/pestphp/pest-plugin-phpstan/extension.neon
Then, analyse your tests directory as usual:
1vendor/bin/phpstan analyse tests
Configuration
To get the most accurate results, tell the plugin about the base test case your tests extend. This allows PHPStan to correctly resolve $this, along with any custom methods and properties, inside your test closures:
1parameters:2 peststan:3 testCaseClass: Tests\TestCase
By default, testCaseClass is set to PHPUnit\Framework\TestCase.
If your Pest configuration lives outside the default tests/Pest.php location, or you have multiple configuration files, you may point the plugin at them so it can resolve globally applied traits and test cases:
1parameters:2 peststan:3 testCaseClass: Tests\TestCase4 pestConfigFiles:5 - tests/Pest.php6 - tests/Feature/Pest.php
Type Inference
Once installed, the plugin makes PHPStan aware of Pest's dynamic API. As a result, analysing your test suite becomes as accurate as analysing your application code. Among other things, the plugin understands:
- The type flowing through an
expect()chain, so matchers such astoHaveLength()ortoHaveKey()know the value they are asserting against. - The
$thisinstance inside test and hook closures, resolving to your configuredtestCaseClassalong with its methods and properties. - Higher order expectations, such as
expect($user)->name->toBe('Nuno'). - Properties and methods shared across your tests through
pest()->extend()anduses().
This means PHPStan can flag genuine mistakes in your tests, like calling a string matcher on an integer, that would otherwise only surface at runtime.
Rules
In addition to type inference, the plugin registers a number of rules that detect mistakes specific to Pest. Each reported error carries a stable identifier (for example, pest.test.staticClosure) so you may ignore it precisely when needed.
Impossible Expectations
Reports expectations that can never pass because the value's type is incompatible with the matcher.
1expect(10)->toStartWith('1'); // int can never satisfy toStartWith()
Redundant Expectations
Reports expectations that are always true because the value's type already guarantees the assertion.
1expect('pest')->toBeString(); // $value is already known to be a string
Matcher Value Types
Reports matchers called on a value that does not meet their requirements, such as a string, iterable, or countable value.
1expect(42)->toHaveLength(2); // toHaveLength() requires a string
Static Test Closures
Reports test and hook closures declared as static, which prevents Pest from binding the test case instance to $this.
1it('does something', static function () { // remove the "static" keyword2 // ...3});
$this In beforeAll() And afterAll()
Reports usage of $this inside beforeAll() and afterAll(), which run in a static context where the test case instance is not available.
Disallowed Calls In describe()
Reports hooks that cannot be used directly inside a describe() block, such as beforeAll() and afterAll(), suggesting the correct alternative.
Describe Without Tests
Reports describe() blocks that contain no tests.
Duplicate Test Descriptions
Reports two tests sharing the same description within a single file.
Empty Test Closures
Reports tests with an empty closure body, suggesting you add assertions or chain ->todo() to mark the test as pending.
Invalid Repeat Value
Reports repeat() calls with a value that is not greater than 0.
Invalid Group Names
Reports group() calls that are missing a non-empty string argument.
Redundant Local Uses
Reports uses() calls in a test file for a trait or test case that is already applied globally through your Pest configuration.
Invalid throws() Exceptions
Reports throws() calls that reference a class that does not exist or is not a Throwable.
Missing covers() References
Reports covers() calls that reference a class or function that does not exist.
Now that you know how to add static analysis to your test suite, you may also be interested in type coverage to ensure your application code is fully typed.