Skip to content

Commit 8251b9f

Browse files
authored
Merge pull request #66 from AndreasMalecki/add-support-for-mockery
Add support for mockery
2 parents 554dcf5 + 9408757 commit 8251b9f

File tree

271 files changed

+13710
-64
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

271 files changed

+13710
-64
lines changed

README.md

Lines changed: 203 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,19 @@ Stable version, JetBrains repository:
2424

2525
## Feature list
2626

27-
* method autocomplete for class, abstract class and trait mock objects;
28-
* type providers: `getMock`, `getMockForAbstractClass`, etc. will return mock object with methods of mocking class and `PHPUnit_Framework_MockObject_MockObject`;
29-
* supported PHPUnit methods:
27+
* Method autocompletion for class, abstract class and trait mock objects;
28+
* Type providers: `getMock`, `getMockForAbstractClass`, etc. will return mock object with methods of mocking class and `PHPUnit_Framework_MockObject_MockObject`;
29+
* Supported PHPUnit methods:
3030
* `PHPUnit_Framework_MockObject_MockBuilder::setMethods`
3131
* `PHPUnit_Framework_TestCase::getMock`
3232
* `PHPUnit_Framework_TestCase::getMockClass`
3333
* `PHPUnit_Framework_TestCase::getMockForAbstractClass`
3434
* `PHPUnit_Framework_TestCase::getMockForTrait`
3535
* `PHPUnit_Framework_MockObject_Builder_InvocationMocker::method`
36-
* code navigation (go to declaration, find usages, etc.) and refactoring (rename methods);
37-
* highlighting of incorrect method usages;
36+
* Code navigation (go to declaration, find usages, etc.) and refactoring (rename methods);
37+
* Highlighting of incorrect method usages;
3838
* Prophecy support.
39+
* Mockery support.
3940

4041
### Mocks
4142

@@ -197,4 +198,200 @@ Examples
197198

