Skip to content

Commit b1f2f18

Browse files
committed
docs: improvement
1 parent c621821 commit b1f2f18

1 file changed

Lines changed: 122 additions & 41 deletions

File tree

‎phpstan.md‎

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ description: Pest's PHPStan plugin teaches PHPStan about Pest, providing accurat
1111

1212
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.
1313

14-
To get started, require the plugin via Composer along with PHPStan:
14+
To get started, require the plugin via Composer:
1515

1616
```bash
1717
composer require pestphp/pest-plugin-phpstan --dev
18-
composer require phpstan/phpstan --dev
1918
```
2019

21-
If you use [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), the plugin is registered automatically. Otherwise, include the extension in your `phpstan.neon` configuration file:
20+
As the plugin depends on PHPStan itself, this command will also install PHPStan in your project. If you use [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), the plugin is registered automatically. Otherwise, include the extension in your `phpstan.neon` configuration file:
2221

2322
```neon
2423
includes:
@@ -31,76 +30,120 @@ Then, analyze your `tests` directory as usual:
3130
vendor/bin/phpstan analyse tests
3231
```
3332

33+
There is no configuration to maintain. The plugin reads your `Pest.php` files directly — both the `uses(TestCase::class)->in(...)` and `pest()->extend(...)->use(...)->in(...)` styles — and resolves the right test case for each file automatically. The targets given to `in()` behave exactly as they do in Pest: relative or absolute paths, glob patterns, and single files are all supported.
34+
3435
---
3536

36-
## Configuration
37+
## Type Inference
3738

38-
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:
39+
Once installed, the plugin makes PHPStan aware of Pest's dynamic API. As a result, analyzing your test suite becomes as accurate as analyzing your application code.
3940

40-
```neon
41-
parameters:
42-
peststan:
43-
testCaseClass: Tests\TestCase
41+
### The `$this` Instance
42+
43+
Inside test and hook closures, `$this` resolves to the test case bound to that file through your Pest configuration. For example, given the following `Pest.php` file:
44+
45+
```php
46+
pest()->extend(Tests\TestCase::class)
47+
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
48+
->in('Feature');
4449
```
4550

46-
By default, `testCaseClass` is set to `PHPUnit\Framework\TestCase`.
51+
Any test within the `Feature` directory will see `$this` typed as `Tests\TestCase`, along with all of its methods and properties:
4752

48-
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:
53+
```php
54+
it('has a home page', function () {
55+
$this->get('/')->assertOk(); // $this is Tests\TestCase
56+
});
57+
```
4958

50-
```neon
51-
parameters:
52-
peststan:
53-
testCaseClass: Tests\TestCase
54-
pestConfigFiles:
55-
- tests/Pest.php
56-
- tests/Feature/Pest.php
59+
Of course, a per-file `uses()` call takes precedence over the directory binding, exactly as it does at runtime.
60+
61+
### Custom Properties
62+
63+
Properties you assign in a `beforeEach()` hook are recognized and typed on `$this` in your tests — whether the hook lives in the test file itself or is attached to your Pest configuration:
64+
65+
```php
66+
beforeEach(function () {
67+
$this->user = User::factory()->create();
68+
});
69+
70+
it('can update the profile', function () {
71+
$this->user; // App\Models\User
72+
});
5773
```
5874

59-
---
75+
If a property is assigned in multiple hooks, its type will be the union of every assigned value. You may also guide the inference with a standard `@var` annotation on the assignment.
6076

61-
## Type Inference
77+
### Expectation Chains
78+
79+
The `expect()` function returns a generic `Expectation`, so every matcher knows the exact value it is asserting against. In addition, type-checking matchers narrow the value as the chain progresses:
6280

