Tests
Tests
Niang\Core\Testing\TestCase simule des requêtes en appelant directement le Router,
sans serveur HTTP réel — vérifié contre packages/foundation/src/Testing/*.php et un vrai test du
framework (PasswordResetTest).
TestCase : simuler une requête
namespace Tests\Feature;
use Niang\Core\Testing\TestCase;
class HomeTest extends TestCase
{
public function test_home_page_loads(): void
{
$this->get('/')->assertOk()->assertSee('NiangPro');
}
}
get(), post($uri, $data), put(), patch(), delete() — et call($methode, $uri, $data, $headers) en dessous de tous. Chacun construit une Request et la fait passer par Application::handle(), exactement le chemin qu'emprunterait une vraie requête HTTP (middleware compris).
setUp() reconstruit une Application neuve à chaque test (routes
rechargées, Service Providers ré-exécutés), réinitialise Event et
Queue (sans quoi les listeners s'accumuleraient d'un test à l'autre), et joue les
migrations en attente. APP_ENV=testing (positionné par phpunit.xml)
charge .env.testing à la place de .env :
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
Une base SQLite en mémoire, jamais storage/database.sqlite : la suite de tests ne touche jamais aux données de développement local.
Assertions sur la réponse
get()/post()/... retournent un TestResponse chaînable :
| Méthode | Vérifie |
|---|---|
assertStatus($code) | Code HTTP exact (affiche le corps de la réponse si l'assertion échoue). |
assertOk() | Raccourci pour assertStatus(200). |
assertRedirect($to = null) | 301/302, et optionnellement la valeur de l'en-tête Location. |
assertSee($texte) / assertDontSee($texte) | Présence/absence d'une sous-chaîne dans le corps. |
assertJson($subset) | Le JSON décodé contient au moins ces clés/valeurs ; les objets imbriqués sont comparés de la même façon (['data' => ['email' => ...]] ignore les autres champs de data), une liste exactement. |
json() / content() / header($clé) / status() | Accès brut, pour des assertions PHPUnit directes. |
RefreshDatabase
La base :memory: survit pour tout le run PHPUnit (contrairement à un vrai process web) : un test qui écrit en base doit annuler ses propres écritures pour ne pas polluer les suivants.
use Niang\Core\Testing\RefreshDatabase;
use Niang\Core\Testing\TestCase;
class PasswordResetTest extends TestCase
{
use RefreshDatabase;
public function test_a_user_can_reset_their_password_end_to_end(): void
{
User::create(['name' => 'Awa', 'email' => 'awa@example.test', 'password' => Hash::make('ancien-mdp-1234')]);
// ... écrit en base sans crainte : tout est annulé après ce test.
}
}
Le trait enrobe chaque test dans une transaction (DB::beginTransaction() en setUp(), DB::rollBack() en tearDown()) — à ajouter (use RefreshDatabase;) dans tout test qui écrit en base, jamais activé par défaut.
Mail::fake()
Bascule le driver mail sur 'array' pour la durée du test, quel que soit MAIL_MAILER dans .env.testing — voir Mail :
protected function tearDown(): void
{
Mail::reset(); // sans quoi le driver 'array' resterait actif pour le test suivant
parent::tearDown();
}
public function test_a_reset_link_is_emailed(): void
{
Mail::fake();
$this->post('/forgot-password', ['_token' => Csrf::token(), 'email' => 'awa@example.test'])
->assertRedirect('/forgot-password');
$sent = Mail::sent();
$this->assertCount(1, $sent);
$this->assertSame('awa@example.test', $sent[0]['to']);
}
Mail::reset() en tearDown() est à la charge du test (comme Event::reset()/Queue::reset() sont, eux, automatiques dans TestCase::setUp()) — sans lui, Mail::fake() resterait actif pour le test suivant du même process.
Structure Unit / Feature / Database
phpunit.xml déclare trois suites, chacune sur son dossier :
| Suite | Contenu type |
|---|---|
tests/Unit/ | Classes isolées (Container, Validator, Router...) — souvent sans étendre Niang\Core\Testing\TestCase, juste PHPUnit\Framework\TestCase. |
tests/Feature/ | Requêtes complètes via Testing\TestCase — routes, middleware, contrôleurs, réponses. |
tests/Database/ | Migrations, Query Builder, eager loading exécutés contre une vraie base (:memory:). |
./bin/niang make:test PostsPage # génère tests/Unit/PostsPageTest.php
composer test # vendor/bin/phpunit
make:test génère toujours dans tests/Unit/, avec un stub qui étend
PHPUnit\Framework\TestCase directement (pas Niang\Core\Testing\TestCase) :
à changer manuellement l'use et l'emplacement du fichier pour un test Feature ou
Database qui a besoin de l'application complète.
Suite de tests de sécurité
vendor/bin/phpunit --testsuite=Security
tests/Security/ regroupe une attaque par test : injection SQL, XSS, CSRF, redirection ouverte, en-tête
Host, fixation de session, cookies, traversée de chemin, fichiers déguisés, affectation de masse, IDOR,
limitation de débit, en-têtes de sécurité, debug en production. Toute faille corrigée y ajoute son test.
Couverture de code
Pour le framework lui-même, la CI mesure la couverture (extension pcov ; en local,
pcov ou xdebug) et impose un seuil global (78 %) et un seuil par composant
critique : Router, Container, Database, Auth (double authentification et OAuth compris), Validation, HTTP,
sécurité. Les seuils sont dans tools/coverage-check.php.
vendor/bin/phpunit --coverage-clover build/clover.xml
php tools/coverage-check.php build/clover.xml
Stabilité de l'API et dépréciations
docs/API_STABILITY.md classe
chaque classe publique : stable (aucune rupture avant la prochaine version majeure),
expérimentale (@experimental : OpenAPI, OAuth, permissions, SSE, disque S3...) ou
interne (@internal). Une API supprimée est d'abord dépréciée pendant au moins une
version : @deprecated et trigger_deprecation(), consignée dans les logs en production.
Le cœur passe PHPStan au niveau 7 et déclare strict_types partout.
Contribuer au framework
Le guide CONTRIBUTING.md
détaille l'installation, les vérifications à lancer (composer test, lint,
analyse) et ce qu'on attend d'une pull request. Une faille de sécurité se signale en privé,
jamais dans une issue : voir SECURITY.md.