Symfony2 - Jobeet - Jour 11 - Testez vos formulaires

1 1 1 1 1 1 1 1 1 1 Rating 0.00 (0 Votes)
Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn

Dans le jour 10 (Symfony2 - Jobeet - Jour 10 - Les formulaires), nous avons créé notre premier formulaire avec Symfony2. Les utilisateurs sont maintenant en mesure de publier une nouvelle offre d'emploi dans Jobeet mais nous avons manqué de temps avant que nous puissions ajouter quelques tests. C'est ce que nous allons faire.

Lancer les tests

Dans un premier temps, nous allons relancer les tests pour vérifier les résultats :

phpunit -c app/ src/Erlem/JobeetBundle/Tests/

Le résultat :

PHPUnit 4.0.17 by Sebastian Bergmann.

.F..........

Time: 24.79 seconds, Memory: 62.75Mb

There was 1 failure:

1) Erlem\JobeetBundle\Tests\Controller\DefaultControllerTest::testIndex
Failed asserting that false is true.

/home/debian/www/jobeet/src/Erlem/JobeetBundle/Tests/Controller/DefaultControllerTest.php:15
                                       
FAILURES!                              
Tests: 12, Assertions: 56, Failures: 1.

Nous pouvons constater qu'il y a un problème avec le test testIndex() du fichier src/Erlem/JobeetBundle/Tests/Controller/DefaultControllerTest.php. Commentez dans un premier temps la ligne qui pose problème, nous y reviendrons plus tard.

  • src/Erlem/JobeetBundle/Tests/Controller/DefaultControllerTest.php
<?php

namespace Erlem\JobeetBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DefaultControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/hello/Fabien');

        // $this->assertTrue($crawler->filter('html:contains("Hello Fabien")')->count() > 0);
    }
}

Puis relancer les tests :

phpunit -c app/ src/Erlem/JobeetBundle/Tests/

Le résultat :

PHPUnit 4.0.17 by Sebastian Bergmann.

............

Time: 26.59 seconds, Memory: 62.50Mb

OK (12 tests, 55 assertions)

Soumission d'un formulaire

Ouvrons le fichier JobControllerTest et ajoutons des tests fonctionnels pour la création d'emplois et le processus de validation. A la fin du fichier, ajoutez le code suivant pour obtenir la page de création d'emplois :

  •  src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testJobForm()
{
    $client = static::createClient();

    $crawler = $client->request('GET', '/job/new');
    $this->assertEquals('Erlem\JobeetBundle\Controller\JobController::newAction', $client->getRequest()->attributes->get('_controller'));
}

Pour sélectionner les formulaires, nous allons utiliser la méthode selectButton(). Cette méthode permet de sélectionner des étiquettes de bouton et de soumettre les balises d'entrée. Une fois que vous avez un Crawler qui représente un bouton, appelez la méthode form() pour obtenir une instance de formulaire :

$form = $crawler->selectButton('Submit Form')->form();

L'exemple ci-dessus sélectionne une entrée de type submit aide de son attribut de valeur "Soumettre le formulaire».

Lorsque vous appelez la méthode form(), vous pouvez aussi passer un tableau de valeurs de champ qui remplace les valeurs par défaut :

$form = $crawler->selectButton('submit')->form(array(
    'name' => 'Fabien',
    'my_form[subject]' => 'Symfony Rocks!'
));

Il est maintenant temps de choisir réellement et transmettre des valeurs valides pour la formulaire :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testJobForm()
{
    $client = static::createClient();

    $crawler = $client->request('GET', '/job/new');
    $this->assertEquals('Erlem\JobeetBundle\Controller\JobController::newAction', $client->getRequest()->attributes->get('_controller'));

    $form = $crawler->selectButton('Preview your job')->form(array(
        'job[company]'      => 'Sensio Labs',
        'job[url]'          => 'http://www.sensio.com/',
        'job[file]'         => __DIR__.'/../../../../../web/bundles/erlemjobeet/images/sensio-labs.gif',
        'job[position]'     => 'Developer',
        'job[location]'     => 'Atlanta, USA',
        'job[description]'  => 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' => 'Send me an email',
        'job[email]'        => 'for.a.job[at]example.com',
        'job[is_public]'    => false,
    ));

    $client->submit($form);
    $this->assertEquals('Erlem\JobeetBundle\Controller\JobController::createAction', $client->getRequest()->attributes->get('_controller'));
}

Le navigateur simule aussi l'upload de fichiers si vous passez le chemin absolu du fichier à uploader.

Après avoir soumis le formulaire, nous avons vérifié que l'action exécutée est créée.

Test du formulaire

Si le formulaire est valide, l'offre doit être créée et l'utilisateur redirigé vers la page d'aperçu :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $client->followRedirect();
    $this->assertEquals('Erlem\JobeetBundle\Controller\JobController::previewAction', $client->getRequest()->attributes->get('_controller'));
}

