Issues (9)

src/PlatineTestCase.php (9 issues)

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
63
/**
64
 * @class PlatineTestCase
65
 * @package Platine\Dev
66
 */
67
class PlatineTestCase extends TestCase
68
{
69
    /**
70
     * The class create object maps
71
     * @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...
72
     */
73
    protected array $createObjectMaps = [];
74
75
76
    /**
77
     * Set create object map
78
     * @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...
79
     * @param array<string, mixed> $maps
80
     * @return $this
81
     */
82
    public function setClassCreateObjectMaps(string $class, array $maps): self
83
    {
84
        $this->createObjectMaps[$class] = $maps;
85
86
        return $this;
87
    }
88
89
    /**
90
     * Create object
91
     * @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...
92
     * @return object|null
93
     */
94
    public function createObject(string $class): ?object
95
    {
96
        $rc = new ReflectionClass($class);
97
98
        if ($rc->isInstantiable() === false) {
99
            return null;
100
        }
101
102
        $constructor = $rc->getConstructor();
103
104
        if ($constructor === null) {
105
            return $rc->newInstanceWithoutConstructor();
106
        }
107
108
        $arguments = [];
109
        $parameters = $constructor->getParameters();
110
        $maps = $this->createObjectMaps[$class] ?? [];
111
        foreach ($parameters as /** @var ReflectionParameter $parameter */ $parameter) {
112
            $name = $parameter->getName();
113
            if (array_key_exists($name, $maps)) {
114
                $arguments[] = $maps[$name];
115
                continue;
116
            }
117
118
            /** @var ReflectionNamedType|null $type */
119
            $type = $parameter->getType();
120
            $value = null;
121
            if ($type !== null && $type->isBuiltin() === false) {
122
                // Create Mock
123
                /** @var class-string $className */
124
                $className = $type->getName();
125
                $value = $this->getMockInstance($className);
126
            }
127
128
            if ($value === null) {
129
                if ($parameter->isDefaultValueAvailable()) {
130
                    $value = $parameter->getDefaultValue();
131
                }
132
            }
133
134
            $arguments[] = $value;
135
        }
136
137
        return $rc->newInstanceArgs($arguments);
138
    }
139
140
    /**
141
     * Test object method call count
142
     * @param MockObject $object
143
     * @param string $method
144
     * @param int $count
145
     * @return void
146
     */
147
    public function expectMethodCallCount(MockObject $object, string $method, int $count = 1): void
148
    {
149
        $object->expects($this->exactly($count))
150
                ->method($method);
151
    }
152
153
    /**
154
     * Method to test private & protected method
155
     *
156
     * @param object $object the class instance to use
157
     * @param string $method the name of the method
158
     * @param array<int, mixed> $args the list of method arguments
159
     * @return mixed
160
     */
161
    public function runPrivateProtectedMethod(
162
        object $object,
163
        string $method,
164
        array $args = []
165
    ) {
166
        $reflection = new ReflectionObject($object);
167
        $reflectionMethod = $reflection->getMethod($method);
168
        $reflectionMethod->setAccessible(true);
169
        return $reflectionMethod->invokeArgs($object, $args);
170
    }
171
172
    /**
173
     * Method to set/get private & protected attribute
174
     *
175
     * @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...
176
     * @param string $attr the name of the class attribute
177
     */
178
    public function getPrivateProtectedAttribute(
179
        string $className,
180
        string $attr
181
    ): ReflectionProperty {
182
        $rProp = new ReflectionProperty($className, $attr);
183
        $rProp->setAccessible(true);
184
        return $rProp;
185
    }
186
187
    /**
188
     * Create virtual file with the given content
189
     * @param  string $filename
190
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator> $destination
191
     * @param  string $content
192
     * @return vfsStreamFile
193
     */
194
    public function createVfsFile(
195
        string $filename,
196
        vfsStreamContainer $destination,
197
        string $content = ''
198
    ): vfsStreamFile {
199
        return vfsStream::newFile($filename)
200
                        ->at($destination)
201
                        ->setContent($content);
202
    }
203
204
    /**
205
     * Create virtual file without content
206
     * @param  string $filename
207
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator> $destination
208
     * @return vfsStreamFile
209
     */
210
    public function createVfsFileOnly(
211
        string $filename,
212
        vfsStreamContainer $destination
213
    ): vfsStreamFile {
214
        return vfsStream::newFile($filename)
215
                        ->at($destination);
216
    }
217
218
    /**
219
     * Create virtual directory
220
     * @param  string $name
221
     * @param  vfsStreamContainer<\org\bovigo\vfs\vfsStreamContainerIterator>|null $destination
222
     * @return vfsStreamDirectory
223
     */
224
    public function createVfsDirectory(
225
        string $name,
226
        vfsStreamContainer $destination = null
227
    ): vfsStreamDirectory {
228
        if ($destination !== null) {
229
            return vfsStream::newDirectory($name)->at($destination);
230
        }
231
        return vfsStream::newDirectory($name);
232
    }
233
234
    /**
235
     * Return the list of methods to mocks in the parameters of PHPUnit::TestCase::getMock()
236
     *
237
     * @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...
238
     * @param string[] $exclude list of methods to exclude
239
     * @return string[]
240
     */
241
    public function getClassMethodsToMock(string|object $class, array $exclude = []): array
242
    {
243
        $methods = [];
244
245
        if (is_string($class) && !class_exists($class)) {
246
            throw new InvalidArgumentException(
247
                sprintf('Can not find class [%s]', $class)
248
            );
249
        }
250
251
        $reflectionClass = new ReflectionClass($class);
252
253
        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
254
            if (!in_array($reflectionMethod->name, $exclude)) {
255
                $methods[] = $reflectionMethod->name;
256
            }
257
        }
258
259
        return $methods;
260
    }