198199
![PHPUnit Prophecy](https://jetbrains-plugins.s3.amazonaws.com/9674/screenshot_16953.png)
199200

200-
![PHPUnit Expected exception](https://download.plugins.jetbrains.com/9674/screenshot_17449.png)
201+
![PHPUnitExpected exception](https://download.plugins.jetbrains.com/9674/screenshot_17449.png)
202+
203+
## Mockery
204+
205+
Has support for
206+
* Method referencing and autocomplete for method string in `allows`, `expects`, `shouldReceive`, `shouldNotReceive`,
207+
`shouldHaveReceived`, `shouldNotHaveReceived`. As well as in Generated partial mocks.
208+
* Highlighting for incorrect methods used inside an `allows` etc., when method is private, protected, or not found.
209+
* Type providers to enable new Mockery syntax: `$mock->allows()->foo('arg')->andReturns('mocked_result')`.
210+
* Configurable inspection for replacing legacy Mockery syntax: replacing`$mock->shouldReceive("foo")->with("arg")->andReturn("result")`
211+
with `$mock->allows()->foo("arg")->andReturns("result")`.
212+
213+
### Referencing
214+
215+
In the following code snippets referencing, autocompletion, and refactoring are supported at the carets.
216+
Note that these all work with aliases, overloaded mocks, proxies, and partial mocks.
217+
218+
```php
219+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
220+
{
221+
protected function setUp(): void
222+
{
223+
$mock = Mockery::mock('MockeryPlugin\DemoProject\Dependency');
224+
$mock->allows('fo<caret>o');
225+
$mock->expects('f<caret>oo');
226+
$mock->shouldReceive('ba<caret>r');
227+
$mock->shouldNotReceive('b<caret>ar');
228+
}
229+
}
230+
```
231+
232+
```php
233+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
234+
{
235+
protected function setUp(): void
236+
{
237+
$this->mock = Mockery::mock('MockeryPlugin\DemoProject\Dependency');
238+
}
239+
240+
public function test(): void
241+
{
242+
$this->mock->allows('fo<caret>o')->andReturns('result');
243+
}
244+
}
245+
```
246+
247+
```php
248+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
249+
{
250+
protected function setUp(): void
251+
{
252+
$mock = Mockery::spy('MockeryPlugin\DemoProject\Dependency');
253+
// ...
254+
$mock->shouldHaveReceived('b<caret>ar');
255+
$mock->shouldNotHaveReceived('fo<caret>o');
256+
}
257+
}
258+
```
259+
260+
```php
261+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
262+
{
263+
protected function setUp(): void
264+
{
265+
$mock = Mockery::mock('MockeryPlugin\DemoProject\Dependency');
266+
$mock->shouldReceive('foo', 'b<caret>ar')
267+
$mock->shouldReceive([
268+
'foo' => 'mocked result',
269+
'ba<caret>r' => 'mocked result'
270+
]);
271+
}
272+
}
273+
```
274+
275+
### Generated Partial Mocks
276+
277+
Method name referencing/refactoring is supported when creating generated partial mocks.
278+
```php
279+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
280+
{
281+
protected function setUp(): void
282+
{
283+
$mock = Mockery::mock(Dependency::class . "[f<caret>oo]");
284+
$mock = Mockery::mock('MockeryPlugin\DemoProject\Dependency[f<caret>oo]');
285+
}
286+
}
287+
```
288+
289+
### Method Annotations
290+
291+
A warning highlight is given when the method being used is protected, private, or not found.
292+
293+
```php
294+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
295+
{
296+
protected function setUp(): void
297+
{
298+
$mock = Mockery::mock(Dependency::class);
299+
$mock->expects('protectedMethod');
300+
$mock->expects('privateMethod');
301+
$mock->expects('unknownMethod');
302+
}
303+
}
304+
```
305+
306+
### Inspection
307+
308+
An inspection is provided which will highlight legacy mockery syntax and provides a quick fix
309+
to update. Legacy Mockery uses `shouldReceive`/`shouldNotReceive`, and it gets replaced by `allows`/`expects`, e.g.
310+
311+
```php
312+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
313+
{
314+
protected function setUp(): void
315+
{
316+
$mock = Mockery::mock(Dependency::class);
317+
$mock->shouldReceive('foo')->with('arg')->andReturn('result');
318+
// replaced by
319+
$mock->allows('foo')->with('arg')->andReturns('result');
320+
321+
$mock->shouldReceive('foo')->with('arg')->andReturn('result')->once();
322+
// replaced by
323+
$mock->expects('foo')->with('arg')->andReturns('result');
324+
}
325+
}
326+
```
327+
328+
If a `shouldReceive` has multiple method parameters then these will get combined into an array parameter.
329+
But the inspection can be configured to prefer writing multiple `allows`/`expects` statements.
330+
331+
```php
332+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
333+
{
334+
protected function setUp(): void
335+
{
336+
$mock = Mockery::mock(Dependency::class);
337+
$mock->$this->dependency->shouldReceive('foo', 'bar');
338+
// replaced by
339+
$mock->allows(['foo', 'bar']);
340+
341+
$mock->shouldReceive('foo', 'bar')->andReturns('mocked result');
342+
// replaced by
343+
$mock->allows(['foo' => 'mocked result', 'bar' => 'mocked result']);
344+
}
345+
}
346+
```
347+
348+
```php
349+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
350+
{
351+
protected function setUp(): void
352+
{
353+
$mock = Mockery::mock(Dependency::class);
354+
$mock->$this->dependency->shouldReceive('foo', 'bar');
355+
// replaced by
356+
$this->dependency->allows('foo');
357+
$this->dependency->allows('bar');
358+
359+
$mock->shouldReceive('foo', 'bar')->andReturns('mocked result');
360+
// replaced by
361+
$this->dependency->allows('foo')->andReturns('mocked result');
362+
$this->dependency->allows('bar')->andReturns('mocked result'); }
363+
}
364+
```
365+
366+
The inspection can also be configured to prefer the new Mockery syntax in which the mocked methods are called
367+
like normal rather than as a string.
368+
369+
```php
370+
class Foo extends Mockery\Adapter\Phpunit\MockeryTestCase
371+
{
372+
protected function setUp(): void
373+
{
374+
$mock = Mockery::mock(Dependency::class);
375+
$mock->shouldReceive('foo')->with('arg')->andReturn('result');
376+
// replaced by
377+
$mock->allows()->foo('arg')->andReturns('result');
378+
379+
$mock->shouldReceive('foo')->with('arg')->andReturn('result')->once();
380+
// replaced by
381+
$mock->expects()->foo('arg')->andReturns('result');
382+
}
383+
}
384+
```
385+
386+
### New Mockery Syntax Type Provider
387+
388+
Type providers are implemented so that when calling `allows()` on a mock it will have the type of the
389+
mocked class. Further `allows()->foo()` will be given the type Mockery/Expectation so that methods like `andReturns(..)`
390+
work as expected. This extends also to `expects()`, `shouldReceive()`, `shouldNotReceive()` and `shouldHaveReceived()`.
391+
Note: this new syntax does not extend tp `shouldNotHaveReceived`.
392+
393+
In the following example the first caret has type Dependency, and the second type Expectation.
394+
```php
395+
$mock = Mockery::mock(Dependency::class);
396+
$mock->allows<caret>()->foo<caret>('arg')->andReturns('result');
397+
```
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package de.espend.idea.php.phpunit.annotator;
2+
3+
import com.intellij.lang.annotation.AnnotationHolder;
4+
import com.intellij.lang.annotation.Annotator;
5+
import com.intellij.lang.annotation.HighlightSeverity;
6+
import com.intellij.openapi.util.TextRange;
7+
import com.intellij.patterns.PsiElementPattern;
8+
import com.intellij.psi.PsiElement;
9+
import com.jetbrains.php.PhpIndex;
10+
import com.jetbrains.php.lang.psi.elements.Method;
11+
import com.jetbrains.php.lang.psi.elements.PhpClass;
12+
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
13+
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
14+
import de.espend.idea.php.phpunit.utils.MockeryReferencingUtil;
15+
import de.espend.idea.php.phpunit.utils.PatternUtil;
16+
import org.apache.commons.lang.StringUtils;
17+
import org.jetbrains.annotations.NotNull;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.function.Function;
22+
import java.util.stream.Collectors;
23+
24+
public class MockeryAnnotator implements Annotator {
25+
26+
private enum Scope {
27+
PARAMETER(PatternUtil.getMethodReferenceWithParameterPattern(),
28+
MockeryReferencingUtil::findMockeryMockParametersOnParameterScope),
29+
ARRAY_HASH(PatternUtil.getMethodReferenceWithArrayHashPattern(),
30+
MockeryReferencingUtil::findMockeryMockParametersOnArrayHashScope),
31+
ARRAY_ELEMENT(PatternUtil.getMethodReferenceWithArrayElementPattern(),
32+
MockeryReferencingUtil::findMockeryMockParametersOnArrayElementScope);
33+
34+
private final PsiElementPattern.Capture<StringLiteralExpression> psiElementPattern;
35+
private final Function<StringLiteralExpression, String[]> getMockCreationParametersMethod;
36+
37+
Scope(PsiElementPattern.Capture<StringLiteralExpression> psiElementPattern, Function<StringLiteralExpression, String[]> getMockCreationParametersMethod) {
38+
this.psiElementPattern = psiElementPattern;
39+
this.getMockCreationParametersMethod = getMockCreationParametersMethod;
40+
}
41+
42+
public PsiElementPattern.Capture<StringLiteralExpression> getPsiElementPattern() {
43+
return psiElementPattern;
44+
}
45+
46+
public String[] getMockCreationParameters(StringLiteralExpression exp) {
47+
return getMockCreationParametersMethod.apply(exp);
48+
}
49+
}
50+
51+
@Override
52+
public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder annotationHolder) {
53+
annotateByScope(Scope.PARAMETER, psiElement, annotationHolder);
54+
annotateByScope(Scope.ARRAY_HASH, psiElement, annotationHolder);
55+
annotateByScope(Scope.ARRAY_ELEMENT, psiElement, annotationHolder);
56+
}
57+
58+
private void annotateByScope(Scope scope, @NotNull PsiElement psiElement, @NotNull AnnotationHolder annotationHolder) {
59+
PsiElementPattern.Capture<StringLiteralExpression> pattern = scope.getPsiElementPattern();
60+
61+
if (pattern.accepts(psiElement)) {
62+
if (psiElement instanceof StringLiteralExpression) {
63+
// the method name put as input string in expects/allows etc
64+
String contents = ((StringLiteralExpression) psiElement).getContents();
65+
66+
if (StringUtils.isNotBlank(contents)) {
67+
String[] mockCreationParameters = scope.getMockCreationParameters((StringLiteralExpression) psiElement);
68+
69+
if (mockCreationParameters != null && mockCreationParameters.length > 0) {
70+
71+
Set<Method> allMethods = new HashSet<>();
72+
Set<String> classNames = new HashSet<>();
73+
for (String mockCreationParameter : mockCreationParameters) {
74+
for (PhpClass phpClass : PhpIndex.getInstance(psiElement.getProject()).getAnyByFQN(mockCreationParameter)) {
75+
classNames.add(phpClass.getName());
76+
77+
allMethods.addAll(phpClass.getMethods().stream()
78+
.filter(method -> !method.getAccess().isPublic() || !method.getName().startsWith("__"))
79+
.collect(Collectors.toSet())
80+
);
81+
}
82+
}
83+
84+
Set<String> allMethodNames = allMethods.stream()
85+
.map(PhpNamedElement::getName)
86+
.collect(Collectors.toSet());
87+
88+
Set<String> privateMethodNames = allMethods.stream()
89+
.filter(method -> method.getAccess().isPrivate())
90+
.map(PhpNamedElement::getName)
91+
.collect(Collectors.toSet());
92+
93+
Set<String> protectedMethodNames = allMethods.stream()
94+
.filter(method -> method.getAccess().isProtected())
95+
.map(PhpNamedElement::getName)
96+
.collect(Collectors.toSet());
97+
98+
99+
TextRange textRange = psiElement.getTextRange();
100+
TextRange annotationTextRange = new TextRange(textRange.getStartOffset() + 1, textRange.getEndOffset() - 1);
101+
102+
if (!allMethodNames.contains(contents)) {
103+
if (classNames.size() == 1) {
104+
String className = classNames.toArray()[0].toString();
105+
annotationHolder.newAnnotation(HighlightSeverity.WARNING, "Method '" + contents + "' not found in class " + className)
106+
.range(annotationTextRange).create();
107+
} else {
108+
annotationHolder.newAnnotation(HighlightSeverity.WARNING, "Method '" + contents + "' not found in any of classes " + classNames)
109+
.range(annotationTextRange).create();
110+
}
111+
} else if (privateMethodNames.contains(contents)) {
112+
annotationHolder.newAnnotation(HighlightSeverity.WARNING, "Method '" + contents + "' is private, Mockery does not support private methods")
113+
.range(annotationTextRange).create();
114+
} else if (protectedMethodNames.contains(contents)) {
115+
annotationHolder.newAnnotation(HighlightSeverity.WARNING, "Method '" + contents + "' is protected. Mocking protected methods is not suggested. Further guidance can be found here http://docs.mockery.io/en/latest/reference/protected_methods.html")
116+
.range(annotationTextRange).create();
117+
}
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)