Test de l'enregistrement dans la BDD

Finalement, nous voulons vérifier que l'offre a été créée dans la BDD et vérifier que la colonne is_activated est définie sur false lorsque l'utilisateur n'a pas encore publié.

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');

    $query = $em->createQuery('SELECT count(j.id) from ErlemJobeetBundle:Job j WHERE j.location = :location AND j.is_activated IS NULL AND j.is_public = 0');
    $query->setParameter('location', 'Atlanta, USA');
    $this->assertTrue(0 < $query->getSingleScalarResult());
}

Test d'erreurs

Le formulaire de création d'offre fonctionne comme prévu lorsque nous soumettons des valeurs valides. Ajoutons un test pour vérifier le comportement lorsque nous soumettons des données non valides :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testJobForm()
{
    // ...
    $crawler = $client->request('GET', '/job/new');
    $form = $crawler->selectButton('Preview your job')->form(array(
        'job[company]'      => 'Sensio Labs',
        'job[position]'     => 'Developer',
        'job[location]'     => 'Atlanta, USA',
        'job[email]'        => 'not.an.email',
    ));
    $crawler = $client->submit($form);

    // check if we have 3 errors
    $this->assertTrue($crawler->filter('.error_list')->count() == 3);

    // check if we have error on job_description field
    $this->assertTrue($crawler->filter('#job_description')->siblings()->first()->filter('.error_list')->count() == 1);

    // check if we have error on job_how_to_apply field
    $this->assertTrue($crawler->filter('#job_how_to_apply')->siblings()->first()->filter('.error_list')->count() == 1);

    // check if we have error on job_email field
    $this->assertTrue($crawler->filter('#job_email')->siblings()->first()->filter('.error_list')->count() == 1);
}

Maintenant, nous avons besoin de tester la barre d'administration sur la page de prévisualisation d'offre. Lorsqu'une offre n'a pas encore été activée, vous pouvez modifier, supprimer ou publier l'offre. Pour tester ces trois actions, il nous faudra d'abord créer une offre. Mais c'est beaucoup de copier/coller. Nous allons donc ajouter une méthode de création d'offre dans la classe JobControllerTest :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function createJob($values = array())
{
    $client = static::createClient();
    $crawler = $client->request('GET', '/job/new');
    $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
        'job[company]'      => 'Sensio Labs',
        'job[url]'          => 'http://www.sensio.com/',
        'job[position]'     => 'Developer',
        'job[location]'     => 'Atlanta, USA',
        'job[description]'  => 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' => 'Send me an email',
        'job[email]'        => 'for.a.job[at]example.com',
        'job[is_public]'    => false,
  ), $values));

    $client->submit($form);
    $client->followRedirect();

    return $client;
}

La méthode createJob() crée une offre, suit la redirection et retourne au navigateur. Vous pouvez aussi passer un tableau de valeurs qui seront fusionnées avec les valeurs par défaut.

Le test de l'action "Publier" est maintenant plus simple :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
public function testPublishJob()
{
    $client = $this->createJob(array('job[position]' => 'FOO1'));
    $crawler = $client->getCrawler();
    $form = $crawler->selectButton('Publish')->form();
    $client->submit($form);

    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');

    $query = $em->createQuery('SELECT count(j.id) from ErlemJobeetBundle:Job j WHERE j.position = :position AND j.is_activated = 1');
    $query->setParameter('position', 'FOO1');
    $this->assertTrue(0 < $query->getSingleScalarResult());
}

Tester l'action "Supprimer" est assez similaire :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testDeleteJob()
{
    $client = $this->createJob(array('job[position]' => 'FOO2'));
    $crawler = $client->getCrawler();
    $form = $crawler->selectButton('Delete')->form();
    $client->submit($form);

    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');

    $query = $em->createQuery('SELECT count(j.id) from ErlemJobeetBundle:Job j WHERE j.position = :position');
    $query->setParameter('position', 'FOO2');
    $this->assertTrue(0 == $query->getSingleScalarResult());
}

Les tests comme protection

Quand une offre est publiée, vous ne pouvez plus la modifier. Même si le lien "Modifier" ne s'affiche plus sur la page de prévisualisation, ajoutons quelques tests de cette exigence.

