Tests d'application
Aller plus loin que les tests isolés : vérifier que votre application fonctionne réellement, avec sa vraie base de données

Petite vérification
Avant de commencer, vérifiez si vous savez...
- citer la classe mère d'un test unitaire, et celle d'un test fonctionnel, dans Symfony
- citer la méthode qui simule une requête HTTP depuis un test fonctionnel, et celle qui vérifie qu'une réponse a réussi
- pour le test
GameControllerTest::testCreateCharacter()de la séance précédente, expliquer pourquoi ce test ne prouve pas qu'un personnage a réellement été enregistré en base de données
📌 Réponse attendue
Notions théoriques
Ce que les tests de la séance précédente ne vérifient pas
Le test unitaire PlayerTest ne touche à aucune base de données : il manipule un simple objet PHP,
en mémoire. Le test fonctionnel GameControllerTest démarre l'application et simule une vraie
requête HTTP, mais il ne vérifie que la réponse renvoyée, jamais l'état de la base de
données après cette requête.
Or, la plupart des bugs réels apparaissent justement à cette frontière : un formulaire qui semble fonctionner, mais dont les données ne sont jamais réellement sauvegardées. On appelle tests d'application (ou tests d'intégration) les tests qui vérifient le comportement réel de l'application, base de données comprise.
Le terme n'est pas figé : certains parlent de "tests d'intégration", d'autres élargissent le sens de "tests fonctionnels" pour y inclure la vérification de la base. Ce qui compte est le principe : vérifier des effets réels, pas seulement une réponse HTTP.
Ne jamais tester sur la base de développement
Vous avez vu, dans la séance sur la configuration par environnement, que Symfony peut utiliser une
base de données différente pour chaque environnement, grâce à .env.test. C'est indispensable
pour les tests : ils vont créer, modifier et supprimer des données, ce qui détruirait votre base de
travail habituelle.
# .env.test (déjà vu en séance sur la configuration des environnements)
DATABASE_URL="mysql://app:app@127.0.0.1:3306/my_game_test?serverVersion=8.0"
Avant de lancer des tests pour la première fois, préparez la base de test :
php bin/console doctrine:database:create --env=test
php bin/console doctrine:schema:create --env=test
Si votre projet utilise des migrations (vues en séance dédiée), utilisez
php bin/console doctrine:migrations:migrate --env=test à la place de doctrine:schema:create,
pour que la base de test suive exactement la même structure que la base de production.
KernelTestCase : des vrais services, sans requête HTTP
WebTestCase simule une requête HTTP complète. C'est parfois plus qu'il n'en faut : pour tester un
repository ou un service, KernelTestCase démarre l'application sans simuler de requête, et
donne accès au conteneur de services :
namespace App\Tests\Repository;
use App\Entity\Player;
use App\Repository\PlayerRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Doctrine\ORM\EntityManagerInterface;
class PlayerRepositoryTest extends KernelTestCase
{
public function testFindAllReturnsCreatedPlayer(): void
{
self::bootKernel();
$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$player = new Player();
$player->setName('JoueurDeTest');
$entityManager->persist($player);
$entityManager->flush();
$playerRepository = self::getContainer()->get(PlayerRepository::class);
$players = $playerRepository->findAll();
$this->assertGreaterThanOrEqual(1, count($players));
}
}
self::getContainer() donne accès à n'importe quel service de votre application (repository,
service métier, mailer...) directement dans un test, comme s'il était injecté dans un contrôleur.
Nettoyer après chaque test
Contrairement à un test unitaire, un test avec une vraie base de données laisse des traces : le
JoueurDeTest créé ci-dessus reste en base après le test. Deux bonnes pratiques permettent d'éviter
que les tests ne se polluent les uns les autres :
- donner à vos données de test des noms uniques et reconnaissables, jamais des noms qui pourraient exister par ailleurs ;
- supprimer, dans une méthode
tearDown(), ce que le test a créé.
protected function tearDown(): void
{
parent::tearDown();
$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$player = $entityManager->getRepository(Player::class)->findOneBy(['name' => 'JoueurDeTest']);
if ($player) {
$entityManager->remove($player);
$entityManager->flush();
}
}
Tester un formulaire de bout en bout
La séance sur les formulaires a créé l'action create_player, qui affiche et traite un formulaire
d'ajout de joueur. Un test fonctionnel peut désormais soumettre ce formulaire, avec
submitForm(), puis vérifier que le joueur a bien été enregistré :
namespace App\Tests\Controller;
use App\Repository\PlayerRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class GameControllerTest extends WebTestCase
{
public function testCreatePlayerIsPersisted(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/game/create');
$client->submitForm('Create', [
'player[name]' => 'JoueurFormulaireTest',
]);
$this->assertResponseRedirects();
$playerRepository = static::getContainer()->get(PlayerRepository::class);
$player = $playerRepository->findOneBy(['name' => 'JoueurFormulaireTest']);
$this->assertNotNull($player);
}
}
submitForm('Create', [...]) retrouve le formulaire grâce au texte du bouton de validation
(<button type="submit">Create</button>), puis remplit ses champs avant de le soumettre. Le nom du
champ (player[name]) reprend le nom du formulaire (PlayerType) et le nom de la propriété.
Ce test-ci vérifie une chose que le test de la séance précédente ne pouvait pas vérifier : que le joueur existe réellement en base après la soumission, pas seulement que la page a répondu correctement.
Tester un formulaire protégé par un rôle
Certaines actions ne doivent être accessibles qu'à certains rôles. Par exemple, seul un utilisateur
ROLE_SUPER_ADMIN devrait pouvoir créer un nouvel utilisateur (vu en séance sur la sécurité
avancée) :
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/admin/user/create', name: 'admin_create_user')]
#[IsGranted('ROLE_SUPER_ADMIN')]
public function createUser(Request $request): Response
{
$user = new User();
$form = $this->formFactory->create(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->userRepository->save($user, true);
return $this->redirectToRoute('admin_users');
}
return $this->render('admin/create_user.html.twig', ['form' => $form->createView()]);
}
Pour tester un accès protégé, il faut simuler un utilisateur déjà connecté, avec un rôle précis.
C'est le rôle de loginUser() : il authentifie directement un objet User dans le client de test,
sans passer par le formulaire de connexion.
namespace App\Tests\Controller;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Doctrine\ORM\EntityManagerInterface;
class AdminControllerTest extends WebTestCase
{
public function testSuperAdminCanCreateUser(): void
{
$client = static::createClient();
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
$superAdmin = new User();
$superAdmin->setEmail('super-admin-test@exemple.fr');
$superAdmin->setPassword('mot-de-passe-non-verifie-par-loginUser');
$superAdmin->setRoles(['ROLE_SUPER_ADMIN']);
$entityManager->persist($superAdmin);
$entityManager->flush();
$client->loginUser($superAdmin);
$client->request('GET', '/admin/user/create');
$this->assertResponseIsSuccessful();
}
public function testRegularUserCannotCreateUser(): void
{
$client = static::createClient();
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
$regularUser = new User();
$regularUser->setEmail('utilisateur-test@exemple.fr');
$regularUser->setPassword('mot-de-passe-non-verifie-par-loginUser');
$regularUser->setRoles(['ROLE_USER']);
$entityManager->persist($regularUser);
$entityManager->flush();
$client->loginUser($regularUser);
$client->request('GET', '/admin/user/create');
$this->assertResponseStatusCodeSame(403);
}
}
loginUser() marque directement l'utilisateur comme authentifié pour la durée du test : il ne
vérifie jamais le mot de passe. Il est donc inutile (et déconseillé) d'y placer un vrai mot de
passe en clair ou même un hash valide — seul le rôle compte pour ce genre de test.
Le premier test prouve que la protection laisse bien passer la personne autorisée : un oubli
fréquent, qui bloquerait un administrateur légitime. Le second prouve que la protection bloque
bien tout le monde d'autre, avec le bon code HTTP (403 Forbidden) plutôt qu'un simple bug
silencieux. Tester uniquement l'un des deux cas laisserait passer une régression sur l'autre.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Votre défi pour aujourd'hui consiste à écrire des tests d'application pour votre jeu
Étape 1 — Préparer la base de données de test
Étape 2 — Tester un repository avec KernelTestCase
Écrivez un test qui vérifie que CharacterRepository::findAll() retrouve bien un personnage tout
juste créé.
Préfixez ou suffixez vos données de test (PersonnageDeTest, JoueurFormulaireTest) pour les
distinguer immédiatement de vraies données, aussi bien dans le code que si vous consultez la base
par erreur pendant le développement.
Étape 3 — Tester le formulaire de création de joueur de bout en bout
Pas besoin de tout retester à ce niveau : réservez les tests d'application aux parcours qui comptent vraiment (inscription, paiement, création d'une ressource principale). Le détail de chaque règle métier reste plus rapide et plus simple à couvrir par des tests unitaires, comme vu à la séance précédente.