Passed
Push — master ( cea28e...02d9ed )
by Dominik
03:43 queued 01:36
created

MockByCallsTrait::getMockCallback()   A

Complexity

Conditions 6
Paths 1

Size

Total Lines 27
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 12
c 2
b 0
f 0
dl 0
loc 27
ccs 12
cts 12
cp 1
rs 9.2222
cc 6
nc 1
nop 4
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Mock;
6
7
use Chubbyphp\Mock\Argument\ArgumentInterface;
8
use PHPUnit\Framework\MockObject\MockObject;
9
10
trait MockByCallsTrait
11
{
12
    /**
13
     * @param string[]|string $class
14
     * @param Call[]          $calls
15
     *
16
     * @return MockObject
17
     */
18 11
    private function getMockByCalls($class, array $calls = []): MockObject
19
    {
20 11
        $mock = $this->prepareMock($class);
21
22 11
        $mockName = (new \ReflectionObject($mock))->getShortName();
23
24 11
        $className = $this->getMockClassAsString($class);
25
26 11
        $options = JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
27
28 11
        $callIndex = -1;
29
30 11
        $mock->expects(self::exactly(count($calls)))->method(self::anything())->willReturnCallback(
31 11
            function () use ($className, $mock, $mockName, &$callIndex, &$calls, $options) {
32 10
                ++$callIndex;
33
34 10
                $call = array_shift($calls);
35
36 10
                $method = $call->getMethod();
37 10
                $mockedMethod = $this->getMockedMethod($mockName);
38
39 10
                if ($mockedMethod !== $method) {
40 1
                    self::fail(
41 1
                        sprintf(
42 1
                            'Call at index %d on class "%s" expected method "%s", "%s" given',
43 1
                            $callIndex,
44 1
                            $className,
45 1
                            $method,
46 1
                            $mockedMethod
47
                        )
48 1
                        .PHP_EOL
49 1
                        .json_encode($this->getStackTrace($mock), $options)
50
                    );
51
                }
52
53 9
                return $this->getMockCallback($className, $callIndex, $call, $mock)(...func_get_args());
54 11
            }
55
        );
56
57 11
        return $mock;
58
    }
59
60
    /**
61
     * @param string[]|string $class
62
     *
63
     * @return MockObject
64
     */
65 11
    private function prepareMock($class): MockObject
66
    {
67 11
        $mockBuilder = $this->getMockBuilder($class)
0 ignored issues
show
Bug introduced by
It seems like getMockBuilder() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
        $mockBuilder = $this->/** @scrutinizer ignore-call */ getMockBuilder($class)
Loading history...
68 11
            ->disableOriginalConstructor()
69 11
            ->disableOriginalClone()
70
        ;
71 11
72
        return $mockBuilder->getMock();
73
    }
74
75
    /**
76
     * @param string[]|string $class
77
     *
78
     * @return string
79 11
     */
80
    private function getMockClassAsString($class): string
81 11
    {
82 1
        if (is_array($class)) {
83
            return implode('|', $class);
84
        }
85 10
86
        return $class;
87
    }
88
89
    /**
90
     * @param string $mockName
91
     *
92
     * @return string
93 10
     */
94
    private function getMockedMethod(string $mockName): string
95 10
    {
96 10
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
97 10
            if ($mockName === $trace['class']) {
98
                return $trace['function'];
99
            }
100
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
101
    }
102
103
    /**
104
     * @param string     $class
105
     * @param int        $callIndex
106
     * @param Call       $call
107
     * @param MockObject $mock
108
     *
109
     * @return \Closure
110
     */
111
    private function getMockCallback(
112
        string $class,
113
        int $callIndex,
114
        Call $call,
115
        MockObject $mock
116 9
    ): \Closure {
117 9
        return function () use ($class, $callIndex, $call, $mock) {
118 9
            if ($call->hasWith()) {
119
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
120
            }
121 8
122 1
            if (null !== $exception = $call->getException()) {
123
                throw $exception;
124
            }
125 7
126 2
            if ($call->hasReturnSelf()) {
127
                return $mock;
128
            }
129 5
130 1
            if ($call->hasReturn()) {
131
                return $call->getReturn();
132
            }
133 4
134 1
            if ($call->hasReturnCallback()) {
135
                $callback = $call->getReturnCallback();
136 1
137
                return $callback(...func_get_args());
138 9
            }
139
        };
140
    }
141
142
    /**
143
     * @param string $class
144
     * @param string $method
145
     * @param int    $at
146
     * @param array  $expectedArguments
147
     * @param array  $arguments
148 9
     */
149
    private function compareArguments(
150
        string $class,
151
        string $method,
152
        int $at,
153
        array $expectedArguments,
154
        array $arguments
155 9
    ) {
156 9
        $expectedArgumentsCount = count($expectedArguments);
157
        $argumentsCount = count($arguments);
158 9
159 9
        self::assertSame(
160 9
            $expectedArgumentsCount,
161 9
            $argumentsCount,
162 9
            sprintf(
163 9
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
164 9
                $method,
165 9
                $class,
166 9
                $at,
167 9
                $expectedArgumentsCount,
168
                $argumentsCount
169
            )
170
        );
171 9
172 9
        foreach ($expectedArguments as $index => $expectedArgument) {
173 3
            if ($expectedArgument instanceof ArgumentInterface) {
174 3
                $expectedArgument->assert(
175 3
                    $arguments[$index],
176
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
177
                );
178 2
179
                continue;
180
            }
181 8
182 8
            self::assertSame(
183 8
                $expectedArgument,
184 8
                $arguments[$index],
185 8
                sprintf(
186 8
                    'Method "%s" on class "%s" at call %d, argument %d',
187 8
                    $method,
188 8
                    $class,
189 8
                    $at,
190
                    $index
191
                )
192
            );
193 8
        }
194
    }
195
196
    /**
197
     * @param MockObject $mock
198
     *
199
     * @return array
200 1
     */
201
    private function getStackTrace(MockObject $mock): array
202 1
    {
203
        $mockName = (new \ReflectionObject($mock))->getShortName();
204 1
205 1
        $trace = [];
206 1
        $enableTrace = false;
207 1
        foreach (debug_backtrace() as $i => $row) {
208 1
            if (isset($row['class']) && $mockName === $row['class']) {
209
                $enableTrace = true;
210
            }
211 1
212 1
            if ($enableTrace) {
213
                $traceRow = '';
214 1
215 1
                if (isset($row['class'])) {
216
                    $traceRow .= $row['class'];
217
                }
218 1
219 1
                if (isset($row['type'])) {
220
                    $traceRow .= $row['type'];
221
                }
222 1
223 1
                if (isset($row['function'])) {
224
                    $traceRow .= $row['function'];
225
                }
226 1
227 1
                if (isset($row['file'])) {
228
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
229
                }
230 1
231
                $trace[] = $traceRow;
232
            }
233
        }
234 1
235
        krsort($trace);
236 1
237
        return array_values($trace);
238
    }
239
}
240