Tout d'abord, ajoutez un autre argument à la méthode createJob() pour permettre la publication automatique de l'offre, créez une méthode getJobByPosition() qui retourne une offre suivant la valeur de l'intitulé :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function createJob($values = array(), $publish = false)
{
    $client = static::createClient();
    $crawler = $client->request('GET', '/job/new');
    $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
        'job[company]'      => 'Sensio Labs',
        'job[url]'          => 'http://www.sensio.com/',
        'job[position]'     => 'Developer',
        'job[location]'     => 'Atlanta, USA',
        'job[description]'  => 'You will work with symfony to develop websites for our customers.',
        'job[how_to_apply]' => 'Send me an email',
        'job[email]'        => 'for.a.job[at]example.com',
        'job[is_public]'    => false,
  ), $values));

    $client->submit($form);
    $client->followRedirect();

    if($publish) {
      $crawler = $client->getCrawler();
      $form = $crawler->selectButton('Publish')->form();
      $client->submit($form);
      $client->followRedirect();
    }

  return $client;
}

public function getJobByPosition($position)
{
    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');

    $query = $em->createQuery('SELECT j from ErlemJobeetBundle:Job j WHERE j.position = :position');
    $query->setParameter('position', $position);
    $query->setMaxResults(1);
    return $query->getSingleResult();
}

Si une offre est publiée, la page de modification doit retourner un code d'état 404 :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testEditJob()
{
    $client = $this->createJob(array('job[position]' => 'FOO3'), true);
    $crawler = $client->getCrawler();
    $crawler = $client->request('GET', sprintf('/job/%s/edit', $this->getJobByPosition('FOO3')->getToken()));
    $this->assertTrue(404 === $client->getResponse()->getStatusCode());
}

Mais si vous exécutez les tests, vous n'aurez pas le résultat escompté, car nous avons oublié de mettre en œuvre cette mesure de sécurité hier. L'écriture des tests est aussi un excellent moyen de découvrir des bugs, que vous avez besoin de penser à tous les cas de pointe.

La correction du bogue est simple puisque nous avons juste besoin de transmettre à une page 404 si l'emploi est activé :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

public function editAction($token)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('ErlemJobeetBundle:Job')->findOneByToken($token);

    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }

    if ($entity->getIsActivated()) {
        throw $this->createNotFoundException('Job is activated and cannot be edited.');
    }

  // ...
}

Retour vers le futur dans le test

Quand une offre expire dans moins de cinq jours, ou si elle est déjà expirée, l'utilisateur peut étendre la validité de l'offre pour 30 jours à compter de la date actuelle.

Le test de cette exigence dans un navigateur n'est pas facile car la date d'expiration est automatiquement activée lorsque l'offre est créée pour 30 jours dans le futur. Ainsi, lors de l'obtention de la page de l'offre, le lien pour prolonger l'offre n'est pas présent. Bien sûr, vous pouvez adapter la date d'expiration dans la BDD ou modifier le modèle pour afficher en permanence le lien, mais c'est fastidieux et source d'erreurs. Comme vous l'avez déjà deviné, l'écriture de tests va nous aider une fois de plus.

Comme toujours, nous devons ajouter une nouvelle route pour la méthode extend en premier :

  • src/Erlem/JobeetBundle/Resources/config/routing/job.yml
# ...

erlem_job_extend:
    pattern:  /{token}/extend
    defaults: { _controller: "ErlemJobeetBundle:Job:extend" }
    requirements: { _method: post }

Ensuite, remplacez le lien "Extend" dans le template admin.html.twig avec le formulaire extend :

  • src/Erlem/JobeetBundle/Resources/views/Job/admin.html.twig
<!-- ... -->

{% if job.expiresSoon %}
    <form action={{ path('erlem_job_extend', { 'token': job.token }) }} method="post">
        {{ form_widget(extend_form) }}
        <button type="submit">Extend</button> for another 30 days
    </form>
{% endif %}

<!-- ... -->

Ensuite, créez l'action extend et le formulaire extend :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

public function extendAction(Request $request, $token)
{
    $form = $this->createExtendForm($token);
    $request = $this->getRequest();

    $form->bind($request);

    if($form->isValid()) {
        $em=$this->getDoctrine()->getManager();
        $entity = $em->getRepository('ErlemJobeetBundle:Job')->findOneByToken($token);

        if(!$entity){
            throw $this->createNotFoundException('Unable to find Job entity.');
        }

        if(!$entity->extend()){
            throw $this->createNodFoundException('Unable to extend the Job');
        }

        $em->persist($entity);
        $em->flush();

        $this->get('session')->getFlashBag()->add('notice', sprintf('Your job validity has been extended until %s', $entity->getExpiresAt()->format('m/d/Y')));
    }

    return $this->redirect($this->generateUrl('erlem_job_preview', array(
        'company' => $entity->getCompanySlug(),
        'location' => $entity->getLocationSlug(),
        'token' => $entity->getToken(),
        'position' => $entity->getPositionSlug()
    )));
}

private function createExtendForm($token)
{
    return $this->createFormBuilder(array('token' => $token))
        ->add('token', 'hidden')
        ->getForm();
}

En outre, ajoutez le formulaire extend à l'action preview :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

