Completed
Push — master ( 79721a...435615 )
by Dominik
03:03
created

MockByCallsTrait::getMockClassAsString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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