|
1 | 1 | # PHPStan rule testing helper
|
2 | 2 |
|
3 |
| -This is a helper library for slight improvement to DX for testing PHPStan rules. |
4 |
| -It allows you to write the expected error message in the fixture file. |
5 |
| -Anything after `// ERROR ` is considered the expected error message. |
6 |
| -The test classes are simplified as you now specify just the fixture files, and this library will extract the expected error and calculate the correct line number. |
| 3 | +This library offers a couple of improvements to PHPStan's [custom rule test harness](https://phpstan.org/developing-extensions/testing#custom-rules). |
7 | 4 |
|
8 |
| -You can also use an `ErrorMessageFormatter` to further decouple tests from the actual error message. |
9 |
| -See [ErrorMessageFormatter](#error-formatter) section. |
| 5 | +This library provides [AbstractRuleTestCase](src/AbstractRuleTestCase.php), which extends PHPStan's `RuleTestCase`. |
10 | 6 |
|
11 |
| -## Example |
| 7 | +It offers a simpler way to write tests for custom rules. Specifically: |
| 8 | + |
| 9 | +1. No need to specify line numbers in the test code. |
| 10 | +2. You can specify the expected error message once. |
| 11 | + |
| 12 | +## Improvement 1: No more line numbers in tests |
| 13 | + |
| 14 | +The minimal test case specifies the Rule being tested and at least one test. |
| 15 | +Each test must call the `assertIssuesReported` method, which takes the path of one or more fixture files. |
12 | 16 |
|
13 |
| -Test code extends [AbstractRuleTestCase](src/AbstractRuleTestCase.php). |
14 |
| -As with the PHPStan's `RuleTestCase` use the `getRule` method to setup the rule used by the test. |
15 |
| -For each test list the fixture file(s) needed by the test, using the `assertIssuesReported` method. |
16 | 17 |
|
17 | 18 | #### Test code:
|
18 | 19 | ```php
|
19 |
| -use DaveLiddament\PhpstanRuleTestingHelper\AbstractRuleTestCase; |
| 20 | +use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase; |
20 | 21 |
|
21 | 22 | class CallableFromRuleTest extends AbstractRuleTestCase
|
22 | 23 | {
|
@@ -46,154 +47,173 @@ class SomeCode
|
46 | 47 | }
|
47 | 48 | ```
|
48 | 49 |
|
49 |
| -Every line that contains `// ERROR ` is considered an issue that should be picked up by the rule. |
| 50 | +Every line that contains `// ERROR ` is considered an issue that should be picked up by the rule. |
50 | 51 | The text after `// ERROR` is the expected error message.
|
51 | 52 |
|
52 |
| -With this approach you don't need to work out the line number of the error. |
53 |
| -There are further benefits by using the [ErrorMessageFormatter](#error-formatter) to decouple the error messages from the test code. |
54 |
| - |
55 |
| - |
56 |
| - |
57 |
| -NOTE: You can pass in multiple fixture files. E.g. |
58 |
| -```php |
59 |
| -$this->assertIssuesReported( |
60 |
| - __DIR__ . '/Fixtures/SomeCode.php', |
61 |
| - __DIR__ . '/Fixtures/SomeCode2.php', |
62 |
| - // And so on... |
63 |
| - ); |
64 |
| -``` |
65 |
| - |
66 |
| - |
67 |
| -## Installation |
68 |
| - |
69 |
| -```shell |
70 |
| -composer require --dev dave-liddament/phpstan-rule-test-helper |
71 |
| -``` |
72 |
| - |
73 |
| -## Error Formatter |
74 |
| - |
75 |
| -The chances are when you developing PHPStan rules the error message for violations will change. |
76 |
| -Making any change will require you to update all the related tests. |
77 |
| - |
78 |
| -### Constant string error messages |
| 53 | +With this approach you don't need to work out the line number of the error. |
| 54 | +This is particularly handy when you update the Fixture file, you no longer have to update all the line numbers in the test. |
79 | 55 |
|
80 |
| -In the simplest case the error is a message that does provide any context, other than line number. |
81 |
| -E.g. in the example the error is `Can not call method`. No additional information (e.g. who was trying to call the method) is provided. |
82 | 56 |
|
83 |
| -Create a class that extends `ConstantErrorFormatter` and pass the error message to the constructor. |
| 57 | +# Improvement 2: Specify the expected error message once |
84 | 58 |
|
85 |
| -Update the test to tell it to use a `ConstantStringErrorMessageFormatter`. |
| 59 | +Often you end up writing the same error message for every violation. To get round this use the `getErrorFromatter` method to specify the error message. |
86 | 60 |
|
| 61 | +#### Test code: |
87 | 62 | ```php
|
88 |
| -class CallableFromRuleTest extends AbstractRuleTestCase |
| 63 | +use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase; |
| 64 | + |
| 65 | +class CallableFromRuleTest extends AbstractRuleTestCase |
89 | 66 | {
|
90 |
| - // getRule method omitted for brevity |
91 |
| - // testAllowedCall method omitted for brevity |
| 67 | + protected function getRule(): Rule |
| 68 | + { |
| 69 | + return new CallableFromRule($this->createReflectionProvider()); |
| 70 | + } |
92 | 71 |
|
93 |
| - protected function getErrorFormatter(): ErrorMessageFormatter |
| 72 | + public function testAllowedCall(): void |
| 73 | + { |
| 74 | + $this->assertIssuesReported(__DIR__ . '/Fixtures/SomeCode.php'); |
| 75 | + } |
| 76 | + |
| 77 | + protected function getErrorFormatter(): string |
94 | 78 | {
|
95 |
| - return new ConstantStringErrorMessageFormatter("Can not call method"); |
| 79 | + return "Can not call method"; |
96 | 80 | }
|
97 | 81 | }
|
98 | 82 | ```
|
99 | 83 |
|
100 |
| -Now if the error message is changed, the text only needs to be updated in one place. |
101 |
| - |
102 |
| -Finally, the fixture can be simplified. |
103 |
| -There is no need to specify the error message in the fixture file, we just need to specify where the error is. |
| 84 | +The fixture file is simplified as there is no need to specify the error message. Any lines where an error is expected need to end with `// ERROR`. |
| 85 | +#### Fixture: |
104 | 86 |
|
105 |
| -Updated fixture: |
106 |
| -```php |
| 87 | +```php |
107 | 88 | class SomeCode
|
108 | 89 | {
|
109 | 90 | public function go(): void
|
110 | 91 | {
|
111 | 92 | $item = new Item("hello");
|
112 | 93 | $item->updateName("world"); // ERROR
|
113 | 94 | }
|
| 95 | + |
| 96 | + public function go2(): void |
| 97 | + { |
| 98 | + $item = new Item("hello"); |
| 99 | + $item->remove(); // ERROR |
| 100 | + } |
114 | 101 | }
|
115 | 102 | ```
|
116 | 103 |
|
117 |
| -### Error messages with context |
118 |
| - |
119 |
| -Good error message will provide context. |
120 |
| -For example, the error message could be improved to give the name of the calling class. |
121 |
| -The calling class is `SomeClass` so let's update the error message to `Can not call method from SomeCode`. |
| 104 | +### Adding context to error messages |
122 | 105 |
|
123 |
| -The fixture is updated to include the calling class name after `// ERROR` |
| 106 | +Good error message require context. The context is added to the fixture file after `// ERROR `. Multiple pieces of context can be added by separating them with the `|` character. |
124 | 107 |
|
| 108 | +#### Test code: |
125 | 109 | ```php
|
126 |
| -class SomeCode |
| 110 | +use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase; |
| 111 | + |
| 112 | +class CallableFromRuleTest extends AbstractRuleTestCase |
127 | 113 | {
|
128 |
| - public function go(): void |
| 114 | + protected function getRule(): Rule |
129 | 115 | {
|
130 |
| - $item = new Item("hello"); |
131 |
| - $item->updateName("world"); // ERROR SomeCode |
| 116 | + return new CallableFromRule($this->createReflectionProvider()); |
| 117 | + } |
| 118 | + |
| 119 | + public function testAllowedCall(): void |
| 120 | + { |
| 121 | + $this->assertIssuesReported(__DIR__ . '/Fixtures/SomeCode.php'); |
| 122 | + } |
| 123 | + |
| 124 | + protected function getErrorFormatter(): string |
| 125 | + { |
| 126 | + return "Can not call {0} from within class {1}"; |
132 | 127 | }
|
133 | 128 | }
|
134 | 129 | ```
|
135 | 130 |
|
136 |
| -The `CallableFromRuleErrorFormatter` is updated. |
137 |
| -Firstly it now extends `ErrorMessageFormatter` instead of `ConstantErrorFormatter`. |
138 |
| -An implementation of `getErrorMessage` is added. |
139 |
| -This is passed everything after `\\ ERROR`, with whitespace trimmed from each side, and must return the expected error message. |
| 131 | +The fixture file is simplified as there is no need to specify the error message. Any lines where an error is expected need to end with `// ERROR`. |
| 132 | +#### Fixture: |
140 | 133 |
|
141 |
| -```php |
142 |
| -class CallableFromRuleErrorFormatter extends ErrorMessageFormatter |
| 134 | +```php |
| 135 | +class SomeCode |
143 | 136 | {
|
144 |
| - public function getErrorMessage(string $errorContext): string |
| 137 | + public function go(): void |
| 138 | + { |
| 139 | + $item = new Item("hello"); |
| 140 | + $item->updateName("world"); // ERROR Item::updateName|SomeCode |
| 141 | + } |
| 142 | + |
| 143 | + public function go2(): void |
145 | 144 | {
|
146 |
| - return 'Can not call method from ' . $errorContext; |
| 145 | + $item = new Item("hello"); |
| 146 | + $item->remove(); // ERROR Item::remove|SomeCode |
147 | 147 | }
|
148 | 148 | }
|
149 | 149 | ```
|
150 | 150 |
|
151 |
| -### Error message helper methods |
| 151 | +The expected error messages would be: |
| 152 | + |
| 153 | +- Line 6: `Can not call Item::updateName from within class SomeCode` |
| 154 | +- Line 11: `Can not call Item::remove from within class SomeCode` |
152 | 155 |
|
153 |
| -Sometimes the contextual error messages might have 2 or more pieces of information. |
154 |
| -Continuing the example above, the error message could be improved to give the name of the calling class and the method being called. |
155 |
| -E.g. `Can not call Item::updateName from SomeCode`. |
| 156 | +### More flexible error messages |
156 | 157 |
|
157 |
| -The fixture is updated to include both `Item::updateName` and `SomeCode` seperated by the `|` character. |
| 158 | +If you need more flexibility in the error message, you can return an object that implements the `ErrorMessageFormatter` [interface](src/ErrorMessageFormatter.php). |
158 | 159 |
|
159 |
| -E.g. `// ERROR` |
| 160 | +In the example below the message changes depending on the number of parts in the error context. |
160 | 161 |
|
161 | 162 | ```php
|
162 |
| -class SomeCode |
| 163 | +use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase; |
| 164 | + |
| 165 | +class CallableFromRuleTest extends AbstractRuleTestCase |
163 | 166 | {
|
164 |
| - public function go(): void |
| 167 | +protected function getRule(): Rule |
| 168 | +{ |
| 169 | +return new CallableFromRule($this->createReflectionProvider()); |
| 170 | +} |
| 171 | + |
| 172 | + public function testAllowedCall(): void |
165 | 173 | {
|
166 |
| - $item = new Item("hello"); |
167 |
| - $item->updateName("world"); // ERROR Item::updateName|SomeCode |
| 174 | + $this->assertIssuesReported(__DIR__ . '/Fixtures/SomeCode.php'); |
168 | 175 | }
|
| 176 | + |
| 177 | + protected function getErrorFormatter(): ErrorMessageFormatter |
| 178 | + { |
| 179 | + new class() extends ErrorMessageFormatter { |
| 180 | + public function getErrorMessage(string $errorContext): string |
| 181 | + { |
| 182 | + $parts = $this->getErrorMessageAsParts($errorContext); |
| 183 | + $calledFrom = count($parts) === 2 ? 'class '.$parts[1] : 'outside an object'; |
| 184 | + |
| 185 | + return sprintf('Can not call %s from %s', $parts[0], $calledFrom); |
| 186 | + } |
| 187 | + }; |
| 188 | + } |
169 | 189 | }
|
170 | 190 | ```
|
171 | 191 |
|
172 |
| -Use the `getErrorMessageAsParts` helper method to do this, as shown below: |
173 |
| - |
174 |
| -```php |
| 192 | +#### Fixture: |
175 | 193 |
|
176 |
| -class CallableFromRuleErrorFormatter extends ErrorMessageFormatter |
| 194 | +```php |
| 195 | +class SomeCode |
177 | 196 | {
|
178 |
| - public function getErrorMessage(string $errorContext): string |
| 197 | + public function go(): void |
179 | 198 | {
|
180 |
| - $parts = $this->getErrorMessageAsParts($errorContext, 2); |
181 |
| - return sprintf('Can not call %s from %s', $parts[0], $parts[1]); |
| 199 | + $item = new Item("hello"); |
| 200 | + $item->updateName("world"); // ERROR Item::updateName|SomeCode |
182 | 201 | }
|
183 | 202 | }
|
| 203 | + |
| 204 | +$item = new Item("hello"); |
| 205 | +$item->remove(); // ERROR Item::remove |
184 | 206 | ```
|
185 | 207 |
|
186 |
| -The signature of `getErrorMessageAsParts` is: |
| 208 | +The expected error messages would be: |
| 209 | + |
| 210 | +- Line 6: `Can not call Item::updateName from class SomeCode` |
| 211 | +- Line 11: `Can not call Item::remove from outside an object` |
| 212 | + |
187 | 213 |
|
188 |
| -```php |
189 |
| -/** |
190 |
| - * @return list<string> |
191 |
| - */ |
192 |
| -protected function getErrorMessageAsParts( |
193 |
| - string $errorContext, |
194 |
| - int $expectedNumberOfParts, |
195 |
| - string $separator = '|', |
196 |
| -): array |
197 |
| -``` |
198 | 214 |
|
199 |
| -If you use the `getErrorMessageAsParts` and the number of parts is not as expected, the test will error with a message that tells you file and line number of the invalid error. |
| 215 | +## Installation |
| 216 | + |
| 217 | +```shell |
| 218 | +composer require --dev dave-liddament/phpstan-rule-test-helper |
| 219 | +``` |
0 commit comments