ci4

Форк
0
/
CIUnitTestCase.php 
529 строк · 13.2 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Test;
15

16
use CodeIgniter\CodeIgniter;
17
use CodeIgniter\Config\Factories;
18
use CodeIgniter\Database\BaseConnection;
19
use CodeIgniter\Database\MigrationRunner;
20
use CodeIgniter\Database\Seeder;
21
use CodeIgniter\Events\Events;
22
use CodeIgniter\Router\RouteCollection;
23
use CodeIgniter\Session\Handlers\ArrayHandler;
24
use CodeIgniter\Test\Mock\MockCache;
25
use CodeIgniter\Test\Mock\MockCodeIgniter;
26
use CodeIgniter\Test\Mock\MockEmail;
27
use CodeIgniter\Test\Mock\MockSession;
28
use Config\App;
29
use Config\Autoload;
30
use Config\Email;
31
use Config\Modules;
32
use Config\Services;
33
use Config\Session;
34
use Exception;
35
use PHPUnit\Framework\TestCase;
36

37
/**
38
 * Framework test case for PHPUnit.
39
 */
40
abstract class CIUnitTestCase extends TestCase
41
{
42
    use ReflectionHelper;
43

44
    /**
45
     * @var CodeIgniter
46
     */
47
    protected $app;
48

49
    /**
50
     * Methods to run during setUp.
51
     *
52
     * WARNING: Do not override unless you know exactly what you are doing.
53
     *          This property may be deprecated in the future.
54
     *
55
     * @var list<string> array of methods
56
     */
57
    protected $setUpMethods = [
58
        'resetFactories',
59
        'mockCache',
60
        'mockEmail',
61
        'mockSession',
62
    ];
63

64
    /**
65
     * Methods to run during tearDown.
66
     *
67
     * WARNING: This property may be deprecated in the future.
68
     *
69
     * @var list<string> array of methods
70
     */
71
    protected $tearDownMethods = [];
72

73
    /**
74
     * Store of identified traits.
75
     */
76
    private ?array $traits = null;
77

78
    // --------------------------------------------------------------------
79
    // Database Properties
80
    // --------------------------------------------------------------------
81

82
    /**
83
     * Should run db migration?
84
     *
85
     * @var bool
86
     */
87
    protected $migrate = true;
88

89
    /**
90
     * Should run db migration only once?
91
     *
92
     * @var bool
93
     */
94
    protected $migrateOnce = false;
95

96
    /**
97
     * Should run seeding only once?
98
     *
99
     * @var bool
100
     */
101
    protected $seedOnce = false;
102

103
    /**
104
     * Should the db be refreshed before test?
105
     *
106
     * @var bool
107
     */
108
    protected $refresh = true;
109

110
    /**
111
     * The seed file(s) used for all tests within this test case.
112
     * Should be fully-namespaced or relative to $basePath
113
     *
114
     * @var class-string<Seeder>|list<class-string<Seeder>>
115
     */
116
    protected $seed = '';
117

118
    /**
119
     * The path to the seeds directory.
120
     * Allows overriding the default application directories.
121
     *
122
     * @var string
123
     */
124
    protected $basePath = SUPPORTPATH . 'Database';
125

126
    /**
127
     * The namespace(s) to help us find the migration classes.
128
     * `null` is equivalent to running `spark migrate --all`.
129
     * Note that running "all" runs migrations in date order,
130
     * but specifying namespaces runs them in namespace order (then date)
131
     *
132
     * @var array|string|null
133
     */
134
    protected $namespace = 'Tests\Support';
135

136
    /**
137
     * The name of the database group to connect to.
138
     * If not present, will use the defaultGroup.
139
     *
140
     * @var non-empty-string
141
     */
142
    protected $DBGroup = 'tests';
143

144
    /**
145
     * Our database connection.
146
     *
147
     * @var BaseConnection
148
     */
149
    protected $db;
150

151
    /**
152
     * Migration Runner instance.
153
     *
154
     * @var MigrationRunner|null
155
     */
156
    protected $migrations;
157

158
    /**
159
     * Seeder instance
160
     *
161
     * @var Seeder
162
     */
163
    protected $seeder;
164

165
    /**
166
     * Stores information needed to remove any
167
     * rows inserted via $this->hasInDatabase();
168
     *
169
     * @var array
170
     */
171
    protected $insertCache = [];
172

173
    // --------------------------------------------------------------------
174
    // Feature Properties
175
    // --------------------------------------------------------------------
176

177
    /**
178
     * If present, will override application
179
     * routes when using call().
180
     *
181
     * @var RouteCollection|null
182
     */
183
    protected $routes;
184

185
    /**
186
     * Values to be set in the SESSION global
187
     * before running the test.
188
     *
189
     * @var array
190
     */
191
    protected $session = [];
192

193
    /**
194
     * Enabled auto clean op buffer after request call
195
     *
196
     * @var bool
197
     */
198
    protected $clean = true;
199

200
    /**
201
     * Custom request's headers
202
     *
203
     * @var array
204
     */
205
    protected $headers = [];
206

207
    /**
208
     * Allows for formatting the request body to what
209
     * the controller is going to expect
210
     *
211
     * @var string
212
     */
213
    protected $bodyFormat = '';
214

215
    /**
216
     * Allows for directly setting the body to what
217
     * it needs to be.
218
     *
219
     * @var mixed
220
     */
221
    protected $requestBody = '';
222

223
    // --------------------------------------------------------------------
224
    // Staging
225
    // --------------------------------------------------------------------
226

227
    /**
228
     * Load the helpers.
229
     */
230
    public static function setUpBeforeClass(): void
231
    {
232
        parent::setUpBeforeClass();
233

234
        helper(['url', 'test']);
235
    }
236

237
    protected function setUp(): void
238
    {
239
        parent::setUp();
240

241
        if (! $this->app) {
242
            $this->app = $this->createApplication();
243
        }
244

245
        foreach ($this->setUpMethods as $method) {
246
            $this->{$method}();
247
        }
248

249
        // Check for the database trait
250
        if (method_exists($this, 'setUpDatabase')) {
251
            $this->setUpDatabase();
252
        }
253

254
        // Check for other trait methods
255
        $this->callTraitMethods('setUp');
256
    }
257

258
    protected function tearDown(): void
259
    {
260
        parent::tearDown();
261

262
        foreach ($this->tearDownMethods as $method) {
263
            $this->{$method}();
264
        }
265

266
        // Check for the database trait
267
        if (method_exists($this, 'tearDownDatabase')) {
268
            $this->tearDownDatabase();
269
        }
270

271
        // Check for other trait methods
272
        $this->callTraitMethods('tearDown');
273
    }
274

275
    /**
276
     * Checks for traits with corresponding
277
     * methods for setUp or tearDown.
278
     *
279
     * @param string $stage 'setUp' or 'tearDown'
280
     */
281
    private function callTraitMethods(string $stage): void
282
    {
283
        if ($this->traits === null) {
284
            $this->traits = class_uses_recursive($this);
285
        }
286

287
        foreach ($this->traits as $trait) {
288
            $method = $stage . class_basename($trait);
289

290
            if (method_exists($this, $method)) {
291
                $this->{$method}();
292
            }
293
        }
294
    }
295

296
    // --------------------------------------------------------------------
297
    // Mocking
298
    // --------------------------------------------------------------------
299

300
    /**
301
     * Resets shared instanced for all Factories components
302
     */
303
    protected function resetFactories()
304
    {
305
        Factories::reset();
306
    }
307

308
    /**
309
     * Resets shared instanced for all Services
310
     */
311
    protected function resetServices(bool $initAutoloader = true)
312
    {
313
        Services::reset($initAutoloader);
314
    }
315

316
    /**
317
     * Injects the mock Cache driver to prevent filesystem collisions
318
     */
319
    protected function mockCache()
320
    {
321
        Services::injectMock('cache', new MockCache());
322
    }
323

324
    /**
325
     * Injects the mock email driver so no emails really send
326
     */
327
    protected function mockEmail()
328
    {
329
        Services::injectMock('email', new MockEmail(config(Email::class)));
330
    }
331

332
    /**
333
     * Injects the mock session driver into Services
334
     */
335
    protected function mockSession()
336
    {
337
        $_SESSION = [];
338

339
        $config  = config(Session::class);
340
        $session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config);
341

342
        Services::injectMock('session', $session);
343
    }