261
262
    /**
263
     * Get the instance of the given class
264
     * @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...
265
     * @param array<string, mixed> $mockMethods
266
     * @param array<int, string> $excludes
267
     * @return MockObject
268
     */
269
    public function getMockInstance(
270
        string $class,
271
        array $mockMethods = [],
272
        array $excludes = []
273
    ): MockObject {
274
        $methods = $this->getClassMethodsToMock($class, $excludes);
275
276
        $mock = $this->getMockBuilder($class)
277
                    ->disableOriginalConstructor()
278
                    ->onlyMethods($methods)
279
                    ->getMock();
280
281
        foreach ($mockMethods as $method => $returnValue) {
282
            $mock->expects($this->any())
283
                ->method($method)
284
                ->will($this->returnValue($returnValue));
285
        }
286
287
        return $mock;
288
    }
289
290
    /**
291
     * Get the instance of the given class using return map
292
     * @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...
293
     * @param array<string, mixed> $mockMethods
294
     * @param array<int, string> $excludes
295
     * @return MockObject
296
     */
297
    public function getMockInstanceMap(
298
        string $class,
299
        array $mockMethods = [],
300
        array $excludes = []
301
    ): MockObject {
302
        $methods = $this->getClassMethodsToMock($class, $excludes);
303
304
        $mock = $this->getMockBuilder($class)
305
                    ->disableOriginalConstructor()
306
                    ->onlyMethods($methods)
307
                    ->getMock();
308
309
        foreach ($mockMethods as $method => $returnValues) {
310
            $mock->expects($this->any())
311
                ->method($method)
312
                ->will(
313
                    $this->returnValueMap($returnValues)
314
                );
315
        }
316
317
        return $mock;
318
    }
319
320
    /**
321
     * Return the value of private or protected property
322
     * @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...
323
     * @param object $instance
324
     * @param string $name
325
     * @return mixed
326
     */
327
    public function getPropertyValue(string $class, object $instance, string $name): mixed
328
    {
329
        $reflection = $this->getPrivateProtectedAttribute($class, $name);
330
        return $reflection->getValue($instance);
331
    }
332
333
    /**
334
     * Set the value of private or protected property
335
     * @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...
336
     * @param object $instance
337
     * @param string $name
338
     * @param mixed $value
339
     * @return void
340
     */
341
    public function setPropertyValue(
342
        string $class,
343
        object $instance,
344
        string $name,
345
        mixed $value
346
    ): void {
347
        $reflection = $this->getPrivateProtectedAttribute($class, $name);
348
        $reflection->setValue($instance, $value);
349
    }
350
351
    /**
352
     * Test assert command expected given output
353
     * @param string $expected
354
     * @param string $output
355
     * @return void
356
     */
357
    public function assertCommandOutput(string $expected, string $output): void
358
    {
359
        $formattedExpected = preg_replace('~\r\n?~', "\n", $expected);
360
        $formattedOutput = preg_replace('~\r\n?~', "\n", $output);
361
362
        $this->assertEquals($formattedExpected, $formattedOutput);
363
    }
364
365
    /**
366
     * @codeCoverageIgnore
367
     * @return void
368
     */
369
    protected function tearDown(): void
370
    {
371
        //restore all mock variable global value to "false"
372
        foreach ($GLOBALS as $key => $value) {
373
            if (substr((string) $key, 0, 5) === 'mock_') {
374
                $GLOBALS[$key] = false;
375
            }
376
        }
377
    }
378
}
379