Tests


1Présentation

Pour éviter les régressions lors des développements, il est conseillé d'écrire des tests automatisés. Il existe plusieurs frameworks de tests en PHP, le plus couramment utilisé étant PHPUnit.

Vous pouvez utiliser PHPUnit directement pour tester unitairement vos objets. Vous pourriez alors avoir besoin de créer un environnement particulier qui permette d'exécuter ces tests.

Temma permet d'écrire facilement des tests d'intégration. Un test d'intégration vérifie que ce que l'on obtient en sortie d'une action est correct, en fonction des paramètres fournis en entrée.


2Installation de PHPUnit

Il existe plusieurs moyens d'installer PHPUnit (cf. documentation). Nous passerons ici par le téléchargement de l'archive PHAR, que nous copierons dans le répertoire bin/ du projet, et que nous rendrons exécutable :

$ wget https://phar.phpunit.de/phpunit-10.phar -O bin/phpunit
$ chmod +x bin/phpunit

Vous pouvez vérifier que tout s'est bien passé en tapant la commande suivante :

$ bin/phpunit --version
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.

3Principe de fonctionnement

L'objet \Temma\Web\Test permet de lancer des requêtes qui vont exécuter directement les actions des contrôleurs demandés, sans passer par un serveur HTTP. Suivant le type de requête effectuée, plusieurs types de retours sont possibles :

  • Le flux de sortie généré par la vue. Par défaut, ce sera le flux HTML généré par la vue Smarty, mais cela peut aussi être un flux JSON, CSV, RSS
  • Les variables de template positionnées par le contrôleur et les plugins.
  • Un objet de type \Temma\Web\Response, qui permet de récupérer des informations précises sur l'exécution (redirection, code d'erreur HTTP, vue, template).
  • Le composant d'injection de dépendances (le "loader"), qui contient tous les objets instanciés par Temma et par le code applicatif.

La plupart du temps, la récupération des variables de template permet de vérifier que le code applicatif a fonctionné tel que prévu.

L'utilisation du flux de sortie peut être utile pour vérifier la non-régression sur des éléments-clés d'une page HTML.

L'objet de réponse peut se révéler nécessaire dans le cas de vérification pointues.

Enfin, le composant d'injection de dépendances offre un contrôle complet sur le résultat de l'exécution de la requête.


4Contrôleur d'exemple

Imaginons un contrôleur servant à afficher une liste d'article et le contenu des articles (fichier controllers/Article.php) :

/** Contrôleur dédié aux articles. */
class Articles extends \Temma\Web\Controller {
    /** Affiche la liste des articles. */
    public function list() {
        $this['articles'] = $this->_loader->ArticleDao->getList();
    }
    /** Affiche le contenu d'un article. */
    public function show(int $articleId) {
        $this['article'] = $this->_loader->ArticleDao->get($articleId);
        if (!$this['articles'])
            $this->_redirect('/article/list');
    }
}

Pour l'action list, nous avons le template suivant (fichier templates/articles/list.tpl) :

<html>
<body>
    <h1>Articles</h1>
    <ul>
        {foreach $articles as $article}
            <li>{$article.title|escape}</li>
        {/foreach}
    </ul>
</body>

Pour l'action show, nous avons le template suivant (fichier templates/articles/show.tpl) :

<html>
<body>
    <h1>{$article.title|escape}</h1>
    {$article.html}
</body>

5Création d'un test

Nous allons créer un objet contenant deux tests d'intégration (un test par action du contrôleur) dans le fichier tests/ArticlesTest.php :

<?php

class ArticlesTest extends \PHPUnit\Framework\TestCase {
    /** Objet de gestion des tests Temma. */
    private \Temma\Web\Test $_test;