344

345
    // --------------------------------------------------------------------
346
    // Assertions
347
    // --------------------------------------------------------------------
348

349
    /**
350
     * Custom function to hook into CodeIgniter's Logging mechanism
351
     * to check if certain messages were logged during code execution.
352
     *
353
     * @param string|null $expectedMessage
354
     *
355
     * @return bool
356
     */
357
    public function assertLogged(string $level, $expectedMessage = null)
358
    {
359
        $result = TestLogger::didLog($level, $expectedMessage);
360

361
        $this->assertTrue($result, sprintf(
362
            'Failed asserting that expected message "%s" with level "%s" was logged.',
363
            $expectedMessage ?? '',
364
            $level
365
        ));
366

367
        return $result;
368
    }
369

370
    /**
371
     * Asserts that there is a log record that contains `$logMessage` in the message.
372
     */
373
    public function assertLogContains(string $level, string $logMessage, string $message = ''): void
374
    {
375
        $this->assertTrue(
376
            TestLogger::didLog($level, $logMessage, false),
377
            $message ?: sprintf(
378
                'Failed asserting that logs have a record of message containing "%s" with level "%s".',
379
                $logMessage,
380
                $level
381
            )
382
        );
383
    }
384

385
    /**
386
     * Hooks into CodeIgniter's Events system to check if a specific
387
     * event was triggered or not.
388
     *
389
     * @throws Exception
390
     */