63-
Once installed, the plugin makes PHPStan aware of Pest's dynamic API. As a result, analyzing your test suite becomes as accurate as analyzing your application code. Among other things, the plugin understands:
81+
```php
82+
/** @var int|string $value */
83+
expect($value) // Expectation<int|string>
84+
->toBeInt(); // Expectation<int>
85+
```
86+
87+
The value's type continues to flow through the entire expectation API, including `->and(...)` and `->not`, and matchers like `toBeInstanceOf()` narrow the value to the given class.
88+
89+
### Higher Order Expectations
6490

65-
- The type flowing through an `expect()` chain, so matchers such as `toHaveLength()` or `toHaveKey()` know the value they are asserting against.
66-
- The `$this` instance inside test and hook closures, resolving to your configured `testCaseClass` along with its methods and properties.
67-
- Higher order expectations, such as `expect($user)->name->toBe('Nuno')`.
68-
- Properties and methods shared across your tests through `pest()->extend()` and `uses()`.
91+
[Higher order expectations](/docs/higher-order-testing) are fully typed as well. When you access a property or call a method on an expectation, the plugin resolves its type from the underlying value — and after each assertion, the chain returns to the original value, exactly as it does at runtime:
92+
93+
```php
94+
expect($user)
95+
->name->toBe('Nuno')
96+
->email->toContain('@');
97+
```
6998

70-
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.
99+
Similarly, public methods chained directly onto `it()` or `test()` — higher order tests — resolve against your bound test case, so a call like `it('has users')->actingAsAdmin()` is understood.
71100

72101
---
73102

74103
## Rules
75104

76-
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.
105+
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, so you may ignore any rule precisely via PHPStan's standard `ignoreErrors` configuration:
106+
107+
```neon
108+
parameters:
109+
ignoreErrors:
110+
- identifier: pest.expectation.redundant
111+
```
77112

78113
### Impossible Expectations
79114

80-
Reports expectations that can never pass because the value's type is incompatible with the matcher:
115+
> Identifier: `pest.expectation.impossible`
116+
117+
Reports type assertions that can never pass because the value's type is incompatible with the matcher:
81118

82119
```php
83-
expect(10)->toStartWith('1'); // int can never satisfy toStartWith()
120+
expect(10)->toBeString(); // an int can never be a string
84121
```
85122

86123
### Redundant Expectations
87124

88-
Reports expectations that are always true because the value's type already guarantees the assertion:
125+
> Identifier: `pest.expectation.redundant`
126+
127+
Reports type assertions that are always true because the value's type already guarantees them:
89128

90129
```php
91-
expect('pest')->toBeString(); // $value is already known to be a string
130+
expect('pest')->toBeString(); // the value is already known to be a string
92131
```
93132

94133
### Matcher Value Types
95134

96-
Reports matchers called on a value that does not meet their requirements, such as a string, iterable, or countable value:
135+
> Identifiers: `pest.expectation.requiresString`, `pest.expectation.requiresIterable`, `pest.expectation.requiresCountableOrIterable`
136+
137+
Reports matchers called on a value that does not meet their requirements. For instance, string matchers such as `toStartWith()` require a string, while `toHaveCount()` requires a countable or iterable value:
97138

98139
```php
99-
expect(42)->toHaveLength(2); // toHaveLength() requires a string
140+
expect(10)->toStartWith('1'); // toStartWith() requires a string
100141
```
101142

102143
### Static Test Closures
103144

145+
> Identifier: `pest.test.staticClosure`
146+
104147
Reports test and hook closures declared as `static`, which prevents Pest from binding the test case instance to `$this`:
105148