    /** Initialisation. */
    public function setUp() : void {
        $this->_test = new \Temma\Web\Test();
    }
    /** Test de l'action 'list'. */
    public function testList() {
        $data = $this->_test->execData('/articles/list');
        $this->assertIsArray($data['articles'] ?? null);
        $this->assertNotEmpty($data['articles']);
    }
    /** Test de l'action 'show'. */
    public function testShow() {
        $data = $this->_test->execData('/articles/show/1');
        $this->assertEquals(1, ($data['article']['id'] ?? null));
    }
}
  • Ligne 3 : Le nom de l'objet doit avoir le suffixe Test, et il doit hériter de l'objet \PHPUnit\Framework\TestCase.
  • Lignes 8 à 10 : La méthode setUp() est appelée à l'initialisation de l'objet, avant que les tests soient exécutés. Ici, on instancie l'objet de test fourni par Temma, et qui sera utilisé par la suite.
  • Lignes 12 à 16 : Test de l'action list.
    • Ligne 13 : On lance une requête sur l'URL /articles/list, qui doit normalement afficher la liste des articles, et on récupère un tableau contenant toutes les variables de template.
    • Ligne 14 : On vérifie que la variable de template articles existe et que c'est un tableau.
    • Ligne 15 : On vérifie que cette variable n'est pas vide.
  • Lignes 18 à 21 : Test de l'action show.
    • Ligne 19 : On lance une requête sur l'URL /articles/show/1, qui doit normalement afficher le contenu d'un article, et on récupère un tableau contenant toutes les variables de template.
    • Ligne 20 : On vérifie que la variable de template article existe, qu'elle contient une clé id, et que la valeur associée à cette clé vaut 1.

6Exécution du test en ligne de commande

Pour exécuter tous les tests qui auront été écrits dans le répertoire tests/, il suffit d'exécuter la commande suivante :

$ bin/phpunit --bootstrap tests/autoload.php tests

Vous devriez en retour avoir un affichage de ce type :

PHPUnit 10.5.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.2-1ubuntu2.14

..                                                                  2 / 2 (100%)

Time: 00:00.003, Memory: 22.57 MB

OK (2 tests, 5 assertions)

Il est aussi possible d'exécuter un seul test spécifique :

$ bin/phpunit --bootstrap tests/autoload.php tests/ArticlesTest.php

7Appels des URLs

Quatre méthodes sont disponibles pour lancer l'exécution d'un test :

  • execOutput() retourne le flux de sortie de la vue.
  • execData() retourne les variables de template ou la chaîne de redirection.
  • execResponse() retourne un objet \Temma\Web\Response.
  • execLoader() retourne le composant d'injection de dépendances créé pour l'exécution de la requête.

Ces méthodes peuvent toutes prendre quatre paramètres :

  1. string $url : (obligatoire) URL à appeler, en commençant par un caractère "slash" (/).
  2. string $httpMethod : (optionnel) La méthode HTTP de la requête. Par défaut, c'est la méthodde GET.
  3. ?array $data : (optionnel) Tableau associatif contenant les paramètres GET ou POST à transmettre. Le type (GET ou POST) est déterminé par le paramètre $httpMethod.
  4. ?array $cookies : (optionnel) Tableau associatif contenant les cookies à transmettre.

7.1Récupération du flux de sortie

La méthode execOutput() permet de récupérer le flux généré par la vue. Cette méthode peut retourner une chaîne vide.

Exemple :

$html = $this->_test->execOutput('/articles/show/1');

// vérification simple
$this->assertStringContainsString('<h1>', $html);

// vérification avec une expression régulière
$this->assertMatchesRegularExpression('/<h1>.+<\/h1>/', $html);

7.2Récupération de données

Nous avons vu plus haut la méthode execData(), qui retourne les variables de template qui ont été définies par le contrôleur et les plugins.

En fait, cette méthode est susceptible de retourner trois différents types de données :

  • Un tableau associatif, contenant les variables de template.
  • Une chaîne de caractères, si une redirection a été définie. Dans ce cas, c'est l'URL de redirection qui est retournée.
  • La valeur null, si l'exécution a été interrompue.

Exemple :

$data = $this->_test->execData('/articles/show/1');

// ne doit pas être nul
$this->assertNotNull($data, "Exécution interrompue.");

// si c'est une redirection, c'est que l'article n'existe pas
$this->assertIsNotString($data, "Article inconnu.");

// on vérifie l'identifiant de l'article
$this->assertEquals(1, ($data['article']['id'] ?? null));

