3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Test;
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;
35
use PHPUnit\Framework\TestCase;
38
* Framework test case for PHPUnit.
40
abstract class CIUnitTestCase extends TestCase
50
* Methods to run during setUp.
52
* WARNING: Do not override unless you know exactly what you are doing.
53
* This property may be deprecated in the future.
55
* @var list<string> array of methods
57
protected $setUpMethods = [
65
* Methods to run during tearDown.
67
* WARNING: This property may be deprecated in the future.
69
* @var list<string> array of methods
71
protected $tearDownMethods = [];
74
* Store of identified traits.
76
private ?array $traits = null;
78
// --------------------------------------------------------------------
79
// Database Properties
80
// --------------------------------------------------------------------
83
* Should run db migration?
87
protected $migrate = true;
90
* Should run db migration only once?
94
protected $migrateOnce = false;
97
* Should run seeding only once?
101
protected $seedOnce = false;
104
* Should the db be refreshed before test?
108
protected $refresh = true;
111
* The seed file(s) used for all tests within this test case.
112
* Should be fully-namespaced or relative to $basePath
114
* @var class-string<Seeder>|list<class-string<Seeder>>
116
protected $seed = '';
119
* The path to the seeds directory.
120
* Allows overriding the default application directories.
124
protected $basePath = SUPPORTPATH . 'Database';
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)
132
* @var array|string|null
134
protected $namespace = 'Tests\Support';
137
* The name of the database group to connect to.
138
* If not present, will use the defaultGroup.
140
* @var non-empty-string
142
protected $DBGroup = 'tests';
145
* Our database connection.
147
* @var BaseConnection
152
* Migration Runner instance.
154
* @var MigrationRunner|null
156
protected $migrations;
166
* Stores information needed to remove any
167
* rows inserted via $this->hasInDatabase();
171
protected $insertCache = [];
173
// --------------------------------------------------------------------
174
// Feature Properties
175
// --------------------------------------------------------------------
178
* If present, will override application
179
* routes when using call().
181
* @var RouteCollection|null
186
* Values to be set in the SESSION global
187
* before running the test.
191
protected $session = [];
194
* Enabled auto clean op buffer after request call
198
protected $clean = true;
201
* Custom request's headers
205
protected $headers = [];
208
* Allows for formatting the request body to what
209
* the controller is going to expect
213
protected $bodyFormat = '';
216
* Allows for directly setting the body to what
221
protected $requestBody = '';
223
// --------------------------------------------------------------------
225
// --------------------------------------------------------------------
230
public static function setUpBeforeClass(): void
232
parent::setUpBeforeClass();
234
helper(['url', 'test']);
237
protected function setUp(): void
242
$this->app = $this->createApplication();
245
foreach ($this->setUpMethods as $method) {
249
// Check for the database trait
250
if (method_exists($this, 'setUpDatabase')) {
251
$this->setUpDatabase();
254
// Check for other trait methods
255
$this->callTraitMethods('setUp');
258
protected function tearDown(): void
262
foreach ($this->tearDownMethods as $method) {
266
// Check for the database trait
267
if (method_exists($this, 'tearDownDatabase')) {
268
$this->tearDownDatabase();
271
// Check for other trait methods
272
$this->callTraitMethods('tearDown');
276
* Checks for traits with corresponding
277
* methods for setUp or tearDown.
279
* @param string $stage 'setUp' or 'tearDown'
281
private function callTraitMethods(string $stage): void
283
if ($this->traits === null) {
284
$this->traits = class_uses_recursive($this);
287
foreach ($this->traits as $trait) {
288
$method = $stage . class_basename($trait);
290
if (method_exists($this, $method)) {
296
// --------------------------------------------------------------------
298
// --------------------------------------------------------------------
301
* Resets shared instanced for all Factories components
303
protected function resetFactories()
309
* Resets shared instanced for all Services
311
protected function resetServices(bool $initAutoloader = true)
313
Services::reset($initAutoloader);
317
* Injects the mock Cache driver to prevent filesystem collisions
319
protected function mockCache()
321
Services::injectMock('cache', new MockCache());
325
* Injects the mock email driver so no emails really send
327
protected function mockEmail()
329
Services::injectMock('email', new MockEmail(config(Email::class)));
333
* Injects the mock session driver into Services
335
protected function mockSession()
339
$config = config(Session::class);
340
$session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config);
342
Services::injectMock('session', $session);
345
// --------------------------------------------------------------------
347
// --------------------------------------------------------------------
350
* Custom function to hook into CodeIgniter's Logging mechanism
351
* to check if certain messages were logged during code execution.
353
* @param string|null $expectedMessage
357
public function assertLogged(string $level, $expectedMessage = null)
359
$result = TestLogger::didLog($level, $expectedMessage);
361
$this->assertTrue($result, sprintf(
362
'Failed asserting that expected message "%s" with level "%s" was logged.',
363
$expectedMessage ?? '',
371
* Asserts that there is a log record that contains `$logMessage` in the message.
373
public function assertLogContains(string $level, string $logMessage, string $message = ''): void
376
TestLogger::didLog($level, $logMessage, false),
378
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
386
* Hooks into CodeIgniter's Events system to check if a specific
387
* event was triggered or not.
391
public function assertEventTriggered(string $eventName): bool
394
$eventName = strtolower($eventName);
396
foreach (Events::getPerformanceLogs() as $log) {
397
if ($log['event'] !== $eventName) {
405
$this->assertTrue($found);
411
* Hooks into xdebug's headers capture, looking for presence of
412
* a specific header emitted.
414
* @param string $header The leading portion of the header we are looking for
416
public function assertHeaderEmitted(string $header, bool $ignoreCase = false): void
418
$this->assertNotNull(
419
$this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
420
"Didn't find header for {$header}"
425
* Hooks into xdebug's headers capture, looking for absence of
426
* a specific header emitted.
428
* @param string $header The leading portion of the header we don't want to find
430
public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void
433
$this->getHeaderEmitted($header, $ignoreCase, __METHOD__),
434
"Found header for {$header}"
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.
444
* @param float|int $actual
448
public function assertCloseEnough(int $expected, $actual, string $message = '', int $tolerance = 1)
450
$difference = abs($expected - (int) floor($actual));
452
$this->assertLessThanOrEqual($tolerance, $difference, $message);
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.
461
* @param mixed $expected
462
* @param mixed $actual
468
public function assertCloseEnoughString($expected, $actual, string $message = '', int $tolerance = 1)
470
$expected = (string) $expected;
471
$actual = (string) $actual;
472
if (strlen($expected) !== strlen($actual)) {
477
$expected = (int) substr($expected, -2);
478
$actual = (int) substr($actual, -2);
479
$difference = abs($expected - $actual);
481
$this->assertLessThanOrEqual($tolerance, $difference, $message);
482
} catch (Exception) {
487
// --------------------------------------------------------------------
489
// --------------------------------------------------------------------
492
* Loads up an instance of CodeIgniter
493
* and gets the environment setup.
495
* @return CodeIgniter
497
protected function createApplication()
499
// Initialize the autoloader.
500
service('autoloader')->initialize(new Autoload(), new Modules());
502
$app = new MockCodeIgniter(new App());
509
* Return first matching emitted header.
511
protected function getHeaderEmitted(string $header, bool $ignoreCase = false, string $method = __METHOD__): ?string
513
if (! function_exists('xdebug_get_headers')) {
514
$this->markTestSkipped($method . '() requires xdebug.');
517
foreach (xdebug_get_headers() as $emittedHeader) {
519
? (stripos($emittedHeader, $header) === 0)
520
: (str_starts_with($emittedHeader, $header));
523
return $emittedHeader;