Refactor core test runtime and simplify project documentation

This commit is contained in:
julien
2026-03-20 22:52:02 +01:00
parent 9b1dd9417c
commit 24b3bb4177
19 changed files with 266 additions and 132 deletions

View File

@@ -18,7 +18,6 @@ use Netig\Netslim\Identity\UI\Http\UserController;
use Netig\Netslim\Kernel\Http\Infrastructure\Twig\AppExtension;
use Netig\Netslim\Kernel\Runtime\Http\MiddlewareRegistrar;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use Netig\Netslim\Media\Application\MediaServiceInterface;
use Netig\Netslim\Media\UI\Http\MediaController;
use Netig\Netslim\Notifications\Application\NotificationServiceInterface;
@@ -30,6 +29,7 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Tests\Support\TestRuntimeFactory;
use Twig\Loader\FilesystemLoader;
final class ContainerWiringIntegrationTest extends TestCase
@@ -56,8 +56,7 @@ final class ContainerWiringIntegrationTest extends TestCase
protected function setUp(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
ModuleRegistry::reset();
TestRuntimeFactory::resetRuntime();
foreach (self::ENV_DEFAULTS as $key => $value) {
$this->envBackup[$key] = $_ENV[$key] ?? null;
$_ENV[$key] = $value;
@@ -66,8 +65,6 @@ final class ContainerWiringIntegrationTest extends TestCase
protected function tearDown(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
foreach (self::ENV_DEFAULTS as $key => $_) {
$previous = $this->envBackup[$key] ?? null;

View File

@@ -6,14 +6,14 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseNotProvisionedException;
use Netig\Netslim\Kernel\Persistence\Infrastructure\DatabaseReadiness;
use PDO;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
final class DatabaseReadinessTest extends TestCase
{
public function testAssertProvisionedFailsWhenModuleTablesAreMissing(): void
{
$db = new PDO('sqlite::memory:');
$db = TestDatabaseFactory::createInMemory();
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)');
$this->expectException(DatabaseNotProvisionedException::class);
@@ -24,7 +24,7 @@ final class DatabaseReadinessTest extends TestCase
public function testAssertProvisionedAcceptsCompleteCoreSchema(): void
{
$db = new PDO('sqlite::memory:');
$db = TestDatabaseFactory::createInMemory();
$db->exec('CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT, run_at TEXT)');
$db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, email TEXT, password_hash TEXT, role TEXT, session_version INTEGER, created_at TEXT)');

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator;
use PDO;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
/**
* Tests unitaires pour Migrator.
@@ -18,14 +18,11 @@ use PHPUnit\Framework\TestCase;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class MigratorTest extends TestCase
{
private PDO $db;
private \PDO $db;
protected function setUp(): void
{
$this->db = new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->db = TestDatabaseFactory::createInMemory();
}
public function testRunCreatesMigrationsTable(): void

View File

@@ -8,26 +8,23 @@ use Netig\Netslim\Kernel\Runtime\Module\ModuleInterface;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
final class ModuleRegistryTest extends TestCase
{
protected function setUp(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
TestRuntimeFactory::resetRuntime();
}
protected function tearDown(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
TestRuntimeFactory::resetRuntime();
}
public function testModulesAreDeclaredInExpectedOrderForFixtureApplication(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
TestRuntimeFactory::resetRuntime();
$moduleClasses = array_map(
static fn (ModuleInterface $module): string => $module::class,
@@ -47,7 +44,7 @@ final class ModuleRegistryTest extends TestCase
public function testModuleClassNamesExposeTheActiveApplicationComposition(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
TestRuntimeFactory::resetRuntime();
self::assertSame([
'Netig\Netslim\Kernel\Runtime\KernelModule',
@@ -62,14 +59,14 @@ final class ModuleRegistryTest extends TestCase
public function testApplicationManifestIsResolvedFromTheActiveApplicationRoot(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
TestRuntimeFactory::resetRuntime();
self::assertFileExists(RuntimePaths::getApplicationConfigPath('modules.php'));
}
public function testEveryModuleExposesResolvableMetadata(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
TestRuntimeFactory::resetRuntime();
foreach (ModuleRegistry::modules() as $module) {
self::assertNotSame([], $module->definitions(), $module::class . ' should expose DI definitions.');

View File

@@ -6,21 +6,18 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\Module\ProvidesSchemaInterface;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
final class ModuleSchemaTest extends TestCase
{
protected function setUp(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
ModuleRegistry::reset();
TestRuntimeFactory::resetRuntime();
}
protected function tearDown(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
}

View File

@@ -5,14 +5,15 @@ declare(strict_types=1);
namespace Tests\Kernel;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Provisioner;
use PDO;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class ProvisionerTest extends TestCase
{
private PDO $db;
private \PDO $db;
private string $lockPath;
@@ -20,13 +21,11 @@ final class ProvisionerTest extends TestCase
protected function setUp(): void
{
$this->db = new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
TestRuntimeFactory::resetRuntime();
$this->db = TestDatabaseFactory::createInMemory();
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
$this->lockPath = dirname(__DIR__, 2) . '/database/.provision.lock';
$this->lockPath = TestRuntimeFactory::path('database/.provision.lock');
@unlink($this->lockPath);
$this->envBackup = [

View File

@@ -6,23 +6,20 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\Routing\Routes;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase;
use Slim\Factory\AppFactory;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class RoutesTest extends TestCase
{
protected function setUp(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
ModuleRegistry::reset();
TestRuntimeFactory::resetRuntime();
}
protected function tearDown(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
ModuleRegistry::reset();
}

View File

@@ -6,40 +6,40 @@ namespace Tests\Kernel;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestRuntimeFactory;
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
final class RuntimePathsTest extends TestCase
{
protected function tearDown(): void
{
RuntimePaths::resetApplicationRoot();
RuntimePaths::resetProjectRoot();
TestRuntimeFactory::resetRuntime();
}
public function testGetProjectRootReturnsRepositoryRoot(): void
public function testGetProjectRootReturnsIsolatedTestProjectRoot(): void
{
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getProjectRoot());
self::assertSame(TestRuntimeFactory::projectRoot(), RuntimePaths::getProjectRoot());
}
public function testGetConfigPathReturnsRootConfigDirectoryAndFilePath(): void
{
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getConfigPath());
self::assertSame(dirname(__DIR__, 2) . '/config/modules.php', RuntimePaths::getConfigPath('modules.php'));
self::assertSame(TestRuntimeFactory::path('config'), RuntimePaths::getConfigPath());
self::assertSame(TestRuntimeFactory::path('config/modules.php'), RuntimePaths::getConfigPath('modules.php'));
}
public function testApplicationRootDefaultsToProjectRoot(): void
{
self::assertSame(dirname(__DIR__, 2), RuntimePaths::getApplicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/config', RuntimePaths::getApplicationConfigPath());
self::assertSame(TestRuntimeFactory::applicationRoot(), RuntimePaths::getApplicationRoot());
self::assertSame(TestRuntimeFactory::path('config'), RuntimePaths::getApplicationConfigPath());
}
public function testApplicationRootCanPointToFixtureApplication(): void
public function testApplicationRootCanPointToTheIsolatedFixtureApplication(): void
{
RuntimePaths::setApplicationRoot(dirname(__DIR__, 2) . '/tests/Fixtures/Application');
RuntimePaths::setApplicationRoot(TestRuntimeFactory::applicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application', RuntimePaths::getApplicationRoot());
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/config/modules.php', RuntimePaths::getApplicationConfigPath('modules.php'));
self::assertSame(dirname(__DIR__, 2) . '/tests/Fixtures/Application/templates/Kernel', RuntimePaths::getApplicationPath('templates/Kernel'));
self::assertSame(TestRuntimeFactory::applicationRoot(), RuntimePaths::getApplicationRoot());
self::assertSame(TestRuntimeFactory::path('config/modules.php'), RuntimePaths::getApplicationConfigPath('modules.php'));
self::assertSame(TestRuntimeFactory::path('templates/Kernel'), RuntimePaths::getApplicationPath('templates/Kernel'));
}
public function testGetTwigCacheReturnsFalseInDev(): void
@@ -57,7 +57,7 @@ final class RuntimePathsTest extends TestCase
public function testGetDatabasePathCreatesDatabaseFileWhenMissing(): void
{
$dbFile = dirname(__DIR__, 2) . '/database/app.sqlite';
$dbFile = TestRuntimeFactory::path('database/app.sqlite');
$dbDir = dirname($dbFile);
$backup = $dbFile . '.bak-test';

View File

@@ -5,20 +5,19 @@ declare(strict_types=1);
namespace Tests\Media;
use Netig\Netslim\Kernel\Persistence\Infrastructure\Migrator;
use PDO;
use PDOException;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestDatabaseFactory;
use Tests\Support\TestRuntimeFactory;
final class MediaSchemaIntegrationTest extends TestCase
{
private PDO $db;
private \PDO $db;
protected function setUp(): void
{
$this->db = new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
TestRuntimeFactory::resetRuntime();
$this->db = TestDatabaseFactory::createInMemory();
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
Migrator::run($this->db);

9
tests/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Tests du core
La suite de tests prépare une mini application consommatrice éphémère sous un répertoire temporaire. Les chemins runtime (`database/`, `var/`, `public/media/`) ne pointent donc ni vers le dépôt du core ni vers un projet client réel.
Organisation :
- `Architecture/` : garde-fous de dépendances et de structure
- `Kernel/` : runtime, bootstrap, wiring, utilitaires transverses
- `Support/` : helpers de tests et fabrication du runtime de test
- `Fixtures/` : manifeste et templates minimaux de l'application de test

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
use PDO;
/**
* Fabrique légère pour les bases SQLite de test.
*/
final class TestDatabaseFactory
{
public static function createInMemory(): PDO
{
return new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
public static function createFileBacked(string $name): PDO
{
$path = TestRuntimeFactory::path('database/' . $name . '.sqlite');
if (is_file($path)) {
@unlink($path);
}
return new PDO('sqlite:' . $path, options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
/**
* Prépare une mini application consommatrice éphémère pour les tests du core.
*
* Tous les chemins runtime persistants (database/, var/, public/media) sont
* créés sous un répertoire temporaire isolé afin que la suite de tests ne
* touche ni le dépôt courant ni un projet client réel.
*/
final class TestRuntimeFactory
{
private static ?string $projectRoot = null;
public static function boot(): void
{
if (self::$projectRoot !== null) {
self::applyRuntimeRoots();
return;
}
$baseRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'netslim-core-tests';
if (!is_dir($baseRoot)) {
mkdir($baseRoot, 0777, true);
}
$projectRoot = $baseRoot . DIRECTORY_SEPARATOR . 'run-' . bin2hex(random_bytes(6));
mkdir($projectRoot, 0777, true);
self::copyDirectory(self::fixtureRoot() . '/config', $projectRoot . '/config');
self::copyDirectory(self::fixtureRoot() . '/templates', $projectRoot . '/templates');
foreach (['database', 'var/cache/twig', 'var/cache/htmlpurifier', 'var/logs', 'public/media'] as $directory) {
$path = $projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $directory);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
self::$projectRoot = $projectRoot;
self::applyRuntimeRoots();
register_shutdown_function(static function (): void {
TestRuntimeFactory::cleanup();
});
}
public static function projectRoot(): string
{
self::boot();
return self::$projectRoot;
}
public static function applicationRoot(): string
{
return self::projectRoot();
}
public static function path(string $relativePath = ''): string
{
$root = self::projectRoot();
return $relativePath === ''
? $root
: $root . DIRECTORY_SEPARATOR . ltrim(str_replace('/', DIRECTORY_SEPARATOR, $relativePath), DIRECTORY_SEPARATOR);
}
public static function resetRuntime(): void
{
self::boot();
self::applyRuntimeRoots();
}
private static function applyRuntimeRoots(): void
{
if (self::$projectRoot === null) {
throw new \LogicException('Test runtime project root is not initialized.');
}
RuntimePaths::setProjectRoot(self::$projectRoot);
RuntimePaths::setApplicationRoot(self::$projectRoot);
ModuleRegistry::reset();
}
public static function cleanup(): void
{
if (self::$projectRoot === null || !is_dir(self::$projectRoot)) {
return;
}
self::deleteDirectory(self::$projectRoot);
self::$projectRoot = null;
RuntimePaths::resetProjectRoot();
RuntimePaths::resetApplicationRoot();
ModuleRegistry::reset();
}
private static function fixtureRoot(): string
{
return dirname(__DIR__) . '/Fixtures/Application';
}
private static function copyDirectory(string $source, string $destination): void
{
if (!is_dir($destination)) {
mkdir($destination, 0777, true);
}
$items = scandir($source);
if ($items === false) {
throw new \RuntimeException(sprintf('Unable to read fixture directory: %s', $source));
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$from = $source . DIRECTORY_SEPARATOR . $item;
$to = $destination . DIRECTORY_SEPARATOR . $item;
if (is_dir($from)) {
self::copyDirectory($from, $to);
continue;
}
copy($from, $to);
}
}
private static function deleteDirectory(string $path): void
{
$items = scandir($path);
if ($items === false) {
return;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$current = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($current)) {
self::deleteDirectory($current);
continue;
}
@unlink($current);
}
@rmdir($path);
}
}

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use Netig\Netslim\Kernel\Runtime\Module\ModuleRegistry;
use Netig\Netslim\Kernel\Runtime\RuntimePaths;
use Tests\Support\TestRuntimeFactory;
RuntimePaths::setApplicationRoot(dirname(__DIR__) . '/tests/Fixtures/Application');
RuntimePaths::setProjectRoot(dirname(__DIR__));
ModuleRegistry::reset();
TestRuntimeFactory::boot();