Si vous ne l’avez pas fait, je vous invite à lire l’article Tester son code avec Behat, il détaille l’installation de Behat.

Pour aller plus loin avec Behat, vous avez besoin de comprendre le langage Gherkin. Cela passe par la compréhension des contexts et la possibilité d’utiliser des fonctionnalités de Symfony dans les tests.

Définitions

Les phrases de Behat sont définies dans des contexts. Ce sont des classes dont les méthodes définissent :

  • les phrases en anglais, qui seront utilisées dans les scénarios, ainsi que les variables définies par celles-ci
  • le code qui sera à exécuter pour cette phrase

Exemple de la classe MinkContext:

<?php

class MinkContext extends RawMinkContext implements TranslatableContext
{
    // ...
    
    /**
     * Checks, that current page response status is equal to specified
     * Example: Then the response status code should be 200
     * Example: And the response status code should be 400
     *
     * @Then /^the response status code should be (?P<code>\d+)$/
     */
    public function assertResponseStatus($code)
    {
        $this->assertSession()->statusCodeEquals($code);
    }
    
    // ...
}

Les annotations servent à définir l’orthographe de la phrase et les variables, sous la forme d’une expression régulière. Vous pouvez utiliser @When, @Given, @Then, @And ou @But. Behat ne fait pas de différence entre ces mots clefs qu’on retrouve en début de phrase, si vous avez défini une phrase avec @Given, vous pouvez très bien l’utiliser avec @When, il s’agit plus d’avoir des étapes qui s’approchent d’un paragraphe bien écrit en anglais.

Quand la phrase va être appelée, alors la méthode sera exécutée. Il est possible de nommer les parties capturées, ici code qui est entre parenthèses, le nom sera alors utilisé pour la correspondance avec les arguments de la méthode.

Dans l’exemple, on va vérifier le code de retour du dernier appel effectué avec celui capturé à la fin de la phrase.

Récupérer des contextes

MinkContext de l’extension Mink vous fournit du vocabulaire de base pour tester des requêtes HTTP et leur réponse. Il propose des phrases pour tester le corps de la réponse, en cherchant la présence, ou non, d’un texte spécifique ou d’une balise HTML par exemple.

Pour pouvoir utiliser les phrases qu’il propose, vous devez l’activer dans behat.yml:

default:
  suites:
    default:
      contexts:
        - Behat\MinkExtension\Context\MinkContext
  extensions:
    Behat\MinkExtension: ~

Vous pouvez également trouver des contextes open source adaptés à vos besoins. Il existe notamment Behatch qui fournit notamment la possibilité d’avoir des assertions sur des documents JSON, très pratique dans le cadre de tests d’une API.

Ces contextes peuvent généralement être récupéré via composer, exemple:

composer require --dev behatch/contexts

Puis activer les contextes souhaités :

default:
  suites:
    default:
      contexts:
        - Sanpi\Behatch\Context\JsonContext
  extensions:
    Sanpi\Behatch\Extension: ~

Comment gérer mon test particulier ?

Une entité, son statut et des évènements

Admettons que vous ayez une API avec une entité ayant un statut et que vous envoyez des événements à l’application qui font évoluer ce statut. Quand vous envoyez l’événement, l’API vous retourne une réponse 200 avec l’identifiant de l’événement. Il n’est pas possible pour vous de savoir directement quel est le statut de l’entité.

Si vous rédigez des tests, vous allez tester le contrôleur qui retourne le contenu de l’entité, puis dans un autre jeu de tests, vous allez tester le contrôleur qui reçoit les évènements. Dans les tests des évènements, vous aurez à tester si tel évènement provoque bien le passage à tel statut pour l’entité. Hors si vous faites en sorte, après chaque test d’envoi d’évènement, d’envoyer une requête vers le contrôleur qui retourne l’entité pour tester ensuite son statut, vous doublez le nombre de requêtes, ce qui signifie des tests plus lents, et vous testez a nouveau, indirectement, le contrôleur qui retourne l’entité.

