Completed
Push — master ( 10656e...8ef040 )
by Dan
02:24
created

MockWithExpectationsTrait::atMost()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
c 0
b 0
f 0
nc 1
1
<?php
2
namespace Nopolabs\Test;
3
4
use Closure;
5
use PHPUnit\Framework\Exception;
6
use PHPUnit_Framework_MockObject_Matcher_Invocation;
7
use PHPUnit_Framework_MockObject_MockBuilder;
8
use PHPUnit_Framework_MockObject_MockObject;
9
use ReflectionClass;
10
use ReflectionMethod;
11
12
/**
13
 * This trait expects to be used in a sub-class of PHPUnit\Framework\TestCase
14
 */
15
trait MockWithExpectationsTrait
16
{
17
    // abstract methods implemented by PHPUnit\Framework\TestCase
18
    abstract public function getMockBuilder($className);
19
    abstract public static function any();
20
    abstract public static function at($index);
21
    abstract public static function atLeast($requiredInvocations);
22
    abstract public static function atLeastOnce();
23
    abstract public static function atMost($allowedInvocations);
24
    abstract public static function exactly($count);
25
    abstract public static function never();
26
    abstract public static function once();
27
28
    protected function newPartialMockWithExpectations(
29
        $className,
30
        array $expectations = [],
31
        array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject
32
    {
33
        $methods = $this->getMethodsToMock($className, $expectations);
34
        $mock = $this->newPartialMock($className, $methods, $constructorArgs);
35
36
        if ($this->isAssociative($expectations)) {
37
            return $this->setExpectations($mock, $expectations);
38
        }
39
40
        return $this->setAtExpectations($mock, $expectations);
41
    }
42
43
    protected function newPartialMock(
44
        $className,
45
        array $methods = [],
46
        array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject
47
    {
48
        /** @var PHPUnit_Framework_MockObject_MockBuilder $builder */
49
        $builder = $this->getMockBuilder($className)
50
            ->disableOriginalClone()
51
            ->disableArgumentCloning()
52
            ->disallowMockingUnknownTypes()
53
            ->setMethods(empty($methods) ? null : $methods);
54
55
        if ($constructorArgs === null) {
56
            $builder->disableOriginalConstructor();
57
        } else {
58
            $builder->setConstructorArgs($constructorArgs);
59
        }
60
61
        return $builder->getMock();
62
    }
63
64
    protected function setExpectations(
65
        PHPUnit_Framework_MockObject_MockObject $mock,
66
        array $expectations
67
    ) : PHPUnit_Framework_MockObject_MockObject
68
    {
69
        foreach ($expectations as $method => $expectation) {
70
            if (!is_array($expectation)) {
71
                $expectation = ['invoked' => $expectation];
72
            }
73
            $this->setExpectation($mock, $method, $expectation);
74
        }
75
76
        return $mock;
77
    }
78
79
    protected function setAtExpectations(
80
        PHPUnit_Framework_MockObject_MockObject $mock,
81
        array $atExpectations
82
    ) : PHPUnit_Framework_MockObject_MockObject
83
    {
84
        $index = 0;
85
        foreach ($atExpectations as $atExpectation) {
86
            array_push($atExpectation, []);
87
            list($method, $expectation) = $atExpectation;
88
            if ($expectation === 'never') {
89
                $mock->expects($this->never())->method($method);
90
            } elseif (is_array($expectation)) {
91
                $expectation['invoked'] = $this->at($index++);
92
                $this->setExpectation($mock, $method, $expectation);
93
            } else {
94
                throw new Exception("setAtExpectations cannot understand expectation '$expectation'");
95
            }
96
        }
97
98
        return $mock;
99
    }
100
101
    protected function setExpectation(
102
        PHPUnit_Framework_MockObject_MockObject $mock,
103
        $method,
104
        array $expectation)
105
    {
106
        $invoked = $this->getInvoked($expectation);
107
        $params = $expectation['params'] ?? null;
108
        $result = $expectation['result'] ?? null;
109
        $throws = $expectation['throws'] ?? null;
110
111
        if ($params !== null && !is_array($params)) {
112
            throw new Exception("expected params to be an array, got '$params'");
113
        }
114
115
        if ($result !== null && $throws !== null) {
116
            throw new Exception("cannot expect both 'result' and 'throws'");
117
        }
118
119
        $builder = $mock->expects($invoked)->method($method);
120
121
        if ($params !== null) {
122
            call_user_func_array([$builder, 'with'], $params);
123
        }
124
125
        if ($result !== null) {
126
            if ($result instanceof Closure) {
127
                $builder->willReturnCallback($result);
128
            } else {
129
                $builder->willReturn($result);
130
            }
131
        }
132
133
        if ($throws !== null) {
134
            $builder->willThrowException($throws);
135
        }
136
    }
137
138
    protected function getInvoked(array $expectation): PHPUnit_Framework_MockObject_Matcher_Invocation
139
    {
140
        if (isset($expectation['invoked'])) {
141
            if ($expectation['invoked'] instanceof PHPUnit_Framework_MockObject_Matcher_Invocation) {
142
                $invoked = $expectation['invoked'];
143
            } else {
144
                $invoked = $this->convertToInvocation($expectation['invoked']);
145
            }
146
        } else {
147
            $invoked = $this->any();
148
        }
149
150
        return $invoked;
151
    }
152
153
    protected function convertToInvocation($invoked): PHPUnit_Framework_MockObject_Matcher_Invocation
154
    {
155
        if (is_numeric($invoked)) {
156
            return $this->convertNumeric($invoked);
157
        }
158
        if (preg_match("/(?'method'\w+)(?:\s+(?'count'\d+))?/", $invoked, $matches)) {
159
            if (isset($matches['count'])) {
160
                return $this->convertTwoWords($matches['method'], (int)$matches['count']);
161
            }
162
            return $this->convertOneWord($matches['method']);
163
        }
164
        throw new Exception("convertToMatcher cannot convert '$invoked'");
165
    }
166
167
    protected function convertNumeric(int $times): PHPUnit_Framework_MockObject_Matcher_Invocation
168
    {
169
        if ($times === 0) {
170
            return $this->never();
171
        }
172
        if ($times === 1) {
173
            return $this->once();
174
        }
175
        return $this->exactly($times);
176
    }
177
178
    protected function convertOneWord(string $method): PHPUnit_Framework_MockObject_Matcher_Invocation
179
    {
180
        if ($method === 'once') {
181
            return $this->once();
182
        }
183
        if ($method === 'any') {
184
            return $this->any();
185
        }
186
        if ($method === 'never') {
187
            return $this->never();
188
        }
189
        if ($method === 'atLeastOnce') {
190
            return $this->atLeastOnce();
191
        }
192
        throw new Exception("convertOneWord cannot convert '$method'");
193
    }
194
195
    protected function convertTwoWords(string $method, int $count): PHPUnit_Framework_MockObject_Matcher_Invocation
196
    {
197
        if ($method === 'atLeast') {
198
            return $this->atLeast($count);
199
        }
200
        if ($method === 'exactly') {
201
            return $this->exactly($count);
202
        }
203
        if ($method === 'atMost') {
204
            return $this->atMost($count);
205
        }
206
        throw new Exception("convertTwoWords cannot convert '$method $count'");
207
    }
208
209
    private function isAssociative(array $array): bool
210
    {
211
        return array_keys($array) !== range(0, count($array) - 1);
212
    }
213
214
    private function getMethodsToMock($className, array $expectations)
215
    {
216
        $expectedMethods = $this->isAssociative($expectations)
217
            ? array_unique(array_keys($expectations))
218
            : array_unique(array_column($expectations, 0));
219
220
        return $this->addMissingMethods($className, $expectedMethods);
221
    }
222
223
    private function addMissingMethods($className, array $methods) : array
224
    {
225
        $missingMethods = $this->getMissingMethods($className, $methods);
226
        foreach ($missingMethods as $method) {
227
            $methods[] = $method;
228
        }
229
230
        return $methods;
231
    }
232
233
    private function getMissingMethods($className, array $methods) : array
234
    {
235
        $reflection = new ReflectionClass($className);
236
237 View Code Duplication
        if ($reflection->isInterface()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
238
            $publicMethods = array_map(function (ReflectionMethod $method) {
239
                return $method->name;
240
            }, $reflection->getMethods(ReflectionMethod::IS_PUBLIC));
241
242
            return array_diff($publicMethods, $methods);
243
        }
244
245 View Code Duplication
        if ($reflection->isAbstract()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
            $abstractMethods = array_map(function(ReflectionMethod $method) {
247
                return $method->name;
248
            }, $reflection->getMethods(ReflectionMethod::IS_ABSTRACT));
249
250
            return array_diff($abstractMethods, $methods);
251
        }
252
253
        return [];
254
    }
255
}
256