Completed
Push — master ( 8ef040...c80b7a )
by Dan
02:17
created

MockWithExpectationsTrait::once()

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->getExpectedMethods($expectations);
217
218
        $unimplementedMethods = $this->getUnimplementedMethods($className);
219
220
        return array_unique(array_merge($expectedMethods, $unimplementedMethods));
221
    }
222
223
    private function getExpectedMethods(array $expectations) : array
224
    {
225
        if ($this->isAssociative($expectations)) {
226
            return array_unique(array_keys($expectations));
227
        }
228
229
        return array_unique(array_column($expectations, 0));
230
    }
231
232
    private function getUnimplementedMethods($className) : array
233
    {
234
        $reflection = new ReflectionClass($className);
235
236
        if (!$reflection->isInterface() && !$reflection->isAbstract()) {
237
            return [];
238
        }
239
240
        $filter = $reflection->isInterface()
241
            ? ReflectionMethod::IS_PUBLIC
242
            : ReflectionMethod::IS_ABSTRACT;
243
244
         return array_map(function(ReflectionMethod $method) {
245
             return $method->name;
246
         }, $reflection->getMethods($filter));
247
    }
248
}
249