public function previewAction($token)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('ErlemJobeetBundle:Job')->findOneByToken($token);

    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }

    $deleteForm = $this->createDeleteForm($entity->getId());
    $publishForm = $this->createPublishForm($entity->getToken());
    $extendForm = $this->createExtendForm($entity->getToken());

    return $this->render('ErlemJobeetBundle:Job:show.html.twig', array(
        'entity'      => $entity,
        'delete_form' => $deleteForm->createView(),
        'publish_form' => $publishForm->createView(),
        'extend_form' => $extendForm->createView(),
    ));
}

Comme prévu par l'action, la méthode extend() de Job retourne true si l'offre a été prolongée ou false sinon :

  • src/Erlem/Jobeetbundle/Entity/Job.php
// ...

public function extend()
{
    if (!$this->expiresSoon())
    {
        return false;
    }

    $this->expires_at = new \DateTime(date('Y-m-d H:i:s', time() + 86400 * 30));

    return true;
}

Enfin, ajoutez un scénario de test :

  • src/Erlem/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...

public function testExtendJob()
{
    // A job validity cannot be extended before the job expires soon
    $client = $this->createJob(array('job[position]' => 'FOO4'), true);
    $crawler = $client->getCrawler();
    $this->assertTrue($crawler->filter('input[type=submit]:contains("Extend")')->count() == 0);

    // A job validity can be extended when the job expires soon

    // Create a new FOO5 job
    $client = $this->createJob(array('job[position]' => 'FOO5'), true);

    // Get the job and change the expire date to today
    $kernel = static::createKernel();
    $kernel->boot();
    $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
    $job = $em->getRepository('ErlemJobeetBundle:Job')->findOneByPosition('FOO5');
    $job->setExpiresAt(new \DateTime());
    $em->flush();

    // Go to the preview page and extend the job
    $crawler = $client->request('GET', sprintf('/job/%s/%s/%s/%s', $job->getCompanySlug(), $job->getLocationSlug(), $job->getToken(), $job->getPositionSlug()));
    $crawler = $client->getCrawler();
    $form = $crawler->selectButton('Extend')->form();
    $client->submit($form);

    // Reload the job from db
    $job = $this->getJobByPosition('FOO5');

    // Check the expiration date
    $this->assertTrue($job->getExpiresAt()->format('y/m/d') == date('y/m/d', time() + 86400 * 30));
}

Tâches de maintenance

Même si Symfony est un framework web, il est livré avec un outil de ligne de commande. Vous l'avez déjà utilisé pour créer la structure de répertoire par défaut du paquet de l'application et générer des fichiers divers pour le modèle. Ajouter une nouvelle commande est assez facile.

Lorsqu'un utilisateur crée une offre, il faut l'activer pour la mettre en ligne. Sinon, la BDD grandira avec de vieilles offres. Nous allons créer une commande qui supprime de la BDD les vieilles offres. Cette commande devra être exécutée régulièrement dans une tâche cron.

  • src/Erlem/JobeetBundle/Command/JobeetCleanupCommand.php
<?php

namespace Erlem\JobeetBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Erlem\JobeetBundle\Entity\Job;

class JobeetCleanupCommand extends ContainerAwareCommand {

  protected function configure()
  {
      $this
          ->setName('erlem:jobeet:cleanup')
          ->setDescription('Cleanup Jobeet database')
          ->addArgument('days', InputArgument::OPTIONAL, 'The email', 90)
    ;
  }

  protected function execute(InputInterface $input, OutputInterface $output)
  {
      $days = $input->getArgument('days');

      $em = $this->getContainer()->get('doctrine')->getManager();
      $nb = $em->getRepository('ErlemJobeetBundle:Job')->cleanup($days);

      $output->writeln(sprintf('Removed %d stale jobs', $nb));
  }
}

Vous devrez ajouter la méthode cleanup à la classe JobRepository :

  • src/Erlem/JobeetBundle/Repository/JobRepository.php
// ...

public function cleanup($days)
{
    $query = $this->createQueryBuilder('j')
        ->delete()
        ->where('j.is_activated IS NULL')
        ->andWhere('j.created_at < :created_at')    
        ->setParameter('created_at',  date('Y-m-d', time() - 86400 * $days))
        ->getQuery();

    return $query->execute();
}

Pour lancer la commande, exécutez la commande suivante à partir du dossier de projet :

php app/console erlem:jobeet:cleanup

ou

php app/console erlem:jobeet:cleanup 10

pour supprimer de vieilles offres de plus de 10 jours.


Symfony2 - Jour 10 - Les formulaires
[Sommaire] Symfony2 - Jour 12 - Le paquet Admin >


Pour ce tutoriel, je me suis inspiré du tutoriel Symfony2 Jobeet du site IntelligentBee

Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn