Completed
Branch master (9447a1)
by Dan
02:47
created

MockWithExpectations2Trait::newPartialMock()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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