PlatineTestCase::getMockInstanceMap()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 11
c 2
b 0
f 0
nc 2
nop 3
dl 0
loc 21
rs 9.9
1
<?php
2
3
/**
4
 * Platine Dev Tools
5
 *
6
 * Platine Dev Tools is a collection of some classes/functions
7
 * designed for development
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Dev Tools
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file PlatineTestCase.php
34
 *
35
 *  The Base class used for test case
36
 *
37
 *  @package    Platine\Dev
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Dev;
49
50
use InvalidArgumentException;
51
use org\bovigo\vfs\vfsStream;
52
use org\bovigo\vfs\vfsStreamContainer;
53
use org\bovigo\vfs\vfsStreamDirectory;
54
use org\bovigo\vfs\vfsStreamFile;
55
use PHPUnit\Framework\MockObject\MockObject;
56
use PHPUnit\Framework\TestCase;
57
use ReflectionClass;
58
use ReflectionNamedType;
59
use ReflectionObject;
60
use ReflectionParameter;
61
use ReflectionProperty;
62
use Throwable;
63
64
/**
65
 * @class PlatineTestCase
66
 * @package Platine\Dev
67
 */
68
class PlatineTestCase extends TestCase
69
{
70
    /**
71
     * The class create object maps
72
     * @var array<class-string, array<string, mixed>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, array<string, mixed>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, array<string, mixed>>.
Loading history...
73
     */
74
    protected array $createObjectMaps = [];
75
76
77
    /**
78
     * Set create object map
79
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
80
     * @param array<string, mixed> $maps
81
     * @return $this
82
     */
83
    public function setClassCreateObjectMaps(string $class, array $maps): self
84
    {
85
        $this->createObjectMaps[$class] = $maps;
86
87
        return $this;
88
    }
89
90
    /**
91
     * Create object
92
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
93
     * @return object|null
94
     */
95
    public function createObject(string $class): ?object
96
    {
97
        $rc = new ReflectionClass($class);
98
99
        if ($rc->isInstantiable() === false) {
100
            return null;
101
        }
102
103
        $constructor = $rc->getConstructor();
104
105
        if ($constructor === null) {
106
            return $rc->newInstanceWithoutConstructor();
107
        }
108
109
        $arguments = [];
110
        $parameters = $constructor->getParameters();
111
        $maps = $this->createObjectMaps[$class] ?? [];
112
        foreach ($parameters as /** @var ReflectionParameter $parameter */ $parameter) {
113
            $name = $parameter->getName();
114
            if (array_key_exists($name, $maps)) {
115
                $arguments[] = $maps[$name];
116
                continue;
117
            }
118
119
            /** @var ReflectionNamedType|null $type */
120
            $type = $parameter->getType();
121
            $value = null;
122
            if ($type !== null && $type->isBuiltin() === false) {
123
                // Create Mock
124
                /** @var class-string $className */
125
                $className = $type->getName();
126
                $value = $this->getMockInstance($className);
127
            }
128
129
            if ($value === null) {
130
                if ($parameter->isDefaultValueAvailable()) {
131
                    $value = $parameter->getDefaultValue();
132
                }
133
            }
134
135
            $arguments[] = $value;
136
        }
137
138
        return $rc->newInstanceArgs($arguments);
139
    }
140
141
    /**
142
     * Test object method call count
143
     * @param MockObject $object
144
     * @param string $method
145
     * @param int $count
146
     * @return void
147
     */
148
    public function expectMethodCallCount(MockObject $object, string $method, int $count = 1): void
149
    {
150
        $object->expects($this->exactly($count))
151
                ->method($method);
152
    }
153
154
    /**
155
     * Method to test private & protected method
156
     *
157
     * @param object $object the class instance to use
158
     * @param string $method the name of the method
159
     * @param array<int, mixed> $args the list of method arguments
160
     * @return mixed
161
     */
162
    public function runPrivateProtectedMethod(
163
        object $object,
164
        string $method,
165
        array $args = []
166
    ) {
167
        $reflection = new ReflectionObject($object);
168
        $reflectionMethod = $reflection->getMethod($method);
169
        $reflectionMethod->setAccessible(true);
170
        return $reflectionMethod->invokeArgs($object, $args);
171
    }
172
173
    /**
174
     * Method to set/get private & protected attribute
175
     *
176
     * @param class-string $className the name of the class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
177
     * @param string $attr the name of the class attribute
178
     */
179
    public function getPrivateProtectedAttribute(
180
        string $className,
181
        string $attr
182
    ): ReflectionProperty {
183
        $rProp = new ReflectionProperty($className, $attr);
184
        $rProp->setAccessible(true);
185
        return $rProp;
186
    }
187
188
    /**
189
     * Create virtual file with the given content
190
     * @param  string $filename
191
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator> $destination
192
     * @param  string $content
193
     * @return vfsStreamFile
194
     */
195
    public function createVfsFile(
196
        string $filename,
197
        vfsStreamContainer $destination,
198
        string $content = ''
199
    ): vfsStreamFile {
200
        return vfsStream::newFile($filename)
201
                        ->at($destination)
202
                        ->setContent($content);
203
    }
204
205
    /**
206
     * Create virtual file without content
207
     * @param  string $filename
208
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator> $destination
209
     * @return vfsStreamFile
210
     */
211
    public function createVfsFileOnly(
212
        string $filename,
213
        vfsStreamContainer $destination
214
    ): vfsStreamFile {
215
        return vfsStream::newFile($filename)
216
                        ->at($destination);
217
    }
218
219
    /**
220
     * Create virtual directory
221
     * @param  string $name
222
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator>|null $destination
223
     * @return vfsStreamDirectory
224
     */
225
    public function createVfsDirectory(
226
        string $name,
227
        ?vfsStreamContainer $destination = null
228
    ): vfsStreamDirectory {
229
        if ($destination !== null) {
230
            return vfsStream::newDirectory($name)->at($destination);
231
        }
232
        return vfsStream::newDirectory($name);
233
    }
234
235
    /**
236
     * Return the list of methods to mocks in the parameters of PHPUnit::TestCase::getMock()
237
     *
238
     * @param class-string<object>|object $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<object>|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<object>|object.
Loading history...
239
     * @param string[] $exclude list of methods to exclude
240
     * @return string[]
241
     */
242
    public function getClassMethodsToMock(string|object $class, array $exclude = []): array
243
    {
244
        $methods = [];
245
246
        if (is_string($class) && !class_exists($class)) {
247
            throw new InvalidArgumentException(
248
                sprintf('Can not find class [%s]', $class)
249
            );
250
        }
251
252
        $reflectionClass = new ReflectionClass($class);
253
254
        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
255
            if (!in_array($reflectionMethod->name, $exclude)) {
256
                $methods[] = $reflectionMethod->name;
257
            }
258
        }
259
260
        return $methods;
261
    }
