You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b1f2f18
Browse filesBrowse the repository at this point in the historyBrowse files
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.
13
13
14
-
To get started, require the plugin via Composer along with PHPStan:
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:
22
21
23
22
```neon
24
23
includes:
@@ -31,76 +30,120 @@ Then, analyze your `tests` directory as usual:
31
30
vendor/bin/phpstan analyse tests
32
31
```
33
32
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
+
34
35
---
35
36
36
-
## Configuration
37
+
## Type Inference
37
38
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.
39
40
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:
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:
47
52
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
+
```
49
58
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
+
});
57
73
```
58
74
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.
60
76
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:
62
80
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
64
90
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
+
```
69
98
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.
71
100
72
101
---
73
102
74
103
## Rules
75
104
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
+
```
77
112
78
113
### Impossible Expectations
79
114
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:
81
118
82
119
```php
83
-
expect(10)->toStartWith('1'); // int can never satisfy toStartWith()
120
+
expect(10)->toBeString(); // an int can never be a string
84
121
```
85
122
86
123
### Redundant Expectations
87
124
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:
89
128
90
129
```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
92
131
```
93
132
94
133
### Matcher Value Types
95
134
96
-
Reports matchers called on a value that does not meet their requirements, such as a string, iterable, or countable value:
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:
97
138
98
139
```php
99
-
expect(42)->toHaveLength(2); // toHaveLength() requires a string
140
+
expect(10)->toStartWith('1'); // toStartWith() requires a string
100
141
```
101
142
102
143
### Static Test Closures
103
144
145
+
> Identifier: `pest.test.staticClosure`
146
+
104
147
Reports test and hook closures declared as `static`, which prevents Pest from binding the test case instance to `$this`:
105
148
106
149
```php
@@ -111,44 +154,82 @@ it('does something', static function () { // remove the "static" keyword
111
154
112
155
### `$this` In `beforeAll()` And `afterAll()`
113
156
114
-
Reports usage of `$this` inside `beforeAll()` and `afterAll()`, which run in a static context where the test case instance is not available.
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.
115
160
116
161
### Disallowed Calls In `describe()`
117
162
118
-
Reports hooks that cannot be used directly inside a `describe()` block, such as `beforeAll()` and `afterAll()`, suggesting the correct alternative.
Reports `beforeAll()` and `afterAll()` calls made inside a `describe()` block, where they are not supported, suggesting the correct alternative.
119
166
120
167
### Describe Without Tests
121
168
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.
123
172
124
173
### Duplicate Test Descriptions
125
174
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.
127
178
128
179
### Empty Test Closures
129
180
181
+
> Identifier: `pest.test.emptyClosure`
182
+
130
183
Reports tests with an empty closure body, suggesting you add assertions or chain `->todo()` to mark the test as pending.
131
184
132
185
### Invalid Repeat Value
133
186
187
+
> Identifier: `pest.execution.invalidRepeatValue`
188
+
134
189
Reports `repeat()` calls with a value that is not greater than `0`.
135
190
136
191
### Invalid Group Names
137
192
193
+
> Identifier: `pest.group.invalidName`
194
+
138
195
Reports `group()` calls that are missing a non-empty string argument.
139
196
140
197
### Redundant Local Uses
141
198
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:
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
+
```
147
226
148
227
### Missing `covers()` References
149
228
150
-
Reports `covers()` calls that reference a class or function that does not exist.
Reports `coversClass()`, `coversTrait()`, and `coversFunction()` calls that reference a class, trait, or function that does not exist.
151
232
152
233
---
153
234
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