Skip to content

Commit c084a01

Browse files
committed
Add unit tests for framework classes
1 parent fed5256 commit c084a01

13 files changed

+749
-1
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Model.php';
7+
require_once RELATIVE_PATH . 'framework/Bean.php';
8+
require_once RELATIVE_PATH . 'framework/BeanAdapter.php';
9+
10+
class FakeBeanImplementation
11+
{
12+
public $calls = [];
13+
14+
public function select(...$args)
15+
{
16+
$this->calls[] = ['select', $args];
17+
return 'select:' . implode('-', $args);
18+
}
19+
20+
public function insert()
21+
{
22+
$this->calls[] = ['insert', []];
23+
return 'inserted';
24+
}
25+
26+
public function delete(...$args)
27+
{
28+
$this->calls[] = ['delete', $args];
29+
return 'delete:' . implode('-', $args);
30+
}
31+
32+
public function update(...$args)
33+
{
34+
$this->calls[] = ['update', $args];
35+
return 'update:' . implode('-', $args);
36+
}
37+
38+
public function updateCurrent()
39+
{
40+
$this->calls[] = ['updateCurrent', []];
41+
return 'update-current';
42+
}
43+
44+
public function isSqlError()
45+
{
46+
$this->calls[] = ['isSqlError', []];
47+
return false;
48+
}
49+
50+
public function lastSqlError()
51+
{
52+
$this->calls[] = ['lastSqlError', []];
53+
return '';
54+
}
55+
}
56+
57+
final class BeanAdapterTest extends TestCase
58+
{
59+
private function createAdapter(FakeBeanImplementation $bean): \framework\BeanAdapter
60+
{
61+
$reflection = new ReflectionClass(\framework\BeanAdapter::class);
62+
/** @var \framework\BeanAdapter $adapter */
63+
$adapter = $reflection->newInstanceWithoutConstructor();
64+
$property = $reflection->getProperty('bean');
65+
$property->setAccessible(true);
66+
$property->setValue($adapter, $bean);
67+
68+
return $adapter;
69+
}
70+
71+
public function testAdapterDelegatesCallsToUnderlyingBean(): void
72+
{
73+
$bean = new FakeBeanImplementation();
74+
$adapter = $this->createAdapter($bean);
75+
76+
$this->assertSame('select:10', $adapter->select(10));
77+
$this->assertSame('select:10-20', $adapter->select([10, 20]));
78+
$this->assertSame('inserted', $adapter->insert());
79+
$this->assertSame('delete:5', $adapter->delete(5));
80+
$this->assertSame('delete:7-8', $adapter->delete([7, 8]));
81+
$this->assertSame('update:3', $adapter->update(3));
82+
$this->assertSame('update:4-5', $adapter->update([4, 5]));
83+
$this->assertSame('update-current', $adapter->updateCurrent());
84+
$this->assertFalse($adapter->isSqlError());
85+
$this->assertSame('', $adapter->lastSqlError());
86+
87+
$recorded = array_column($bean->calls, 0);
88+
$this->assertContains('select', $recorded);
89+
$this->assertContains('insert', $recorded);
90+
$this->assertContains('delete', $recorded);
91+
$this->assertContains('update', $recorded);
92+
$this->assertContains('updateCurrent', $recorded);
93+
$this->assertContains('isSqlError', $recorded);
94+
$this->assertContains('lastSqlError', $recorded);
95+
}
96+
}

tests/framework/BeanTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Bean.php';
7+
8+
final class BeanTest extends TestCase
9+
{
10+
public function testBeanInterfaceDefinesExpectedMethods(): void
11+
{
12+
$reflection = new ReflectionClass(\framework\Bean::class);
13+
14+
$this->assertTrue($reflection->isInterface());
15+
$expectedMethods = ['select', 'insert', 'delete', 'update', 'updateCurrent', 'isSqlError', 'lastSqlError'];
16+
17+
foreach ($expectedMethods as $method) {
18+
$this->assertTrue($reflection->hasMethod($method));
19+
}
20+
}
21+
}

tests/framework/BeanUserTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/BeanUser.php';
7+
8+
final class BeanUserTest extends TestCase
9+
{
10+
public function testBeanUserInterfaceDefinesAccessors(): void
11+
{
12+
$reflection = new ReflectionClass(\framework\BeanUser::class);
13+
14+
$this->assertTrue($reflection->isInterface());
15+
$expectedMethods = ['getId', 'getEmail', 'getPassword', 'getRole', 'getToken', 'getTokenTimeStamp'];
16+
17+
foreach ($expectedMethods as $method) {
18+
$this->assertTrue($reflection->hasMethod($method));
19+
}
20+
}
21+
}

