MockByCallsTrait   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 197
Duplicated Lines 0 %

Test Coverage

Coverage 98.84%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 26
eloc 92
c 4
b 2
f 0
dl 0
loc 197
ccs 85
cts 86
cp 0.9884
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getMockedMethod() 0 5 3
A compareArguments() 0 42 3
A getMockCallback() 0 27 6
A getMockByCalls() 0 40 2
A getMockClassAsString() 0 7 2
B getStackTrace() 0 37 9
A prepareMock() 0 8 1
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
    private function getMockByCalls($class, array $calls = []): MockObject
17
    {
18 11
        $mock = $this->prepareMock($class);
19
20 11
        $mockName = (new \ReflectionObject($mock))->getShortName();
21
22 11
        $className = $this->getMockClassAsString($class);
23
24 11
        $options = JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
25
26 11
        $callIndex = -1;
27
28 11
        $mock->expects(self::exactly(count($calls)))->method(self::anything())->willReturnCallback(
29
            function () use ($className, $mock, $mockName, &$callIndex, &$calls, $options) {
30 11
                ++$callIndex;
31 11
32 10
                $call = array_shift($calls);
33
34 10
                $method = $call->getMethod();
35
                $mockedMethod = $this->getMockedMethod($mockName);
36 10
37 10
                if ($mockedMethod !== $method) {
38
                    self::fail(
39 10
                        sprintf(
40 1
                            'Call at index %d on class "%s" expected method "%s", "%s" given',
41 1
                            $callIndex,
42 1
                            $className,
43 1
                            $method,
44 1
                            $mockedMethod
45 1
                        )
46 1
                        .PHP_EOL
47
                        .json_encode($this->getStackTrace($mock), $options)
48 1
                    );
49 1
                }
50
51
                return $this->getMockCallback($className, $callIndex, $call, $mock)(...func_get_args());
52
            }
53 9
        );
54 11
55
        return $mock;
56
    }
57 11
58
    /**
59
     * @param string[]|string $class
60
     */
61
    private function prepareMock($class): MockObject
62
    {
63
        $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

63
        $mockBuilder = $this->/** @scrutinizer ignore-call */ getMockBuilder($class)
Loading history...
64
            ->disableOriginalConstructor()
65 11
            ->disableOriginalClone()
66
        ;
67 11
68 11
        return $mockBuilder->getMock();
69 11
    }
70
71 11
    /**
72
     * @param string[]|string $class
73
     */
74
    private function getMockClassAsString($class): string
75
    {
76
        if (is_array($class)) {
77
            return implode('|', $class);
78
        }
79 11
80
        return $class;
81 11
    }
82 1
83
    private function getMockedMethod(string $mockName): string
84
    {
85 10
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
86
            if ($mockName === $trace['class']) {
87
                return $trace['function'];
88
            }
89
        }
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...
90
    }
91
92
    private function getMockCallback(
93 10
        string $class,
94
        int $callIndex,
95 10
        Call $call,
96 10
        MockObject $mock
97 10
    ): \Closure {
98
        return function () use ($class, $callIndex, $call, $mock) {
99
            if ($call->hasWith()) {
100
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
101
            }
102
103
            if (null !== $exception = $call->getException()) {
104
                throw $exception;
105
            }
106
107
            if ($call->hasReturnSelf()) {
108
                return $mock;
109
            }
110
111
            if ($call->hasReturn()) {
112
                return $call->getReturn();
113
            }
114
115
            if ($call->hasReturnCallback()) {
116 9
                $callback = $call->getReturnCallback();
117 9
118 9
                return $callback(...func_get_args());
119
            }
120
        };
121 8
    }
122 1
123
    private function compareArguments(
124
        string $class,
125 7
        string $method,
126 2
        int $at,
127
        array $expectedArguments,
128
        array $arguments
129 5
    ) {
130 1
        $expectedArgumentsCount = count($expectedArguments);
131
        $argumentsCount = count($arguments);
132
133 4
        self::assertSame(
134 1
            $expectedArgumentsCount,
135
            $argumentsCount,
136 1
            sprintf(
137
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
138 9
                $method,
139
                $class,
140
                $at,
141
                $expectedArgumentsCount,
142
                $argumentsCount
143
            )
144
        );
145
146
        foreach ($expectedArguments as $index => $expectedArgument) {
147
            if ($expectedArgument instanceof ArgumentInterface) {
148 9
                $expectedArgument->assert(
149
                    $arguments[$index],
150
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
151
                );
152
153
                continue;
154
            }
155 9
156 9
            self::assertSame(
157
                $expectedArgument,
158 9
                $arguments[$index],
159 9
                sprintf(
160 9
                    'Method "%s" on class "%s" at call %d, argument %d',
161 9
                    $method,
162 9
                    $class,
163 9
                    $at,
164 9
                    $index
165 9
                )
166 9
            );
167 9
        }
168
    }
169
170
    private function getStackTrace(MockObject $mock): array
171 9
    {
172 9
        $mockName = (new \ReflectionObject($mock))->getShortName();
173 3
174 3
        $trace = [];
175 3
        $enableTrace = false;
176
        foreach (debug_backtrace() as $i => $row) {
177
            if (isset($row['class']) && $mockName === $row['class']) {
178 2
                $enableTrace = true;
179
            }
180
181 8
            if ($enableTrace) {
182 8
                $traceRow = '';
183 8
184 8
                if (isset($row['class'])) {
185 8
                    $traceRow .= $row['class'];
186 8
                }
187 8
188 8
                if (isset($row['type'])) {
189 8
                    $traceRow .= $row['type'];
190
                }
191
192
                if (isset($row['function'])) {
193 8
                    $traceRow .= $row['function'];
194
                }
195
196
                if (isset($row['file'])) {
197
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
198
                }
199
200 1
                $trace[] = $traceRow;
201
            }
202 1
        }
203
204 1
        krsort($trace);
205 1
206 1
        return array_values($trace);
207 1
    }
208
}
209