Completed
Pull Request — master (#1)
by Dominik
49:12 queued 13:03
created

MockByCallsTrait::getMockedMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 5
cp 0.8
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.072
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
97
    /**
98
     * @param string     $class
99
     * @param int        $callIndex
100
     * @param Call       $call
101
     * @param MockObject $mock
102
     *
103
     * @return \Closure
104
     */
105
    private function getMockCallback(
106
        string $class,
107
        int $callIndex,
108
        Call $call,
109
        MockObject $mock
110
    ): \Closure {
111 8
        return function () use ($class, $callIndex, $call, $mock) {
112 8
            if ($call->hasWith()) {
113 8
                $this->compareArguments($class, $call->getMethod(), $callIndex, $call->getWith(), func_get_args());
114
            }
115
116 7
            if (null !== $exception = $call->getException()) {
117 1
                throw $exception;
118
            }
119
120 6
            if ($call->hasReturnSelf()) {
121 2
                return $mock;
122
            }
123
124 4
            if ($call->hasReturn()) {
125 2
                return $call->getReturn();
126
            }
127 8
        };
128
    }
129
130
    /**
131
     * @param string $class
132
     * @param string $method
133
     * @param int    $at
134
     * @param array  $expectedArguments
135
     * @param array  $arguments
136
     */
137 8
    private function compareArguments(
138
        string $class,
139
        string $method,
140
        int $at,
141
        array $expectedArguments,
142
        array $arguments
143
    ) {
144 8
        $expectedArgumentsCount = count($expectedArguments);
145 8
        $argumentsCount = count($arguments);
146
147 8
        self::assertSame(
148 8
            $expectedArgumentsCount,
149 8
            $argumentsCount,
150 8
            sprintf(
151 8
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
152 8
                $method,
153 8
                $class,
154 8
                $at,
155 8
                $expectedArgumentsCount,
156 8
                $argumentsCount
157
            )
158
        );
159
160 8
        foreach ($expectedArguments as $index => $expectedArgument) {
161 8
            if ($expectedArgument instanceof ArgumentInterface) {
162 3
                $expectedArgument->assert(
163 3
                    $arguments[$index],
164 3
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
165
                );
166
167 2
                continue;
168
            }
169
170 7
            self::assertSame(
171 7
                $expectedArgument,
172 7
                $arguments[$index],
173 7
                sprintf(
174 7
                    'Method "%s" on class "%s" at call %d, argument %d',
175 7
                    $method,
176 7
                    $class,
177 7
                    $at,
178 7
                    $index
179
                )
180
            );
181
        }
182 7
    }
183
184
    /**
185
     * @param MockObject $mock
186
     *
187
     * @return array
188
     */
189 8
    private function getStackTrace(MockObject $mock): array
190
    {
191 8
        $mockName = (new \ReflectionObject($mock))->getShortName();
192
193 8
        $trace = [];
194 8
        $enableTrace = false;
195 8
        foreach (debug_backtrace() as $i => $row) {
196 8
            if (isset($row['class']) && $mockName === $row['class']) {
197 8
                $enableTrace = true;
198
            }
199
200 8
            if ($enableTrace) {
201 8
                $traceRow = $row['class'].$row['type'].$row['function'];
202
203 8
                if (isset($row['file'])) {
204 8
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
205
                }
206
207 8
                $trace[] = $traceRow;
208
            }
209
        }
210
211 8
        krsort($trace);
212
213 8
        return array_values($trace);
214
    }
215
}
216