tests/framework/ControllerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use framework\View;
77

88
// Define minimal configuration constants required by the framework.
9-
$rootPath = realpath(__DIR__ . '/tests');
9+
$rootPath = realpath(__DIR__ . '/..');
1010
$projectRoot = dirname($rootPath);
1111

1212
if (!defined('RELATIVE_PATH')) {

tests/framework/DispatcherTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Loader.php';
7+
require_once RELATIVE_PATH . 'framework/Model.php';
8+
require_once RELATIVE_PATH . 'framework/View.php';
9+
require_once RELATIVE_PATH . 'framework/Controller.php';
10+
require_once RELATIVE_PATH . 'framework/Dispatcher.php';
11+
12+
final class DispatcherTest extends TestCase
13+
{
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
$_GET = [];
18+
$_SESSION = [];
19+
}
20+
21+
private function getPrivateProperty(object $object, string $property)
22+
{
23+
$reflection = new ReflectionClass($object);
24+
$prop = $reflection->getProperty($property);
25+
$prop->setAccessible(true);
26+
27+
return $prop->getValue($object);
28+
}
29+
30+
private function invokePrivateMethod(object $object, string $method, array $args = [])
31+
{
32+
$reflection = new ReflectionClass($object);
33+
$methodRef = $reflection->getMethod($method);
34+
$methodRef->setAccessible(true);
35+
36+
return $methodRef->invokeArgs($object, $args);
37+
}
38+
39+
public function testDispatcherParsesUrlSegmentsIntoProperties(): void
40+
{
41+
$_GET['url'] = 'example/show/42';
42+
43+
$dispatcher = new \framework\Dispatcher();
44+
45+
$this->assertSame('controllers\\example', $this->getPrivateProperty($dispatcher, 'controllerClass'));
46+
$this->assertSame('show', $this->getPrivateProperty($dispatcher, 'method'));
47+
$this->assertSame(['42'], $this->getPrivateProperty($dispatcher, 'methodParameters'));
48+
$this->assertSame('example', $this->getPrivateProperty($dispatcher, 'controllerSEOClassName'));
49+
$this->assertSame('', $this->getPrivateProperty($dispatcher, 'currentSubSystem'));
50+
$this->assertArrayHasKey('example', $_SESSION);
51+
}
52+
53+
public function testDispatcherSupportsSubsystems(): void
54+
{
55+
$_GET['url'] = 'sub/report/list';
56+
57+
$dispatcher = new \framework\Dispatcher();
58+
59+
$this->assertSame('controllers\\sub\\report', $this->getPrivateProperty($dispatcher, 'controllerClass'));
60+
$this->assertSame('list', $this->getPrivateProperty($dispatcher, 'method'));
61+
$this->assertSame([], $this->getPrivateProperty($dispatcher, 'methodParameters'));
62+
$this->assertSame('report', $this->getPrivateProperty($dispatcher, 'controllerSEOClassName'));
63+
$this->assertArrayHasKey('report', $_SESSION);
64+
}
65+
66+
public function testUnderscoreToCamelCaseConvertsSeoNames(): void
67+
{
68+
unset($_GET['url']);
69+
$dispatcher = new \framework\Dispatcher();
70+
71+
$result = $this->invokePrivateMethod($dispatcher, 'underscoreToCamelCase', ['user_profile']);
72+
73+
$this->assertSame('userProfile', $result);
74+
}
75+
}

tests/framework/LoaderTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Loader.php';
7+
8+
final class LoaderTest extends TestCase
9+
{
10+
protected function setUp(): void
11+
{
12+
parent::setUp();
13+
$_SESSION = [];
14+
}
15+
16+
public function testGetDirectoriesMergesSubsystemPaths(): void
17+
{
18+
$directories = \framework\Loader::getDirectories();
19+
20+
$this->assertContains(APP_CONTROLLERS_PATH . DIRECTORY_SEPARATOR . 'sub', $directories);
21+
$this->assertContains(APP_VIEWS_PATH . DIRECTORY_SEPARATOR . 'sub', $directories);
22+
$this->assertContains(APP_MODELS_PATH . DIRECTORY_SEPARATOR . 'sub', $directories);
23+
$this->assertContains('framework', $directories);
24+
}
25+
26+
public function testGetCurrentSubSystemDetectsLongestMatch(): void
27+
{
28+
$_SESSION = [];
29+
$sub = \framework\Loader::getCurrentSubSystem('sub/example');
30+
31+
$this->assertSame('sub', $sub);
32+
$this->assertSame('sub', $_SESSION['current_subsystem']);
33+
}
34+
35+
public function testListFoldersReturnsNestedDirectories(): void
36+
{
37+
$folders = \framework\Loader::listFolders(APP_CONTROLLERS_PATH);
38+
39+
$this->assertContains('common', $folders);
40+
$this->assertContains('builders', $folders);
41+
}
42+
}