7.3Récupération de la réponse

La méthode execResponse() permet de récupérer l'objet de type \Temma\Web\Response qui a été créé pendant l'exécution de la requête. Cet objet offre les getters suivants :

  • getRedirection() : Retourne la chaîne de caractère de redirection, ou null si aucune redirection n'a été demandée.
  • getRedirectionCode() : Retourne le code de redirection (301 ou 302).
  • getHttpError() : Retourne le code d'erreur HTTP, ou null.
  • getHttpCode() : Retourne le code HTTP de la réponse (200 par défaut).
  • getView() : Retourne le nom de la vue, ou null si elle n'a pas été définie.
  • getTemplatePrefix() : Retourne le préfixe qui est ajouté au début des chemins de templates, ou null.
  • getTemplate() : Retourne le chemin du template utilisé, ou null.
  • getHeaders() : Retourne la liste des en-têtes HTTP définis.
  • getData() : Retourne un tableau associatif contenant les variables de template définies par le contrôleur et les plugins.

Il est aussi possible d'accéder à une variable de template directement avec une syntaxe de type tableau.

Exemple :

$response = $this->_test->execResponse('/articles/show/1');

// s'il y a une redirection, c'est que l'article n'existe pas
$redir = $response->getRedirection();
$this->assertNull($redir, "Article inconnu.");

// s'il y a un code HTTP différent de 200, c'est qu'il y a une erreur
$code = $response->getHttpCode();
$this->assertEquals(200, $code, "Erreur de traitement.");

// la vue devrait être la vue Smarty
$view = $response->getView();
$this->assertEquals('\Temma\Views\Smarty', $view, "Vue incorrecte.");

// on devrait récupérer la variable de template contenant l'article
$this->assertEquals(1, ($response['article']['id'] ?? null));

7.4Récupération du composant d'injection de dépendances

À chaque requête effectuée, un composant d'injection de dépendances est créé, qui contient les objets instanciés par Temma, mais aussi ceux utilisés par votre code applicatif (si vous avez utilisé le composant).

La méthode execLoader() retourne l'objet de type \Temma\Base\Loader qui a été créé pendant l'exécution de la requête. Ce composant contient au minimum les objets suivants :

  • config : L'objet de configuration (type \Temma\Web\Config) généré à partir du fichier de configuration etc/temma.php.
  • request : L'objet de requête (type \Temma\Web\Request) généré à partir de l'URL demandée.
  • response : L'objet de réponse (type \Temma\Web\Response), décrit dans la documentation de la méthode execResponse() ci-dessus.
  • session : L'objet de gestion de la session (type \Temma\Base\Session), qui permet notamment de récupérer l'identifiant de session qui est enregistré en cookie.

7.5Paramètres GET et POST

Pour envoyer des paramètres GET ou POST, indiquez la méthode à utiliser en deuxième paramètre, et passez un tableau associatif contenant les paramètres en troisième paramètre.

Exemple :

$html = $this->_test->execOutput('/articles/create', 'POST', [
    'title' => "Nouvel article",
    'html'  => "<p>bla bla bla</p>",
]);

8Configuration

8.1Définition du fichier de configuration

Par défaut, Temma se débrouille pour trouver le chemin vers le fichier de configuration etc/temma.php. Cela permet d'utiliser la même configuration que pour vos développements (même base de données, mêmes plugins, etc.).

Mais parfois, il peut être souhaitable d'utiliser un fichier de configuration différent, par exemple pour se connecter à une base de données spécifique aux tests, ou pour activer/désactiver certains plugins.
Dans ce cas, il faut fournir deux paramètres à la création de l'objet \Temma\Web\Test :

  1. ?string $appPath : Chemin vers la racine du projet.
  2. ?string $configPath : Chemin vers le fichier de configuration.

Exemple :

$appPath = '/opt/mon_projet';
$configPath = "$appPath/etc/temma-test.php";
$this->_test = new \Temma\Web\Test($appPath, $configPath);

8.2Définition du composant d'injection de dépendances