391
    public function assertEventTriggered(string $eventName): bool
392
    {
393
        $found     = false;
394
        $eventName = strtolower($eventName);
395

396
        foreach (Events::getPerformanceLogs() as $log) {
397
            if ($log['event'] !== $eventName) {
398
                continue;
399
            }
400

401
            $found = true;
402
            break;
403
        }
404

405
        $this->assertTrue($found);
406

407
        return $found;
408
    }
409

410
    /**
411
     * Hooks into xdebug's headers capture, looking for presence of
412
     * a specific header emitted.
413
     *
414
     * @param string $header The leading portion of the header we are looking for
415
     */
416
    public function assertHeaderEmitted(string $header, bool $ignoreCase = false): void
417
    {
418
        $this->assertNotNull(
419
            $this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
420
            "Didn't find header for {$header}"
421
        );
422
    }
423

424
    /**
425
     * Hooks into xdebug's headers capture, looking for absence of
426
     * a specific header emitted.
427
     *
428
     * @param string $header The leading portion of the header we don't want to find
429
     */
430
    public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void
431
    {
432
        $this->assertNull(
433
            $this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
434
            "Found header for {$header}"
435
        );
436
    }
437

438
    /**
439
     * Custom function to test that two values are "close enough".
440
     * This is intended for extended execution time testing,
441
     * where the result is close but not exactly equal to the
442
     * expected time, for reasons beyond our control.
443
     *
444
     * @param float|int $actual
445
     *
446
     * @throws Exception
447
     */
448
    public function assertCloseEnough(int $expected, $actual, string $message = '', int $tolerance = 1)
449
    {
450
        $difference = abs($expected - (int) floor($actual));
451

452
        $this->assertLessThanOrEqual($tolerance, $difference, $message);
453
    }
454

455
    /**
456
     * Custom function to test that two values are "close enough".
457
     * This is intended for extended execution time testing,
458
     * where the result is close but not exactly equal to the
459
     * expected time, for reasons beyond our control.
460
     *
461
     * @param mixed $expected
462
     * @param mixed $actual
463
     *
464
     * @return bool|void
465
     *
466
     * @throws Exception
467
     */
468
    public function assertCloseEnoughString($expected, $actual, string $message = '', int $tolerance = 1)
469
    {
470
        $expected = (string) $expected;
471
        $actual   = (string) $actual;
472
        if (strlen($expected) !== strlen($actual)) {
473
            return false;
474
        }
475

476
        try {
477
            $expected   = (int) substr($expected, -2);
478
            $actual     = (int) substr($actual, -2);
479
            $difference = abs($expected - $actual);
480

481
            $this->assertLessThanOrEqual($tolerance, $difference, $message);
482
        } catch (Exception) {
483
            return false;
484
        }
485
    }
486

487
    // --------------------------------------------------------------------
488
    // Utility
489
    // --------------------------------------------------------------------
490

491
    /**
492
     * Loads up an instance of CodeIgniter
493
     * and gets the environment setup.
494
     *
495
     * @return CodeIgniter
496
     */
497
    protected function createApplication()
498
    {
499
        // Initialize the autoloader.
500
        service('autoloader')->initialize(new Autoload(), new Modules());
501

502
        $app = new MockCodeIgniter(new App());
503
        $app->initialize();
504

505
        return $app;
506
    }
507

508
    /**
509
     * Return first matching emitted header.
510
     */
511
    protected function getHeaderEmitted(string $header, bool $ignoreCase = false, string $method = __METHOD__): ?string
512
    {
513
        if (! function_exists('xdebug_get_headers')) {
514
            $this->markTestSkipped($method . '() requires xdebug.');
515
        }
516

517
        foreach (xdebug_get_headers() as $emittedHeader) {
518
            $found = $ignoreCase
519
                ? (stripos($emittedHeader, $header) === 0)
520
                : (str_starts_with($emittedHeader, $header));
521

522
            if ($found) {
523
                return $emittedHeader;
524
            }
525
        }
526

527
        return null;
528
    }
529
}
530

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.