106149
```php
@@ -111,44 +154,82 @@ it('does something', static function () { // remove the "static" keyword
111154

112155
### `$this` In `beforeAll()` And `afterAll()`
113156

114-
Reports usage of `$this` inside `beforeAll()` and `afterAll()`, which run in a static context where the test case instance is not available.
157+
> Identifiers: `pest.lifecycle.beforeAllThisUsage`, `pest.lifecycle.afterAllThisUsage`
158+
159+
Reports usage of `$this` inside `beforeAll()` and `afterAll()`, which run in a static context where the test case instance is not available, suggesting `beforeEach()` or `afterEach()` instead.
115160

116161
### Disallowed Calls In `describe()`
117162

118-
Reports hooks that cannot be used directly inside a `describe()` block, such as `beforeAll()` and `afterAll()`, suggesting the correct alternative.
163+
> Identifiers: `pest.lifecycle.beforeAllDisallowed`, `pest.lifecycle.afterAllDisallowed`
164+
165+
Reports `beforeAll()` and `afterAll()` calls made inside a `describe()` block, where they are not supported, suggesting the correct alternative.
119166

120167
### Describe Without Tests
121168

122-
Reports `describe()` blocks that contain no tests.
169+
> Identifier: `pest.describe.withoutTests`
170+
171+
Reports `describe()` blocks that contain no tests. Tests created dynamically — for example, inside a loop or a condition — are counted as you would expect.
123172

124173
### Duplicate Test Descriptions
125174

126-
Reports two tests sharing the same description within a single file.
175+
> Identifier: `pest.test.duplicateDescription`
176+
177+
Reports two tests sharing the same description within a single file. Descriptions given to `it()` are prefixed with "it", exactly as Pest does at runtime, so `it('works')` and `test('it works')` are considered duplicates.
127178

128179
### Empty Test Closures
129180

181+
> Identifier: `pest.test.emptyClosure`
182+
130183
Reports tests with an empty closure body, suggesting you add assertions or chain `->todo()` to mark the test as pending.
131184

132185
### Invalid Repeat Value
133186

187+
> Identifier: `pest.execution.invalidRepeatValue`
188+
134189
Reports `repeat()` calls with a value that is not greater than `0`.
135190

136191
### Invalid Group Names
137192

193+
> Identifier: `pest.group.invalidName`
194+
138195
Reports `group()` calls that are missing a non-empty string argument.
139196

140197
### Redundant Local Uses
141198

142-
Reports `uses()` calls in a test file for a trait or test case that is already applied globally through your Pest configuration.
199+
> Identifier: `pest.config.redundantLocalUse`
200+
201+
Reports `uses()` and `pest()->use()` calls in a test file for a trait or test case that is already applied to that file globally through your Pest configuration:
202+
203+
```php
204+
// tests/Pest.php
205+
pest()->extend(TestCase::class)->use(RefreshDatabase::class)->in('Feature');
206+
207+
// tests/Feature/ExampleTest.php
208+
uses(RefreshDatabase::class); // already applied through tests/Pest.php
209+
```
143210

144211
### Invalid `throws()` Exceptions
145212

146-
Reports `throws()` calls that reference a class that does not exist or is not a `Throwable`.
213+
> Identifiers: `pest.throws.classNotFound`, `pest.throws.invalidException`
214+
215+
Reports `throws()` calls that reference a class that does not exist or is not a `Throwable`. Of course, passing a plain string as the expected exception message is perfectly valid, and the plugin will not flag it:
216+
217+
```php
218+
it('rejects invalid input', function () {
219+
// ...
220+
})->throws(ValidationException::class);
221+
222+
it('fails gracefully', function () {
223+
// ...
224+
})->throws('Something went wrong'); // treated as the expected message
225+
```
147226

148227
### Missing `covers()` References
149228

150-
Reports `covers()` calls that reference a class or function that does not exist.
229+
> Identifiers: `pest.covers.classNotFound`, `pest.covers.functionNotFound`
230+
231+
Reports `coversClass()`, `coversTrait()`, and `coversFunction()` calls that reference a class, trait, or function that does not exist.
151232

152233
---
153234

154-
Now that you know how to add static analysis to your test suite, let's explore the wider ecosystem of plugins that can enhance your Pest experience: [Plugins](/docs/plugins)
235+
In this chapter, we have discussed how Pest's PHPStan plugin brings accurate static analysis to your test suite. In the following chapter, we explore the wider ecosystem of plugins that can enhance your Pest experience: [Plugins →](/docs/plugins)

0 commit comments

Comments
 (0)