Lors de vos tests, vous pouvez vouloir faire en sorte de "mocker" certains objets, c'est-à-dire de forcer l'utilisation d'un objet de substitution au moment où le code applicatif va vouloir appeler un objet donné. Cela peut être utile pour éviter que certains traitements soient effectués, comme la connexion à un service externe.

Dans ce cas, il faut préparer un composant d'injection de dépendances auquel vous aurez fourni l'objet de substitution, en lui donnant le nom sous lequel l'objet initial est appelé. Ensuite, le composant doit être fourni en paramètre au constructeur de l'objet \Temma\Web\Test (troisième paramètre, ou paramètre nommé loader).

Exemple :

// fausse classe "ArticleDao"
class MockArticleDao {
    /** Retourne une fausse liste d'articles. */
    public function getList() {
        return [
            ['id' => 1, 'title' => 'Titre 1'],
            ['id' => 2, 'title' => 'Titre 2'],
            ['id' => 3, 'title' => 'Titre 3'],
        ];
    }
    /** Retourne un faux article. */
    public function get(int $id) {
        return [
            'id'    => $id,
            'title' => "Titre $id",
            'html'  => "<p>bla bla bla</p>",
        ];
    }
}

// création de l'objet loader
$loader = new \Temma\Base\Loader([
    'ArticleDao' => new MockArticleDao(),
]);

// lancement d'un test en utilisant ce loader
$this->_test = new \Temma\Web\Test(loader: $loader);

Attention, il est possible de surcharger le composant d'injection de dépendances dans le fichier etc/temma.php, avec la directive loader (voir la documentation de la configuration).

Le cas échéant, pensez à utiliser l'objet défini dans la configuration, ou un autre objet surchargé qui aura un comportement similaire lorsqu'il sera utilisé par le code applicatif.


9Gestion des utilisateurs

9.1Sessions

Les sessions utilisateurs sont créées automatiquement par Temma, et passent par l'enregistrement d'un cookie de session sur le navigateur. Dans le cadre des tests automatisés, si vous souhaitez tester un enchaînement de pages qui nécessitent une session, il va falloir récupérer l'identifiant de session généré lors de l'appel à la première page, puis le transmettre aux pages suivantes.

$test = new \Temma\Web\Test();

// appel de la première page
$loader = $test->execLoader('/page1');

// récupération de l'identifiant de session
$sessionId = $loader->session->getSessionId();
// création du tableau des cookies
$cookies = ['TemmaSession' => $sessionId];

// appel des pages suivantes
$data = $test->execData('/page2', 'GET', null, $cookies);
...

Le nom du cookie de session ("TemmaSession" dans l'exemple ci-dessus) doit être le même que celui indiqué dans la variable sessionName du fichier de configuration etc/temma.php (voir la documentation de la configuration).


9.2Authentification de l'utilisateur de test

Votre site web peut avoir certaines pages qui ne sont accessibles qu'aux utilisateurs connectés. Pour cela, vous pouvez utiliser le plugin/contrôleur Auth et l'attribut Auth, fournis par Temma.

Dans ce cas, il faut commencer par procéder à l'authentification d'un utilisateur, puis passer le cookie de session de page en page.

$appPath = '/opt/mon_projet';
$configPath = "$appPath/etc/temma-test.php";
$test = new \Temma\Web\Test($appPath, $configPath);

// authentification
$loader = $test->execLoader('/auth/authentication', 'POST', [
    'email' => $email,
]);
$token = $loader->response['token'];
$sessionId = $loader->session->getSessionId();

// validation du token
$cookies = ['TemmaSession' => $sessionId];
$loader = $test->execLoader("/auth/check/$token", 'GET', null, $cookies);

// test d'une page accessible uniquement aux utilisateurs authentifiés
$html = $test->execOutput("/account", 'GET', null, $cookies);
$this->assertStringContainsString('Mon compte', $html);

Pour que cette authentification fonctionne correctement, il faut que le fichier de configuration contienne la directive robotCheckDisabled. Il est aussi recommandé de désactiver l'envoi des messages de connexion :

<?php

return [
    'x-security' => [
        'auth' => [
            'robotCheckDisabled' => true
        ]
    ],
    'x-email' => [
        'disabled' => true
    ]
];