You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
13 KiB
527 lines
13 KiB
<?php |
|
|
|
/** |
|
* This file is part of CodeIgniter 4 framework. |
|
* |
|
* (c) CodeIgniter Foundation <admin@codeigniter.com> |
|
* |
|
* For the full copyright and license information, please view |
|
* the LICENSE file that was distributed with this source code. |
|
*/ |
|
|
|
namespace CodeIgniter\Test; |
|
|
|
use CodeIgniter\CodeIgniter; |
|
use CodeIgniter\Config\Factories; |
|
use CodeIgniter\Database\BaseConnection; |
|
use CodeIgniter\Database\MigrationRunner; |
|
use CodeIgniter\Database\Seeder; |
|
use CodeIgniter\Events\Events; |
|
use CodeIgniter\Router\RouteCollection; |
|
use CodeIgniter\Session\Handlers\ArrayHandler; |
|
use CodeIgniter\Test\Mock\MockCache; |
|
use CodeIgniter\Test\Mock\MockCodeIgniter; |
|
use CodeIgniter\Test\Mock\MockEmail; |
|
use CodeIgniter\Test\Mock\MockSession; |
|
use Config\App; |
|
use Config\Autoload; |
|
use Config\Email; |
|
use Config\Modules; |
|
use Config\Services; |
|
use Config\Session; |
|
use Exception; |
|
use PHPUnit\Framework\TestCase; |
|
|
|
/** |
|
* Framework test case for PHPUnit. |
|
*/ |
|
abstract class CIUnitTestCase extends TestCase |
|
{ |
|
use ReflectionHelper; |
|
|
|
/** |
|
* @var CodeIgniter |
|
*/ |
|
protected $app; |
|
|
|
/** |
|
* Methods to run during setUp. |
|
* |
|
* WARNING: Do not override unless you know exactly what you are doing. |
|
* This property may be deprecated in the future. |
|
* |
|
* @var list<string> array of methods |
|
*/ |
|
protected $setUpMethods = [ |
|
'resetFactories', |
|
'mockCache', |
|
'mockEmail', |
|
'mockSession', |
|
]; |
|
|
|
/** |
|
* Methods to run during tearDown. |
|
* |
|
* WARNING: This property may be deprecated in the future. |
|
* |
|
* @var list<string> array of methods |
|
*/ |
|
protected $tearDownMethods = []; |
|
|
|
/** |
|
* Store of identified traits. |
|
*/ |
|
private ?array $traits = null; |
|
|
|
// -------------------------------------------------------------------- |
|
// Database Properties |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Should run db migration? |
|
* |
|
* @var bool |
|
*/ |
|
protected $migrate = true; |
|
|
|
/** |
|
* Should run db migration only once? |
|
* |
|
* @var bool |
|
*/ |
|
protected $migrateOnce = false; |
|
|
|
/** |
|
* Should run seeding only once? |
|
* |
|
* @var bool |
|
*/ |
|
protected $seedOnce = false; |
|
|
|
/** |
|
* Should the db be refreshed before test? |
|
* |
|
* @var bool |
|
*/ |
|
protected $refresh = true; |
|
|
|
/** |
|
* The seed file(s) used for all tests within this test case. |
|
* Should be fully-namespaced or relative to $basePath |
|
* |
|
* @var class-string<Seeder>|list<class-string<Seeder>> |
|
*/ |
|
protected $seed = ''; |
|
|
|
/** |
|
* The path to the seeds directory. |
|
* Allows overriding the default application directories. |
|
* |
|
* @var string |
|
*/ |
|
protected $basePath = SUPPORTPATH . 'Database'; |
|
|
|
/** |
|
* The namespace(s) to help us find the migration classes. |
|
* `null` is equivalent to running `spark migrate --all`. |
|
* Note that running "all" runs migrations in date order, |
|
* but specifying namespaces runs them in namespace order (then date) |
|
* |
|
* @var array|string|null |
|
*/ |
|
protected $namespace = 'Tests\Support'; |
|
|
|
/** |
|
* The name of the database group to connect to. |
|
* If not present, will use the defaultGroup. |
|
* |
|
* @var non-empty-string |
|
*/ |
|
protected $DBGroup = 'tests'; |
|
|
|
/** |
|
* Our database connection. |
|
* |
|
* @var BaseConnection |
|
*/ |
|
protected $db; |
|
|
|
/** |
|
* Migration Runner instance. |
|
* |
|
* @var MigrationRunner|null |
|
*/ |
|
protected $migrations; |
|
|
|
/** |
|
* Seeder instance |
|
* |
|
* @var Seeder |
|
*/ |
|
protected $seeder; |
|
|
|
/** |
|
* Stores information needed to remove any |
|
* rows inserted via $this->hasInDatabase(); |
|
* |
|
* @var array |
|
*/ |
|
protected $insertCache = []; |
|
|
|
// -------------------------------------------------------------------- |
|
// Feature Properties |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* If present, will override application |
|
* routes when using call(). |
|
* |
|
* @var RouteCollection|null |
|
*/ |
|
protected $routes; |
|
|
|
/** |
|
* Values to be set in the SESSION global |
|
* before running the test. |
|
* |
|
* @var array |
|
*/ |
|
protected $session = []; |
|
|
|
/** |
|
* Enabled auto clean op buffer after request call |
|
* |
|
* @var bool |
|
*/ |
|
protected $clean = true; |
|
|
|
/** |
|
* Custom request's headers |
|
* |
|
* @var array |
|
*/ |
|
protected $headers = []; |
|
|
|
/** |
|
* Allows for formatting the request body to what |
|
* the controller is going to expect |
|
* |
|
* @var string |
|
*/ |
|
protected $bodyFormat = ''; |
|
|
|
/** |
|
* Allows for directly setting the body to what |
|
* it needs to be. |
|
* |
|
* @var mixed |
|
*/ |
|
protected $requestBody = ''; |
|
|
|
// -------------------------------------------------------------------- |
|
// Staging |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Load the helpers. |
|
*/ |
|
public static function setUpBeforeClass(): void |
|
{ |
|
parent::setUpBeforeClass(); |
|
|
|
helper(['url', 'test']); |
|
} |
|
|
|
protected function setUp(): void |
|
{ |
|
parent::setUp(); |
|
|
|
if (! $this->app) { |
|
$this->app = $this->createApplication(); |
|
} |
|
|
|
foreach ($this->setUpMethods as $method) { |
|
$this->{$method}(); |
|
} |
|
|
|
// Check for the database trait |
|
if (method_exists($this, 'setUpDatabase')) { |
|
$this->setUpDatabase(); |
|
} |
|
|
|
// Check for other trait methods |
|
$this->callTraitMethods('setUp'); |
|
} |
|
|
|
protected function tearDown(): void |
|
{ |
|
parent::tearDown(); |
|
|
|
foreach ($this->tearDownMethods as $method) { |
|
$this->{$method}(); |
|
} |
|
|
|
// Check for the database trait |
|
if (method_exists($this, 'tearDownDatabase')) { |
|
$this->tearDownDatabase(); |
|
} |
|
|
|
// Check for other trait methods |
|
$this->callTraitMethods('tearDown'); |
|
} |
|
|
|
/** |
|
* Checks for traits with corresponding |
|
* methods for setUp or tearDown. |
|
* |
|
* @param string $stage 'setUp' or 'tearDown' |
|
*/ |
|
private function callTraitMethods(string $stage): void |
|
{ |
|
if ($this->traits === null) { |
|
$this->traits = class_uses_recursive($this); |
|
} |
|
|
|
foreach ($this->traits as $trait) { |
|
$method = $stage . class_basename($trait); |
|
|
|
if (method_exists($this, $method)) { |
|
$this->{$method}(); |
|
} |
|
} |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Mocking |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Resets shared instanced for all Factories components |
|
*/ |
|
protected function resetFactories() |
|
{ |
|
Factories::reset(); |
|
} |
|
|
|
/** |
|
* Resets shared instanced for all Services |
|
*/ |
|
protected function resetServices(bool $initAutoloader = true) |
|
{ |
|
Services::reset($initAutoloader); |
|
} |
|
|
|
/** |
|
* Injects the mock Cache driver to prevent filesystem collisions |
|
*/ |
|
protected function mockCache() |
|
{ |
|
Services::injectMock('cache', new MockCache()); |
|
} |
|
|
|
/** |
|
* Injects the mock email driver so no emails really send |
|
*/ |
|
protected function mockEmail() |
|
{ |
|
Services::injectMock('email', new MockEmail(config(Email::class))); |
|
} |
|
|
|
/** |
|
* Injects the mock session driver into Services |
|
*/ |
|
protected function mockSession() |
|
{ |
|
$_SESSION = []; |
|
|
|
$config = config(Session::class); |
|
$session = new MockSession(new ArrayHandler($config, '0.0.0.0'), $config); |
|
|
|
Services::injectMock('session', $session); |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Assertions |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Custom function to hook into CodeIgniter's Logging mechanism |
|
* to check if certain messages were logged during code execution. |
|
* |
|
* @param string|null $expectedMessage |
|
* |
|
* @return bool |
|
*/ |
|
public function assertLogged(string $level, $expectedMessage = null) |
|
{ |
|
$result = TestLogger::didLog($level, $expectedMessage); |
|
|
|
$this->assertTrue($result, sprintf( |
|
'Failed asserting that expected message "%s" with level "%s" was logged.', |
|
$expectedMessage ?? '', |
|
$level |
|
)); |
|
|
|
return $result; |
|
} |
|
|
|
/** |
|
* Asserts that there is a log record that contains `$logMessage` in the message. |
|
*/ |
|
public function assertLogContains(string $level, string $logMessage, string $message = ''): void |
|
{ |
|
$this->assertTrue( |
|
TestLogger::didLog($level, $logMessage, false), |
|
$message ?: sprintf( |
|
'Failed asserting that logs have a record of message containing "%s" with level "%s".', |
|
$logMessage, |
|
$level |
|
) |
|
); |
|
} |
|
|
|
/** |
|
* Hooks into CodeIgniter's Events system to check if a specific |
|
* event was triggered or not. |
|
* |
|
* @throws Exception |
|
*/ |
|
public function assertEventTriggered(string $eventName): bool |
|
{ |
|
$found = false; |
|
$eventName = strtolower($eventName); |
|
|
|
foreach (Events::getPerformanceLogs() as $log) { |
|
if ($log['event'] !== $eventName) { |
|
continue; |
|
} |
|
|
|
$found = true; |
|
break; |
|
} |
|
|
|
$this->assertTrue($found); |
|
|
|
return $found; |
|
} |
|
|
|
/** |
|
* Hooks into xdebug's headers capture, looking for presence of |
|
* a specific header emitted. |
|
* |
|
* @param string $header The leading portion of the header we are looking for |
|
*/ |
|
public function assertHeaderEmitted(string $header, bool $ignoreCase = false): void |
|
{ |
|
$this->assertNotNull( |
|
$this->getHeaderEmitted($header, $ignoreCase, __METHOD__), |
|
"Didn't find header for {$header}" |
|
); |
|
} |
|
|
|
/** |
|
* Hooks into xdebug's headers capture, looking for absence of |
|
* a specific header emitted. |
|
* |
|
* @param string $header The leading portion of the header we don't want to find |
|
*/ |
|
public function assertHeaderNotEmitted(string $header, bool $ignoreCase = false): void |
|
{ |
|
$this->assertNull( |
|
$this->getHeaderEmitted($header, $ignoreCase, __METHOD__), |
|
"Found header for {$header}" |
|
); |
|
} |
|
|
|
/** |
|
* Custom function to test that two values are "close enough". |
|
* This is intended for extended execution time testing, |
|
* where the result is close but not exactly equal to the |
|
* expected time, for reasons beyond our control. |
|
* |
|
* @param mixed $actual |
|
* |
|
* @throws Exception |
|
*/ |
|
public function assertCloseEnough(int $expected, $actual, string $message = '', int $tolerance = 1) |
|
{ |
|
$difference = abs($expected - (int) floor($actual)); |
|
|
|
$this->assertLessThanOrEqual($tolerance, $difference, $message); |
|
} |
|
|
|
/** |
|
* Custom function to test that two values are "close enough". |
|
* This is intended for extended execution time testing, |
|
* where the result is close but not exactly equal to the |
|
* expected time, for reasons beyond our control. |
|
* |
|
* @param mixed $expected |
|
* @param mixed $actual |
|
* |
|
* @return bool|void |
|
* |
|
* @throws Exception |
|
*/ |
|
public function assertCloseEnoughString($expected, $actual, string $message = '', int $tolerance = 1) |
|
{ |
|
$expected = (string) $expected; |
|
$actual = (string) $actual; |
|
if (strlen($expected) !== strlen($actual)) { |
|
return false; |
|
} |
|
|
|
try { |
|
$expected = (int) substr($expected, -2); |
|
$actual = (int) substr($actual, -2); |
|
$difference = abs($expected - $actual); |
|
|
|
$this->assertLessThanOrEqual($tolerance, $difference, $message); |
|
} catch (Exception $e) { |
|
return false; |
|
} |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Utility |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Loads up an instance of CodeIgniter |
|
* and gets the environment setup. |
|
* |
|
* @return CodeIgniter |
|
*/ |
|
protected function createApplication() |
|
{ |
|
// Initialize the autoloader. |
|
Services::autoloader()->initialize(new Autoload(), new Modules()); |
|
|
|
$app = new MockCodeIgniter(new App()); |
|
$app->initialize(); |
|
|
|
return $app; |
|
} |
|
|
|
/** |
|
* Return first matching emitted header. |
|
*/ |
|
protected function getHeaderEmitted(string $header, bool $ignoreCase = false, string $method = __METHOD__): ?string |
|
{ |
|
if (! function_exists('xdebug_get_headers')) { |
|
$this->markTestSkipped($method . '() requires xdebug.'); |
|
} |
|
|
|
foreach (xdebug_get_headers() as $emittedHeader) { |
|
$found = $ignoreCase |
|
? (stripos($emittedHeader, $header) === 0) |
|
: (strpos($emittedHeader, $header) === 0); |
|
|
|
if ($found) { |
|
return $emittedHeader; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
}
|
|
|