• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 14569795065

21 Apr 2025 07:55AM UTC coverage: 84.402% (+0.007%) from 84.395%
14569795065

Pull #9528

github

web-flow
Merge 4ad1f19d8 into 3d3ba0512
Pull Request #9528: feat: add Time::addCalendarMonths() function

11 of 11 new or added lines in 1 file covered. (100.0%)

136 existing lines in 21 files now uncovered.

20827 of 24676 relevant lines covered (84.4%)

191.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.73
/system/Test/CIUnitTestCase.php
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();
239✔
233

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

237
    protected function setUp(): void
238
    {
239
        parent::setUp();
6,590✔
240

241
        if (! $this->app instanceof CodeIgniter) {
6,590✔
242
            $this->app = $this->createApplication();
6,590✔
243
        }
244

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

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

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

258
    protected function tearDown(): void
259
    {
260
        parent::tearDown();
6,496✔
261

262
        foreach ($this->tearDownMethods as $method) {
6,496✔
263
            $this->{$method}();
×
264
        }
265

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

271
        // Check for other trait methods
272
        $this->callTraitMethods('tearDown');
6,496✔
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) {
6,802✔
284
            $this->traits = class_uses_recursive($this);
6,802✔
285
        }
286

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

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

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

300
    /**
301
     * Resets shared instanced for all Factories components
302
     *
303
     * @return void
304
     */
305
    protected function resetFactories()
306
    {
307
        Factories::reset();
6,590✔
308
    }
309

310
    /**
311
     * Resets shared instanced for all Services
312
     *
313
     * @return void
314
     */
315
    protected function resetServices(bool $initAutoloader = true)
316
    {
317
        Services::reset($initAutoloader);
1,507✔
318
    }
319

320
    /**
321
     * Injects the mock Cache driver to prevent filesystem collisions
322
     *
323
     * @return void
324
     */
325
    protected function mockCache()
326
    {
327
        Services::injectMock('cache', new MockCache());
6,590✔
328
    }
329

330
    /**
331
     * Injects the mock email driver so no emails really send
332
     *
333
     * @return void
334
     */
335
    protected function mockEmail()
336
    {
337
        Services::injectMock('email', new MockEmail(config(Email::class)));
6,590✔
338
    }
339

340
    /**
341
     * Injects the mock session driver into Services
342
     *
343
     * @return void
344
     */
345
    protected function mockSession()
346
    {
347
        $_SESSION = [];
6,590✔
348

349
        $config  = config(Session::class);
6,590✔
350
        $session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config);
6,590✔
351

352
        Services::injectMock('session', $session);
6,590✔
353
    }
354

355
    // --------------------------------------------------------------------
356
    // Assertions
357
    // --------------------------------------------------------------------
358

359
    /**
360
     * Custom function to hook into CodeIgniter's Logging mechanism
361
     * to check if certain messages were logged during code execution.
362
     *
363
     * @param string|null $expectedMessage
364
     *
365
     * @return bool
366
     */
367
    public function assertLogged(string $level, $expectedMessage = null)
368
    {
369
        $result = TestLogger::didLog($level, $expectedMessage);
16✔
370

371
        $this->assertTrue($result, sprintf(
16✔
372
            'Failed asserting that expected message "%s" with level "%s" was logged.',
16✔
373
            $expectedMessage ?? '',
16✔
374
            $level,
16✔
375
        ));
16✔
376

377
        return $result;
16✔
378
    }
379

380
    /**
381
     * Asserts that there is a log record that contains `$logMessage` in the message.
382
     */
383
    public function assertLogContains(string $level, string $logMessage, string $message = ''): void
384
    {
385
        $this->assertTrue(
3✔
386
            TestLogger::didLog($level, $logMessage, false),
3✔
387
            $message !== '' ? $message : sprintf(
3✔
388
                'Failed asserting that logs have a record of message containing "%s" with level "%s".',
3✔
389
                $logMessage,
3✔
390
                $level,
3✔
391
            ),
3✔
392
        );
3✔
393
    }
394

395
    /**
396
     * Hooks into CodeIgniter's Events system to check if a specific
397
     * event was triggered or not.
398
     *
399
     * @throws Exception
400
     */
401
    public function assertEventTriggered(string $eventName): bool