tests/framework/ModelTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Model.php';
7+
8+
final class ModelTest extends TestCase
9+
{
10+
private function createModel(): \framework\Model
11+
{
12+
$reflection = new ReflectionClass(\framework\Model::class);
13+
/** @var \framework\Model $model */
14+
$model = $reflection->newInstanceWithoutConstructor();
15+
16+
return $model;
17+
}
18+
19+
public function testSetAndGetResultSet(): void
20+
{
21+
$model = $this->createModel();
22+
$result = new stdClass();
23+
24+
$model->setResultSet($result);
25+
26+
$this->assertSame($result, $model->getResultSet());
27+
}
28+
29+
public function testEnvelopeSqlWrapsCurrentQuery(): void
30+
{
31+
$model = $this->createModel();
32+
$reflection = new ReflectionClass($model);
33+
$property = $reflection->getProperty('sql');
34+
$property->setAccessible(true);
35+
$property->setValue($model, 'SELECT * FROM users');
36+
37+
$model->envelopeSql();
38+
39+
$this->assertSame('SELECT mvc_sql_evelop.* FROM (SELECT * FROM users) mvc_sql_evelop', $property->getValue($model));
40+
}
41+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
require_once __DIR__ . '/TestHelper.php';
6+
require_once RELATIVE_PATH . 'framework/Model.php';
7+
require_once RELATIVE_PATH . 'framework/MySqlRecord.php';
8+
9+
class TestableMySqlRecord extends \framework\MySqlRecord
10+
{
11+
public function __construct()
12+
{
13+
// Skip parent constructor to avoid opening a database connection.
14+
}
15+
16+
public function real_escape_string($string)
17+
{
18+
return addslashes($string);
19+
}
20+
}
21+
22+
final class MySqlRecordTest extends TestCase
23+
{
24+
private function getProtectedProperty(object $object, string $property)
25+
{
26+
$reflection = new ReflectionClass($object);
27+
$prop = $reflection->getProperty($property);
28+
$prop->setAccessible(true);
29+
30+
return $prop;
31+
}
32+
33+
public function testLastSqlAndErrorAccessorsWork(): void
34+
{
35+
$record = new TestableMySqlRecord();
36+
$lastSqlProp = $this->getProtectedProperty($record, 'lastSql');
37+
$lastErrorProp = $this->getProtectedProperty($record, 'lastSqlError');
38+
39+
$lastSqlProp->setValue($record, 'SELECT 1');
40+
$lastErrorProp->setValue($record, 'error');
41+
42+
$this->assertSame('SELECT 1', $record->lastSql());
43+
$this->assertSame('error', $record->lastSqlError());
44+
$this->assertTrue($record->isSqlError());
45+
46+
$resetMethod = (new ReflectionClass($record))->getMethod('resetLastSqlError');
47+
$resetMethod->setAccessible(true);
48+
$resetMethod->invoke($record);
49+
50+
$this->assertFalse($record->isSqlError());
51+
}
52+
53+
public function testParseValueCastsAndFormatsData(): void
54+
{
55+
$record = new TestableMySqlRecord();
56+
$reflection = new ReflectionClass($record);
57+
$method = $reflection->getMethod('parseValue');
58+
$method->setAccessible(true);
59+
60+
$this->assertSame(10, $method->invoke($record, '10', 'int'));
61+
$this->assertSame("'abc'", $method->invoke($record, 'abc', 'string'));
62+
$this->assertSame("STR_TO_DATE('2024-01-01','" . STORED_DATE_FORMAT . "')", $method->invoke($record, '2024-01-01', 'date'));
63+
$this->assertSame('NULL', $method->invoke($record, null, 'string'));
64+
}
65+
66+
public function testReplaceAposBackSlashAndSuccessLast(): void
67+
{
68+
$record = new TestableMySqlRecord();
69+
$reflection = new ReflectionClass($record);
70+
$method = $reflection->getMethod('replaceAposBackSlash');
71+
$method->setAccessible(true);
72+
73+
$this->assertSame("O'Reilly", $method->invoke($record, "O\\'Reilly"));
74+
$this->assertSame('path\\file', $method->invoke($record, 'path\\\\file'));
75+
76+
$record->affected_rows = 2;
77+
$this->assertTrue($record->successLast());
78+
$record->affected_rows = 0;
79+
$this->assertFalse($record->successLast());
80+
}
81+
}

0 commit comments

Comments
 (0)