Passed
Push — master ( d5ffdb...32435a )
by Dominik
43s
created

MockByCallsTrait   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 229
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 99.01%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 4
dl 0
loc 229
ccs 100
cts 101
cp 0.9901
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getMockByCalls() 0 41 2
A prepareMock() 0 8 1
A getMockClassAsString() 0 8 2
A getMockedMethod() 0 8 3
B getMockCallback() 0 30 6
A compareArguments() 0 46 3
B getStackTrace() 0 38 9
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
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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
                $mocketMethod = $this->getMockedMethod($mockName);
38
39 10
                if ($mocketMethod !== $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
                            $mocketMethod
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());
0 ignored issues
show
Bug introduced by
It seems like $call defined by array_shift($calls) on line 34 can be null; however, Chubbyphp\Mock\MockByCallsTrait::getMockCallback() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

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