262
263
    /**
264
     * Get the instance of the given class
265
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
266
     * @param array<string, mixed> $mockMethods
267
     * @param array<int, string> $excludes
268
     * @return MockObject
269
     */
270
    public function getMockInstance(
271
        string $class,
272
        array $mockMethods = [],
273
        array $excludes = []
274
    ): MockObject {
275
        $methods = $this->getClassMethodsToMock($class, $excludes);
276
277
        $mock = $this->getMockBuilder($class)
278
                    ->disableOriginalConstructor()
279
                    ->onlyMethods($methods)
280
                    ->getMock();
281
282
        foreach ($mockMethods as $method => $returnValue) {
283
            if ($returnValue === 'self') {
284
                $mock->expects($this->any())
285
                    ->method($method)
286
                    ->will($this->returnSelf());
287
288
                continue;
289
            }
290
291
            if ($returnValue instanceof Throwable) {
292
                $mock->expects($this->any())
293
                    ->method($method)
294
                    ->will($this->throwException($returnValue));
295
296
                continue;
297
            }
298
299
            $mock->expects($this->any())
300
                ->method($method)
301
                ->will($this->returnValue($returnValue));
302
        }
303
304
        return $mock;
305
    }
306
307
    /**
308
     * Get the instance of the given class using return map
309
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
310
     * @param array<string, mixed> $mockMethods
311
     * @param array<int, string> $excludes
312
     * @return MockObject
313
     */
314
    public function getMockInstanceMap(
315
        string $class,
316
        array $mockMethods = [],
317
        array $excludes = []
318
    ): MockObject {
319
        $methods = $this->getClassMethodsToMock($class, $excludes);
320
321
        $mock = $this->getMockBuilder($class)
322
                    ->disableOriginalConstructor()
323
                    ->onlyMethods($methods)
324
                    ->getMock();
325
326
        foreach ($mockMethods as $method => $returnValues) {
327
            $mock->expects($this->any())
328
                ->method($method)
329
                ->will(
330
                    $this->returnValueMap($returnValues)
331
                );
332
        }
333
334
        return $mock;
335
    }
336
337
    /**
338
     * Return the value of private or protected property
339
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
340
     * @param object $instance
341
     * @param string $name
342
     * @return mixed
343
     */
344
    public function getPropertyValue(string $class, object $instance, string $name): mixed
345
    {
346
        $reflection = $this->getPrivateProtectedAttribute($class, $name);
347
        return $reflection->getValue($instance);
348
    }
349
350
    /**
351
     * Set the value of private or protected property
352
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
353
     * @param object $instance
354
     * @param string $name
355
     * @param mixed $value
356
     * @return void
357
     */
358
    public function setPropertyValue(
359
        string $class,
360
        object $instance,
361
        string $name,
362
        mixed $value
363
    ): void {
364
        $reflection = $this->getPrivateProtectedAttribute($class, $name);
365
        $reflection->setValue($instance, $value);
366
    }
367
368
    /**
369
     * Test assert command expected given output
370
     * @param string $expected
371
     * @param string $output
372
     * @return void
373
     */
374
    public function assertCommandOutput(string $expected, string $output): void
375
    {
376
        $formattedExpected = preg_replace('~\r\n?~', "\n", $expected);
377
        $formattedOutput = preg_replace('~\r\n?~', "\n", $output);
378
379
        $this->assertEquals($formattedExpected, $formattedOutput);
380
    }
381
382
    /**
383
     * @codeCoverageIgnore
384
     * @return void
385
     */
386
    protected function tearDown(): void
387
    {
388
        //restore all mock variable global value to "false"
389
        foreach ($GLOBALS as $key => $value) {
390
            if (substr((string) $key, 0, 5) === 'mock_') {
391
                $GLOBALS[$key] = false;
392
            }
393
        }
394
    }
395
}
396