Jobeet FR

Le tutoriel pour Symfony2 en français

Vue normale

Les tests fonctionnels sont un excellent outil pour tester votre application de bout en bout: de la demande faite par un navigateur jusqu'à la réponse envoyée par le serveur. Ils testent toutes les couches d'une application: le routage, le modèle, les actions et les templates. Ils sont très similaires à ce que vous avez probablement déjà fait manuellement: chaque fois que vous ajoutez ou modifiez une action, vous devez aller dans le navigateur et vérifier que tout fonctionne comme prévu en cliquant sur les liens et vérifier les éléments sur la page rendue. En d'autres termes, vous exécutez un scénario correspondant au cas d'utilisation que vous venez de mettre en œuvre.

Comme le processus est manuel, il est fastidieux et source d'erreurs. Chaque fois que vous changez quelque chose dans votre code, vous devez faire défiler tous les scénarios pour vérifier que vous n'avez rien cassé. C'est de la folie. Les tests fonctionnels dans Symfony fournissent un moyen facile de décrire des scénarios. Chaque scénario peut alors être automatiquement lu maintes et maintes fois en simulant l'expérience d'un utilisateur dans un navigateur. Comme les tests unitaires, ils vous donnent l'assurance nécessaire pour coder en paix.

Les tests fonctionnels ont un flux de travail très spécifique:

  • - Faire une demande;
  • - Tester la réponse;
  • - Cliquer sur un lien ou remplir un formulaire;
  • - Tester la réponse;
  • - Nettoyer et répéter.

Notre premier test fonctionnel

Les tests fonctionnels sont de simples fichiers PHP qui habituellement se situent dans le répertoire Tests/Controller de votre paquet. Si vous voulez tester les pages traitées par votre classe CategoryController, commencez par créer un fichier CategoryControllerTest.php qui étend une classe spéciale WebTestCase:

// src/Ens/JobeetBundle/Tests/Controller/CategoryControllerTest.php
namespace Ens\JobeetBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 
class CategoryControllerTest extends WebTestCase
{
  public function testShow()
  {
    $client = static::createClient();
 
    $crawler = $client->request('GET', '/category/index');
    $this->assertEquals('Ens\JobeetBundle\Controller\CategoryController::showAction', $client->getRequest()->attributes->get('_controller'));
    $this->assertTrue(200 === $client->getResponse()->getStatusCode());
  }
}

Exécution des tests fonctionnels

Comme pour les tests unitaires, le lancement des tests fonctionnels peut être fait en exécutant la commande phpunit:

phpunit -c app/ src/Ens/JobeetBundle/Tests/Controller/CategoryControllerTest

Ce test va échouer parce que l'URL testée, /category/index, n'est pas une URL valide dans Jobeet:

PHPUnit 3.6.10 by Sebastian Bergmann.
 
Configuration read from /home/dragos/work/jobeet/app/phpunit.xml.dist
 
F
 
Time: 2 seconds, Memory: 14.25Mb
 
There was 1 failure:
 
1) Ens\JobeetBundle\Tests\Controller\CategoryControllerTest::testShow
Failed asserting that false is true.

Écriture des tests fonctionnels

Écrire des tests fonctionnels, c'est comme jouer un scénario dans un navigateur. Nous avons déjà écrit tous les scénarios que nous avons besoin de tester dans le cadre du deuxième chapitre.

Tout d'abord, nous allons tester la page d'accueil Jobeet en modifiant le fichier de test JobControllerTest.php. Remplacez le code par le suivant:

LES OFFRES EXPIRÉES NE SONT PAS LISTÉES
// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
namespace Ens\JobeetBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 
class JobControllerTest extends WebTestCase
{
  public function testIndex()
  {
    $client = static::createClient();
 
    $crawler = $client->request('GET', '/');
    $this->assertEquals('Ens\JobeetBundle\Controller\JobController::indexAction', $client->getRequest()->attributes->get('_controller'));
    $this->assertTrue($crawler->filter('.jobs td.position:contains("Expired")')->count() == 0);
  }
}