Afin de garder des tests rapides et de ne tester que l’essentiel, vous pouvez ajouter un contexte avec une phrase qui vous permettra de faire l’équivalent de : “Vérifier que mon entité ayant l’id XXXX a le statut YYYY”.

Configuration pour un nouveau contexte

Les contextes Behat peuvent recevoir les services de Symfony. À partir de là vous pouvez aisément récupérer les services qui s’occupent de la sécurité afin de générer un token valide pour l’authentification sur API ou dans notre cas, récupérer Doctrine qui nous permettra d’effectuer une requête SQL pour valider les données.

Pour ajouter un contexte, vous pouvez commencer par le déclarer dans le fichier behat.yml :

default:
  suites:
    default:
      contexts:
        - AppBundle\Tests\Behat\Context\ArticleContext: {registry: '@doctrine'}

Vous pouvez remarquer qu’on lui passe le service par son identifiant. La classe ArticleContext, notre futur contexte, va recevoir les éléments passés dans l’accolade dans son constructeur, sur le même principe que les services de Symfony.

Le contexte et la phrase

Maintenant que le contexte est déclaré, on peut ajouter la classe :

<?php

namespace AppBundle\Tests\Behat\Context;

// ...

class ArticleContext extends BaseContext implements Context
{
    /** @var RegistryInterface */
    protected $registry;

    /**
     * @param RegistryInterface $registry
     */
    public function __construct(RegistryInterface $registry)
    {
        $this->registry = $registry;
    }
    
    /**
     * @Then the article ":articleId" must have the status ":status"
     *
     * @param int $articleId
     * @param string $status
     */
    public function theArticleMustHaveTheStatus($articleId, $status)
    {
        /** @var Article $article */
        $article = $this->registry->getRepository('AppBundle:Article')->findOneById($articleId);

        $this->assertTrue(!empty($article), 'The article ID is not found.');
        $this->assertSame($status, $article->getStatus());
    }
}

On peut voir que le contexte étend BaseContext qui vous fournit de quoi effectuer les assertions et quelques méthodes de recherche d’éléments HTML dans le corps d’une réponse HTTP. BaseContext est fourni par Behatch.

Le registre Doctrine est bien injecté dans le constructeur.

Ensuite on ajoute la méthode qui contient la logique et les vérifications qu’il faut exécuter. Ici, on a défini la phrase après une annotation @Then. Les mots commençants par : vont capturer les valeurs saisies dans les features et injectées dans le paramètre ayant le nom correspondant. Les guillemets sont optionnels, mais aident à avoir une compréhension intuitive lors de la rédaction des scénarios.

La méthode est basique, on sélectionne l’entité puis on vérifie si elle existe et si la valeur du statut correspond bien à la valeur attendue.

Ainsi au lieu de faire une requête à l’API qui passerait par les couches de sécurité, par les vérifications, par le sérialiseur, une simple requête est effectuée en base de données. On pourrait simplifier encore plus la chose en effectuant une requête directement en SQL en passant par la connexion plutôt que d’utiliser le repository de l’entité.

La feature

Tout est maintenant prêt pour l’utilisation de la phrase dans un test :

@articles
Feature: Articles
  Scenario: Send a INVALIDATE event
      Given the article "1" must have the status "published"
      Given I send a POST json request to "/article/1/events" with body:
      """
      {
        "reason": "MyReason",
        "type": "INVALIDATE"
      }
      """
      Then the response status code should be 202
      And the response should be empty
      And the article "1" must have the status "draft"

On envoie donc un évènement, qui a une action sur l’article d’id 1. On vérifie avant et après l’envoi de l’évènement le statut de l’article avec notre nouvelle phrase.

Il ne reste plus qu’à lancer les tests pour effectuer les vérifications.

Conclusions

Afin de garder les tests pertinents et rapides, n’hésitez pas à rajouter des contextes et des phrases spécialisées. Je vous invite à lire la documentation sur les contextes pour apprendre les possibilités supplémentaires.