Passed
Push — master ( e211b4...6009c5 )
by Dan
01:29
created

MockWithExpectations::createMockWithExpectations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Nopolabs\Test;
5
6
use PHPUnit\Framework\TestCase;
7
use PHPUnit_Framework_MockObject_MockBuilder;
8
use PHPUnit_Framework_MockObject_MockObject;
9
use ReflectionClass;
10
use ReflectionMethod;
11
12
class MockWithExpectations
13
{
14
    /** @var TestCase */
15
    private $testCase;
16
17
    public function __construct(TestCase $testCase)
18
    {
19
        $this->testCase = $testCase;
20
    }
21
22
    public function createMockWithExpectations(
23
        string $className,
24
        array $expectations = [],
25
        array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject
26
    {
27
        $expectations = $this->prepareExpectations($expectations);
28
29
        return $this->newPartialMockWithExpectations($className, $expectations, $constructorArgs);
30
    }
31
32
    public function setExpectation(
33
        PHPUnit_Framework_MockObject_MockObject $mock,
34
        $expectation) : void
35
    {
36
        if (!$expectation instanceof Expectation) {
37
            $expectation = $this->prepareExpectation((array)$expectation);
38
        }
39
40
        $expectation->build($mock);
41
    }
42
43
    public function setExpectations(
44
        PHPUnit_Framework_MockObject_MockObject $mock,
45
        array $expectations)
46
    {
47
        foreach ($expectations as $expectation) {
48
            $this->setExpectation($mock, $expectation);
49
        }
50
    }
51
52
    private function prepareExpectation(array $expects) : Expectation
53
    {
54
        list($method, $params, $result, $throws, $invoked) = $this->normalizeExpectation($expects);
55
56
        return new Expectation($method, $params, $result, $throws, $invoked);
57
    }
58
59
    private function prepareExpectations(array $expectations) : array
60
    {
61
        if ($this->isAssociative($expectations)) {
62
            return $this->prepareExpectationsMap($expectations);
63
        }
64
65
        return $this->prepareExpectationsList($expectations);
66
    }
67
68
    private function prepareExpectationsMap(array $map) : array
69
    {
70
        $expectations = [];
71
72
        foreach ($map as $method => $expects) {
73
            if (!\is_array($expects)) {
74
                $expects = ['invoked' => $expects];
75
            }
76
            $expects['method'] = $method;
77
            $expectations[] = $this->prepareExpectation($expects);
78
        }
79
80
        return $expectations;
81
    }
82
83
    private function prepareExpectationsList(array $list) : array
84
    {
85
        $expectations = [];
86
87
        $index = 0;
88
        foreach ($list as $expectation) {
89
            if (!($expectation instanceof Expectation)) {
90
                list($method, $params, $result, $throws, $invoked) = $this->normalizeExpectation((array)$expectation);
91
                $invoked = $invoked ?? TestCase::at($index++);
92
                $expectation = new Expectation($method, $params, $result, $throws, $invoked);
93
            }
94
95
            $expectations[] = $expectation;
96
        }
97
98
        return $expectations;
99
    }
100
101
    private function normalizeExpectation(array $expects) : array
102
    {
103
        $method = $expects['method'] ?? null;
104
        $params = $expects['params'] ?? null;
105
        $result = $expects['result'] ?? null;
106
        $throws = $expects['throws'] ?? null;
107
        $invoked = $expects['invoked'] ?? null;
108
109
        unset(
110
            $expects['method'],
111
            $expects['params'],
112
            $expects['result'],
113
            $expects['throws'],
114
            $expects['invoked']
115
        );
116
117
        $method = (string)($method ?? array_shift($expects));
118
        if ($invoked === null && $expects === ['never']) {
119
            $invoked = array_shift($expects);
120
        }
121
        $params = (array)($params ?? array_shift($expects));
122
        $result = $result ?? array_shift($expects);
123
124
        if ($result !== null && $throws !== null) {
125
            throw new TestException("cannot expect both 'result' and 'throws'");
126
        }
127
128
        return [$method, $params, $result, $throws, $invoked];
129
    }
130
131
    private function newPartialMockWithExpectations(
132
        $className,
133
        array $expectations,
134
        array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject
135
    {
136
        $methods = $this->getMethodsToMock($className, $expectations);
137
        $mock = $this->newPartialMock($className, $methods, $constructorArgs);
138
        $this->setExpectations($mock, $expectations);
139
140
        return $mock;
141
    }
142
143
    private function newPartialMock(
144
        $className,
145
        array $methods = [],
146
        array $constructorArgs = null): PHPUnit_Framework_MockObject_MockObject
147
    {
148
        /** @var PHPUnit_Framework_MockObject_MockBuilder $builder */
149
        $builder = $this->testCase->getMockBuilder($className);
150
        $builder->disableOriginalClone();
151
        $builder->disableArgumentCloning();
152
        $builder->disallowMockingUnknownTypes();
153
        $builder->setMethods(empty($methods) ? null : $methods);
154
155
        if ($constructorArgs === null) {
156
            $builder->disableOriginalConstructor();
157
        } else {
158
            $builder->setConstructorArgs($constructorArgs);
159
        }
160
161
        return $builder->getMock();
162
    }
163
164
    private function isAssociative(array $array): bool
165
    {
166
        return array_keys($array) !== range(0, \count($array) - 1);
167
    }
168
169
    private function getMethodsToMock($className, array $expectations) : array
170
    {
171
        $expectedMethods = $this->getExpectedMethods($expectations);
172
        $missingMethods = $this->getMissingMethods($className);
173
174
        return array_unique(array_merge($expectedMethods, $missingMethods));
175
    }
176
177
    private function getExpectedMethods(array $expectations) : array
178
    {
179
        return array_unique(array_map(
180
            function(Expectation $expectation) {
181
                return $expectation->getMethod();
182
            }, $expectations
183
        ));
184
    }
185
186
    private function getMissingMethods($className) : array
187
    {
188
        $reflection = new ReflectionClass($className);
189
190
        if ($reflection->isInterface()) {
191
            return $this->getPublicMethods($reflection);
192
        }
193
194
        if ($reflection->isAbstract()) {
195
            return $this->getAbstractMethods($reflection);
196
        }
197
198
        return [];
199
    }
200
201
    private function getPublicMethods(ReflectionClass $reflection) : array
202
    {
203
        return array_map(function (ReflectionMethod $method) {
204
            return $method->name;
205
        }, $reflection->getMethods(ReflectionMethod::IS_PUBLIC));
206
    }
207
208
    private function getAbstractMethods(ReflectionClass $reflection) : array
209
    {
210
        return array_map(function (ReflectionMethod $method) {
211
            return $method->name;
212
        }, $reflection->getMethods(ReflectionMethod::IS_ABSTRACT));
213
    }
214
}
215