Passed
Pull Request — master (#1)
by Dominik
16:22
created

MockByCallsTrait::getMockedMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.3332

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 4
cts 6
cp 0.6667
rs 9.9332
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.3332
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 8
    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 8
        $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...
21 8
            ->disableOriginalConstructor()
22 8
            ->disableOriginalClone();
23
24 8
        $mock = $mockBuilder->getMock();
25
26 8
        $mockName = (new \ReflectionObject($mock))->getShortName();
27
28 8
        $class = $this->getMockClassAsString($class);
29
30 8
        $options = JSON_PRETTY_PRINT | JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
31
32 8
        $callIndex = -1;
33
34 8
        $mock->expects(self::any())->method(self::anything())->willReturnCallback(
35 8
            function () use ($class, $mock, $mockName, $callIndex, &$calls, $options) {
36 8
                ++$callIndex;
37 8
                $call = array_shift($calls);
38
39 8
                if (!$call instanceof Call) {
40 1
                    self::fail(
41
                        // fixme: $callIndex + 1 is logically wrong ...
42 1
                        sprintf('Additional call at index %d on class "%s"', $callIndex + 1, $class)
43 1
                        .PHP_EOL
44 1
                        .json_encode($this->getStackTrace($mock), $options)
45
                    );
46
                }
47
48 8
                $mocketMethod = $this->getMockedMethod($mockName);
49
50 8
                self::assertSame(
51 8
                    $mocketMethod,
52 8
                    $call->getMethod(),
53 8
                    sprintf(
54 8
                        'Call at index %d on class "%s" expected method "%s", "%s" given',
55 8
                        $callIndex,
56 8
                        $class,
57 8
                        $call->getMethod(),
58 8
                        $mocketMethod
59 8
                    ).PHP_EOL.json_encode($this->getStackTrace($mock), $options)
60
                );
61
62 8
                return $this->getMockCallback($class, $callIndex, $call, $mock)(...func_get_args());
0 ignored issues
show
Bug introduced by
It seems like $call defined by array_shift($calls) on line 37 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...
63 8
            }
64
        );
65
66 8
        return $mock;
67
    }
68
69
    /**
70
     * @param string[]|string $class
71
     *
72
     * @return string
73
     */
74 8
    private function getMockClassAsString($class): string
75
    {
76 8
        if (is_array($class)) {
77 1
            return implode('|', $class);
78
        }
79
80 7
        return $class;
81
    }
82
83
    /**
84
     * @param string $mockName
85
     *
86
     * @return string
87
     */
88 8
    private function getMockedMethod(string $mockName): string
89
    {
90 8
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
91 8
            if ($mockName === $trace['class']) {
92 8
                return $trace['function'];
93
            }
94
        }
95
96
        self::fail('Cannot find mocked function');
97
    }
98
99
    /**
100
     * @param string     $class
101
     * @param int        $callIndex
102
     * @param Call       $call
103
     * @param MockObject $mock
104
     *
105
     * @return \Closure
106
     */
107
    private function getMockCallback(
108
        string $class,
109
        int $callIndex,
110
        Call $call,
111
        MockObject $mock
112
    ): \Closure {
113 8
        return function () use ($class, $callIndex, $call, $mock) {
114 8
            if ($call->hasWith()) {
115 8
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
116
            }
117
118 7
            if (null !== $exception = $call->getException()) {
119 1
                throw $exception;
120
            }
121
122 6
            if ($call->hasReturnSelf()) {
123 2
                return $mock;
124
            }
125
126 4
            if ($call->hasReturn()) {
127 2
                return $call->getReturn();
128
            }
129 8
        };
130
    }
131
132
    /**
133
     * @param string $class
134
     * @param string $method
135
     * @param int    $at
136
     * @param array  $expectedArguments
137
     * @param array  $arguments
138
     */
139 8
    private function compareArguments(
140
        string $class,
141
        string $method,
142
        int $at,
143
        array $expectedArguments,
144
        array $arguments
145
    ) {
146 8
        $expectedArgumentsCount = count($expectedArguments);
147 8
        $argumentsCount = count($arguments);
148
149 8
        self::assertSame(
150 8
            $expectedArgumentsCount,
151 8
            $argumentsCount,
152 8
            sprintf(
153 8
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
154 8
                $method,
155 8
                $class,
156 8
                $at,
157 8
                $expectedArgumentsCount,
158 8
                $argumentsCount
159
            )
160
        );
161
162 8
        foreach ($expectedArguments as $index => $expectedArgument) {
163 8
            if ($expectedArgument instanceof ArgumentInterface) {
164 3
                $expectedArgument->assert(
165 3
                    $arguments[$index],
166 3
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
167
                );
168
169 2
                continue;
170
            }
171
172 7
            self::assertSame(
173 7
                $expectedArgument,
174 7
                $arguments[$index],
175 7
                sprintf(
176 7
                    'Method "%s" on class "%s" at call %d, argument %d',
177 7
                    $method,
178 7
                    $class,
179 7
                    $at,
180 7
                    $index
181
                )
182
            );
183
        }
184 7
    }
185
186
    /**
187
     * @param MockObject $mock
188
     *
189
     * @return array
190
     */
191 8
    private function getStackTrace(MockObject $mock): array
192
    {
193 8
        $mockName = (new \ReflectionObject($mock))->getShortName();
194
195 8
        $trace = [];
196 8
        $enableTrace = false;
197 8
        foreach (debug_backtrace() as $i => $row) {
198 8
            if (isset($row['class']) && $mockName === $row['class']) {
199 8
                $enableTrace = true;
200
            }
201
202 8
            if ($enableTrace) {
203 8
                $traceRow = $row['class'].$row['type'].$row['function'];
204
205 8
                if (isset($row['file'])) {
206 8
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
207
                }
208
209 8
                $trace[] = $traceRow;
210
            }
211
        }
212
213 8
        krsort($trace);
214
215 8
        return array_values($trace);
216
    }
217
}
218