Pour vérifier l'exclusion des offres expirées depuis la page d'accueil, nous vérifions que le sélecteur CSS .jobs td.position:contains("Expired") ne correspond à rien dans le contenu de la réponse HTML (rappelons que dans les fixtures, la seule offre expirée contient "Expired" dans l'intitulé).

SEULEMENT N OFFRES SONT AFFICHÉES POUR UNE CATÉGORIE

Ajoutez le code suivant à la fin du fichier de test. Pour obtenir le paramètre personnalisé défini dans app/config/config.yml dans notre test fonctionnel, nous allons utiliser le noyau:

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
$kernel = static::createKernel();
$kernel->boot();
$max_jobs_on_homepage = $kernel->getContainer()->getParameter('max_jobs_on_homepage');
$this->assertTrue($crawler->filter('.category_programming tr')->count());
// ...

Pour que ce test fonctionne, nous aurons besoin d'ajouter la classe CSS correspondant à chaque catégorie dans le fichier Job/index.html.twig (afin que nous puissions sélectionner chaque catégorie et compter les offres répertoriées):

<!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
<!-- ... -->
 
{% for category in categories %}
<div class="category_{{ category.slug }}">
<!-- ... -->
UNE CATÉGORIE A UN LIEN VERS LA PAGE CATÉGORIE SEULEMENT S'IL Y A TROP D'OFFRES
// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
$this->assertTrue($crawler->filter('.category_design .more_jobs')->count() == 0);
$this->assertTrue($crawler->filter('.category_programming .more_jobs')->count() == 1);
// ...

Dans ces tests, nous vérifions qu'il n'y a pas de "plus d'offres" pour la catégorie Design (.category_design .more_jobs n'existe pas), et qu'il y a un "plus d'offres" pour la catégorie Programming (.category_programming .more_jobs existe ).

LES OFFRES SONT TRIÉES PAR DATE

Pour tester si les offres sont effectivement triées par date, nous devons vérifier que le premier emploi figurant sur la page d'accueil est celui que nous attendons. Cela peut être fait en vérifiant que l'URL contient la clé primaire attendue. Comme la clé primaire peut changer entre les exécutions, nous avons besoin de l'objet Doctrine de la BDD en premier.

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
$kernel = static::createKernel();
$kernel->boot();
$em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
$query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j LEFT JOIN j.category c WHERE c.slug = :slug AND j.expires_at > :date ORDER BY j.created_at DESC');
$query->setParameter('slug', 'programming');
$query->setParameter('date', date('Y-m-d H:i:s', time()));
$query->setMaxResults(1);
$job = $query->getSingleResult();
 
$this->assertTrue($crawler->filter('.category_programming tr')->first()->filter(sprintf('a[href*="/%d/"]', $job->getId()))->count() == 1);
// ...

Même si le test fonctionne tel quel, nous avons besoin de factoriser le code un peu, car l'obtention de la première offre de la catégorie Programming peut être réutilisée ailleurs dans nos tests. Nous ne bougerons pas le code vers la couche Modèle puisque le code est spécifique au test. Au lieu de cela, nous allons déplacer le code de la fonction getMostRecentProgrammingJob() dans notre classe de test:

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
public function getMostRecentProgrammingJob()
{
  $kernel = static::createKernel();
  $kernel->boot();
  $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
  $query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j LEFT JOIN j.category c WHERE c.slug = :slug AND j.expires_at > :date ORDER BY j.created_at DESC');
  $query->setParameter('slug', 'programming');
  $query->setParameter('date', date('Y-m-d H:i:s', time()));
  $query->setMaxResults(1);
  return $query->getSingleResult();
}
 
// ...

Vous pouvez maintenant remplacer le code de test précédent par le suivant:

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
$this->assertTrue($crawler->filter('.category_programming tr')->first()->filter(sprintf('a[href*="/%d/"]', $this->getMostRecentProgrammingJob()->getId()))->count() == 1);
// ...
CHAQUE OFFRE SUR LA PAGE D'ACCUEIL EST CLIQUABLE

Pour tester le lien d'offre sur le site, nous simulons un clic sur le texte "Web Developer". Comme il y a beaucoup d'entre eux sur la page, nous avons explicitement demandé au navigateur de cliquer sur la première.

Chaque paramètre de requête est ensuite testé pour s'assurer que le routage a fait son travail correctement.

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
// ...
 
$job = $this->getMostRecentProgrammingJob();
$link = $crawler->selectLink('Web Developer')->first()->link();
$crawler = $client->click($link);
$this->assertEquals('Ens\JobeetBundle\Controller\JobController::showAction', $client->getRequest()->attributes->get('_controller'));
$this->assertEquals($job->getCompanySlug(), $client->getRequest()->attributes->get('company'));
$this->assertEquals($job->getLocationSlug(), $client->getRequest()->attributes->get('location'));
$this->assertEquals($job->getPositionSlug(), $client->getRequest()->attributes->get('position'));
$this->assertEquals($job->getId(), $client->getRequest()->attributes->get('id'));
// ...
APPRENEZ PAR L'EXEMPLE

Dans cette section, vous avez tout le code nécessaire pour tester les pages offre et catégorie. Lisez le code avec soin car vous apprendrez quelques nouveaux trucs:

// src/Ens/JobeetBundle/Test/Controller/JobControllerTest.php
 
namespace Ens\JobeetBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 
class JobControllerTest extends WebTestCase
{
  public function getMostRecentProgrammingJob()
  {
    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
    $query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j LEFT JOIN j.category c WHERE c.slug = :slug AND j.expires_at > :date ORDER BY j.created_at DESC');
    $query->setParameter('slug', 'programming');
    $query->setParameter('date', date('Y-m-d H:i:s', time()));
    $query->setMaxResults(1);
    return $query->getSingleResult();
  }
 
  public function getExpiredJob()
  {
    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
    $query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j WHERE j.expires_at < :date');     $query->setParameter('date', date('Y-m-d H:i:s', time()));
    $query->setParameter('date', date('Y-m-d H:i:s', time()));
    $query->setMaxResults(1);
    return $query->getSingleResult();
  }
 
  public function testIndex()
  {
    // get the custom parameters from app config.yml
    $kernel = static::createKernel();
    $kernel->boot();
    $max_jobs_on_homepage = $kernel->getContainer()->getParameter('max_jobs_on_homepage');
    $max_jobs_on_category = $kernel->getContainer()->getParameter('max_jobs_on_category');
 
    $client = static::createClient();
 
    $crawler = $client->request('GET', '/');
    $this->assertEquals('Ens\JobeetBundle\Controller\JobController::indexAction', $client->getRequest()->attributes->get('_controller'));
 
    // expired jobs are not listed
    $this->assertTrue($crawler->filter('.jobs td.position:contains("Expired")')->count() == 0);
 
    // only $max_jobs_on_homepage jobs are listed for a category
    $this->assertTrue($crawler->filter('.category_programming tr')->count() == 10);
    $this->assertTrue($crawler->filter('.category_design .more_jobs')->count() == 0);
    $this->assertTrue($crawler->filter('.category_programming .more_jobs')->count() == 1);
 
    // jobs are sorted by date
    $this->assertTrue($crawler->filter('.category_programming tr')->first()->filter(sprintf('a[href*="/%d/"]', $this->getMostRecentProgrammingJob()->getId()))->count() == 1);
 
    // each job on the homepage is clickable and give detailed information
    $job = $this->getMostRecentProgrammingJob();
    $link = $crawler->selectLink('Web Developer')->first()->link();
    $crawler = $client->click($link);
    $this->assertEquals('Ens\JobeetBundle\Controller\JobController::showAction', $client->getRequest()->attributes->get('_controller'));
    $this->assertEquals($job->getCompanySlug(), $client->getRequest()->attributes->get('company'));
    $this->assertEquals($job->getLocationSlug(), $client->getRequest()->attributes->get('location'));
    $this->assertEquals($job->getPositionSlug(), $client->getRequest()->attributes->get('position'));
    $this->assertEquals($job->getId(), $client->getRequest()->attributes->get('id'));
 
    // a non-existent job forwards the user to a 404
    $crawler = $client->request('GET', '/job/foo-inc/milano-italy/0/painter');
    $this->assertTrue(404 === $client->getResponse()->getStatusCode());
 
    // an expired job page forwards the user to a 404
    $crawler = $client->request('GET', sprintf('/job/sensio-labs/paris-france/%d/web-developer-expired', $this->getExpiredJob()->getId()));
    $this->assertTrue(404 === $client->getResponse()->getStatusCode());
  }
}
// src/Ens/JobeetBundle/Test/Controller/CategoryControllerTest.php
 
namespace Ens\JobeetBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 
class CategoryControllerTest extends WebTestCase
{
  public function testShow()
  {
    // get the custom parameters from app config.yml
    $kernel = static::createKernel();
    $kernel->boot();
    $max_jobs_on_homepage = $kernel->getContainer()->getParameter('max_jobs_on_homepage');
    $max_jobs_on_category = $kernel->getContainer()->getParameter('max_jobs_on_category');
 
    $client = static::createClient();
 
    // categories on homepage are clickable
    $crawler = $client->request('GET', '/');
    $link = $crawler->selectLink('Programming')->link();
    $crawler = $client->click($link);
    $this->assertEquals('Ens\JobeetBundle\Controller\CategoryController::showAction', $client->getRequest()->attributes->get('_controller'));
    $this->assertEquals('programming', $client->getRequest()->attributes->get('slug'));
 
    // categories with more than $max_jobs_on_homepage jobs also have a "more" link
    $crawler = $client->request('GET', '/');
    $link = $crawler->selectLink('22')->link();
    $crawler = $client->click($link);
    $this->assertEquals('Ens\JobeetBundle\Controller\CategoryController::showAction', $client->getRequest()->attributes->get('_controller'));
    $this->assertEquals('programming', $client->getRequest()->attributes->get('slug'));
 
    // only $max_jobs_on_category jobs are listed
    $this->assertTrue($crawler->filter('.jobs tr')->count() == 20);
    $this->assertRegExp('/32 jobs/', $crawler->filter('.pagination_desc')->text());
    $this->assertRegExp('/page 1\/2/', $crawler->filter('.pagination_desc')->text());
 
    $link = $crawler->selectLink('2')->link();
    $crawler = $client->click($link);
    $this->assertEquals(2, $client->getRequest()->attributes->get('page'));
    $this->assertRegExp('/page 2\/2/', $crawler->filter('.pagination_desc')->text());
  }
}

Chapitre précédent Chapitre suivant


Une question ? Une réaction ?

comments powered by Disqus