Mokona Guu Center

Utilisation de pexpect pour les tests

Publié le

Lorsque l'on veut tester des applications de manière automatique, arrive toujours le moment où l'on voudrait tester l'application dans son ensemble, en tant qu'utilisateur, mais selon en suivant des scripts.

C'est pratique pour dérouler les plans de tests aux niveaux les plus simples (est-ce que mon application de lance correctement par exemple), mais aussi dans de la programmation dirigées par des tests de comportement (Behavior Driven Developement).

Si les applications Web ont leurs frameworks (Selenium par exemple) munies d'outils et d'API pour les langages de programmation les plus populaires, quand on commence à toucher à de l'interface utilisateur d'application de bureau, ça se complique.

Côté graphique (GUI), je ne connais rien qui soit standard. Windows a AutoIt. Différentes bibliothèques ont leurs solutions (Qt, GTK+, pour .Net, pour Java...).

Pour mon projet le plus récent, j'étais plutôt parti sur une interface en mode texte afin de tester les fonctionnalités, le tout piloté par Behave. Behave étant un framework Python, j'ai commencé par essayer de dialoguer avec le processus sous-jacent via le module par défaut subprocess.

Ce module est assez pratique pour lancer un processus et récupérer des résultats. Mais on se retrouve très vite limité lorsqu'il s'agit d’interagir, c'est-à-dire attendre des moments clés pour envoyer sur l'entrée standard les commandes suivantes.

C'est ici qu'intervient le package pexpect.

Dans les grandes lignes, pexpect lance un processus sur lequel il garde un canal de communication. L'utilisation en est très simple :

    import pexpect

    exec_path = "path/to/the/executable"

    p = pexpect.spawn(exec_path)
    p.expect("Your choice?")
    p.sendline("my choice")

Lignes de base auxquels on ajoutera le traitement d'exception (est-ce que le programme s'est arrêté avant que la chaîne attendue n'apparaisse ?) ou encore un timeout éventuel pour expect.

Les fonctionnalités vont un peu plus loin, avec de la recherche par expression régulière, la vérification que le processus est toujours vivant, le terminer et attendre qu'il se termine afin de récupérer le statut de sortie.

Mais c'est lent !

Alors que mon nombre de tests augmentait, je me suis aperçu que le temps de réponse de behave augmentait de manière inquiétante. J'étais alors sur un portable un peu daté, sans connexion internet stable. Le temps de réponse restant acceptable (environ 10 secondes tout de même pour une vingtaine de scenarii en un peu plus de 100 étapes) j'ai remis à plus tard mes recherches sur le sujet.

De retour sur un ordinateur un poil plus véloce, je lance les même tests et... obtient le même temps de réponse. L'intuition m'envoie alors vers des problématiques de spawn de process, voire, vu le temps relativement stable d'un ordinateur à l'autre, vers des timers prédéfinis.

Et c'est bien cette dernière raison qui est la bonne. En regardant les sources du package, j'arrive vite sur des commentaires indiquant ces fameux timers et leur raison d'exister.

Il y a plusieurs délais :

  • avant d'envoyer un message (via sendline())
  • après avoir fermé la connexion au process
  • après que le process se termine

Les sources indiquent les raisons, qui sont des problèmes de timing d'OS ou d'echo de ce que l'on envoie par rapport à ce que l'on reçoit par la suite.

Ces valeurs peuvent se modifier après le spawn() d'un process très facilement. À chacun de vérifier l'influence par rapport aux tests écrits. Dans mon cas de behave, baisser ces valeurs très fortement n'a provoqué aucun échec.

Les tests passent par contre de 10 secondes à 1/5ième de secondes (un peu plus sur le portable), ce qui est nettement plus acceptables.

Voici les valeurs que j'utilise :

    import pexpect

    exec_path = "path/to/the/executable"

    p = pexpect.spawn(exec_path)
    p.delaybeforesend = 0.001
    p.delayafterclose = 0.002
    p.delayafterterminate = 0.002