402
    {
403
        $found     = false;
3✔
404
        $eventName = strtolower($eventName);
3✔
405

406
        foreach (Events::getPerformanceLogs() as $log) {
3✔
407
            if ($log['event'] !== $eventName) {
3✔
408
                continue;
2✔
409
            }
410

411
            $found = true;
3✔
412
            break;
3✔
413
        }
414

415
        $this->assertTrue($found);
3✔
416

417
        return $found;
3✔
418
    }
419

420
    /**
421
     * Hooks into xdebug's headers capture, looking for presence of
422
     * a specific header emitted.
423
     *
424
     * @param string $header The leading portion of the header we are looking for
425
     */
426
    public function assertHeaderEmitted(string $header, bool $ignoreCase = false): void
427
    {
428
        $this->assertNotNull(
9✔
429
            $this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
9✔
430
            "Didn't find header for {$header}",
9✔
431
        );
9✔
432
    }
433

434
    /**
435
     * Hooks into xdebug's headers capture, looking for absence of
436
     * a specific header emitted.
437
     *
438
     * @param string $header The leading portion of the header we don't want to find
439
     */
440
    public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void
441
    {
442
        $this->assertNull(
3✔
443
            $this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
3✔
444
            "Found header for {$header}",
3✔
445
        );
3✔
446
    }
447

448
    /**
449
     * Custom function to test that two values are "close enough".
450
     * This is intended for extended execution time testing,
451
     * where the result is close but not exactly equal to the
452
     * expected time, for reasons beyond our control.
453
     *
454
     * @param float|int $actual
455
     *
456
     * @return void
457
     *
458
     * @throws Exception
459
     */
460
    public function assertCloseEnough(int $expected, $actual, string $message = '', int $tolerance = 1)
461
    {
462
        $difference = abs($expected - (int) floor($actual));
8✔
463

464
        $this->assertLessThanOrEqual($tolerance, $difference, $message);
8✔
465
    }
466

467
    /**
468
     * Custom function to test that two values are "close enough".
469
     * This is intended for extended execution time testing,
470
     * where the result is close but not exactly equal to the
471
     * expected time, for reasons beyond our control.
472
     *
473
     * @param mixed $expected
474
     * @param mixed $actual
475
     *
476
     * @return bool|null
477
     *
478
     * @throws Exception
479
     */
480
    public function assertCloseEnoughString($expected, $actual, string $message = '', int $tolerance = 1)
481
    {
482
        $expected = (string) $expected;
18✔
483
        $actual   = (string) $actual;
18✔
484
        if (strlen($expected) !== strlen($actual)) {
18✔
485
            return false;
1✔
486
        }
487

488
        try {
489
            $expected   = (int) substr($expected, -2);
17✔
490
            $actual     = (int) substr($actual, -2);
17✔
491
            $difference = abs($expected - $actual);
17✔
492

493
            $this->assertLessThanOrEqual($tolerance, $difference, $message);
17✔
494
        } catch (Exception) {
1✔
495
            return false;
1✔
496
        }
497

498
        return null;
17✔
499
    }
500

501
    // --------------------------------------------------------------------
502
    // Utility
503
    // --------------------------------------------------------------------
504

505
    /**
506
     * Loads up an instance of CodeIgniter
507
     * and gets the environment setup.
508
     *
509
     * @return CodeIgniter
510
     */
511
    protected function createApplication()
512
    {
513
        // Initialize the autoloader.
514
        service('autoloader')->initialize(new Autoload(), new Modules());
6,590✔
515

516
        $app = new MockCodeIgniter(new App());
6,590✔
517
        $app->initialize();
6,590✔
518

519
        return $app;
6,590✔
520
    }
521

522
    /**
523
     * Return first matching emitted header.
524
     */
525
    protected function getHeaderEmitted(string $header, bool $ignoreCase = false, string $method = __METHOD__): ?string
526
    {
527
        if (! function_exists('xdebug_get_headers')) {
39✔
UNCOV
528
            $this->markTestSkipped($method . '() requires xdebug.');
×
529
        }
530

531
        foreach (xdebug_get_headers() as $emittedHeader) {
39✔
532
            $found = $ignoreCase
39✔
533
                ? (str_starts_with(strtolower($emittedHeader), strtolower($header)))
4✔
534
                : (str_starts_with($emittedHeader, $header));
36✔
535

536
            if ($found) {
39✔
537
                return $emittedHeader;
36✔
538
            }
539
        }
540

541
        return null;
4✔
542
    }
543
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc