Assurance qualité

Auteur:Julien Jehannet <julien.jehannet@logilab.fr>
Date: 2008-05-17
Niveau:Débutant / Confirmé

Le code est fait pour être lu : quelques pistes pour améliorer la lisibilité et la maintenabilité de votre code python.

Cette présentation parle de quelques bonnes pratiques destinées à mettre toutes les chances de votre côté pour que la maintenance de vos applications ne devienne pas rapidement un cauchemar.

Nous allons au préalable définir rapidement ce qu'est le but de l'assurance qualité; puis nous rappellerons brièvement les principes de bases que tous les développeurs python doivent utiliser. Nous passerons ensuite en revue les outils standards de python pour faciliter la mise au point avant de lister des projets plus ambitieux mais indispensables pour la création de code de qualité.

Une définition de l'assurance qualité

Pour satisfaire aux contraintes de qualité et fonctionnalités, l'assurance qualité doit viser un contrôle constant sur :

La maîtrise de l'assurance qualité permet au chef de projet (et a fortiori l'équipe de développement) de proposer un nombre de fonctionnalités croissantes (sans régression) dans un cadre stable et évolutif pour la suite du projet.

Cette maîtrise ne peuvent être obtenue que grâce à un code testé en continu et dont les effets sont reproductibles à volonté.

Principes de bases d'un code maintenable

« Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retrancher. »

Antoine de Saint-Exupéry

Outils standards offerts par Python

« A clever person solves a problem. A wise person avoids it. »

Albert Einstein

Les indispensables:

Outils standards offerts par Python : logging

le module logging

Le module logging contient des fonctions et des classes construisant un système de journalisation flexible pour vos applications

Un logger peut être associé à une sévérité lui indiquant d’ignorer les messages d’importance inférieure à ce niveau

Intérêts

L’envoi d’un message est effectué en appelant une méthode sur une instance de la classe Logger. Ces instances possèdent un nom et sont arrangées selon une hiérarchie en utilisant . comme séparateur Un message est associé à une sévérité (par défaut DEBUG, INFO, WARNING, ERROR ou CRITICAL)

Un logger est associé à une ou plusieurs instances de la classe Handler, qui sont responsables de faire parvenir les messages à une destination particulière

Les handlers permettent une configuration plus poussées de la journalisation: - utilisation de NTEventLogHandler sous Windows - utilisation de SMTPHandler lors d'erreurs critiques seulement (filtre alors sur la sévérité) - utilisation d'un handler pour l'affichage dans une fenêtre d'application

Il est évidemment possible de changer le formattage des messages Vous pouvez mettre au point des règles précise de filtrage avec classe Filter

Tout est configurable par fichier .ini ce qui permet à un administrateur de modifier le comportement sans toucher au code

Outils standards offerts par Python : logging

Exemple simple:

import logging
# Python >= 2.4 uniquement
logging.basicConfig(level=logging.INFO,
                    format=’%(asctime)s %(levelname)s\t\
                            %(message)s’)
logging.debug(’Un message de debogage’)
logging.info("De l’information: %s", ’et hop !’)
logging.warning(’Attention’)
logger = logging.getLogger(’monappli’)
logger.debug(’Un message de debogage de mon appli’)
logger.info("Plus d’info: %s", ’hop hop hop!’)

donne à l’exécution :

2005-11-17 17:24:45,299 INFO De l’information: et hop !
2005-11-17 17:24:45,309 WARNING Attention
2005-11-17 17:24:45,319 INFO Plus d’info: hop hop hop!

Pour des exemples plus évolués:

http://www.red-dove.com/python_logging.html

Outils standards offerts par Python : pydoc

« Programs must be written for people to read, and only incidentally for machines to execute. »

Abelson and Sussman

le module pydoc

Les outils de documentation se basent sur les docstrings qui sont des chaînes de documentation insérées dans le code source de vos programmes python.

La fonction builtin help() utilise le module pydoc et permet d’obtenir de l’aide de manière interactive sur un objet vivant

pydoc est également un script utilisable dans une console shell à la manière des pages de man sous Unix

Intérêts

Les docstrings se placent sur la première ligne suivant les entêtes de classe ou de fonction ou en tête des fichiers contenant des modules. Ce sont de simples textes.

Voir http://www.python.org/dev/peps/pep-0257/

Le module doctest recherche des éléments de textes correspondant à du code en mode intéractif et vérifie son exécution avec le résultat attendu.

En utilisant pydoc, vous vous forcer implicitement à écrire des docstring de qualité car vous savez que ce sera relu

Démonstration du rendu avec "pydoc pydoc"

L'outil epydoc est similaire mais permet de documenter plus en détails avec une génération de graphes de dépendances par exemple.

Outils standards offerts par Python : unittest

« Let the machine do the dirty work »

B. W. Kernighan and P. J. Plauger, The Elements of Programming Style

le module unittest

Le module unittest fait partie de la bibliothèque standard Il sert à coder des tests unitaires pour des modules

Intérêts

Voir http://docs.python.org/lib/module-unittest.html

Automatiser vos tests va grandement améliorer les temps de développements tant en garantissant une offre continue des fonctionnalités déjà livrées.

Cela permet de détecter au plus tôt les erreurs

N'oubliez pas que le temps allouer aux vérifications et à la validation est souvent largement sous-estimé et atteint en réalité près de 40%

Outils standards offerts par Python : unittest (exemple)

Code python d'un module calculant une factorielle

from fact import fact
import unittest
class FactTC(unittest.TestCase):
    def test_first_results(self):
        self.assertEquals(fact(1), 1)
        for n in xrange(2, 10):
            self.failUnless(fact(n) == n*fact(n-1))
    def test_negative_numbers(self):
        self.assertRaises(ValueError, fact, -12)
if __name__ == ’__main__’:
    unittest.main()

Pour chaque cas, on crée une classe dérivant de TestCase du module unittest

Chaque méthode dont le nom commencera par test sera alors considérée comme un test unitaire (convention)

La fonction main() collecte et lance tous les tests définis dans le module, mais il est possible de donner des arguments sur la ligne de commande pour ne lancer que certains cas de tests

Un test unitaire est considéré comme passé lorsque l’exécution de la méthode n’a pas généré d’erreur (i.e génération d’une exception)

Attention ne pas utiliser le mot clef assert qui est supprimé lors de l’optimisation (python -O)

Outils standards offerts par Python : unittest (exemple)

Exemple d'erreur retournée

$ python unittest_compat.py
............F..
-----------------------------------------------
FAIL: test_all (__main__.Py25CompatTC)
-----------------------------------------------
Traceback (most recent call last):
File "unittest_compat.py", line 183, in test_all
self.assertEquals(irange.next(), 2)
File "/usr/lib/python2.3/unittest.py", line 302, in
failUnlessEqual
raise self.failureException,
AssertionError: 1 != 2
-----------------------------------------------
Ran 15 tests in 0.120s
FAILED (failures=1)

On distinguera deux types d’erreurs lors de l’exécution d’un test :

  • si une assertion n’est pas vérifiée, une exception AssertionError est lancée, le test échoue, et la docstring du test est utilisée pour le rapport (failure)
  • si un test génère un autre type d’exception, il s’agit d’une erreur non prévue, le test échoue et la pile d’appels est utilisée pour le rapport (error)

Outils standards offerts par Python : pdb

« Tester un programme démontre la présence de bugs, pas leur absence »

Edsger Dijkstra

le module pdb

Python dispose d’un débogueur intégré dans la bibliothèque standard, à travers le module pdb

Quand l'utiliser ? Un comportement inattendu apparaît et il est impossible d'écrire un test unitaire pour le caractériser

Lancer l’interpréteur python avec l’option -i permet de passer en interactif lorsqu’une exception remonte jusqu’à l’interpréteur On peut alors exécuter la fonction pm() du module pdb pour analyser l’exception en détail, de la même façon qu’on analyserait un core dump

Plus simple encore, vous pouvez placer ces 2 lignes à n'importe quel endroit de votre code et ceci vous fera rentrer automatiquement dans le débogueur

Code python:

import pdb
pdb.set_trace()

Lorsqu’un programme se termine avec une exception, la pile d’appels seule n’est pas toujours suffisante pour comprendre ce qu’il s’est passé

Il y a deux façons d’utiliser ce débogueur: - Exécuter du code pas à pas - Effectuer une analyse post mortem d’un programme après une exception

Outils standards offerts par Python : pdb (exemple)

Lancement du débogueur à la volée:

|    > exemple.py(12)?()
|    -> A = 2
|    (Pdb) l
|      7
|      8     A = 1
|      9
|     10     pdb.set_trace()
|     11
|     12  -> A = 2
|     13     A = 4
|    [EOF]
|    (Pdb) print A
|    1
|    (Pdb) next
|    > exemple.py(13)?()
|    -> A = 4
|    (Pdb) print A
|    2

Autres outils possibles (1)

Environnement pour les tests unitaires

Couverture de code par les tests

Problème !

On ne couvre que le code qui est exécuté par les tests unitaires...

Nose est intéressant dans le sens où il peut englober les tests de plusieurs utilitaires différents sous forme de plugin

Les outils de couverture de code sont moins efficaces dans le cas de langages dynamiques car seul le code exécuté est testé !

La complexité cyclomatique est un outil de métrologie logiciel développé par Thomas McCabe pour mesurer la complexité d'un programme informatique. Cette mesure comptabilise le nombre de "chemins" au travers d'un programme représenté sous la forme d'un graphe.

Autres outils possibles (2)

Refactoring

Les outils d'analyse de code

Problème ?

La nature dynamique du langage python permet plusieurs approches qui provoquent des résultats différents selon les outils.

Lequel utiliser ?

Les trois évidemment !

La refactorisation du code ne peut intervenir que si un environnement de tests unitaires existe car sinon il est impossible de vérifier que les fonctionnalités sont toujours offertes après la réécriture du code.

Refactorisation <==> toujours à fonctionnalités égales

C'est une étape délicate qui doit rester exceptionnelle. Pour éviter ce type de changement, il peut être utile de s'aider d'outils d'analyse de code.

Ces utilitaires de vérification statique de code pour Python effectuent un travail comparable à celui du vérificateur sémantique d’un compilateur C ou Java.

Pyflakes plus rapide: utile pour intégrer dans votre éditeur et vérifier les erreurs de syntaxe et les imports de modules en trop

PyChecker utilise l'import de module d'où une certaine exécution du code

Pylint est encore trop verbeux par défaut mais il est prévu de corriger ça

Fonctionnalités: Conformité PEP 8 Absence de docstring Variables non utilisées Utilisation d’une variable non initialisée Longueur des lignes Module utilisé mais non importé Mauvais nombre d’arguments passés à une méthode ou une fonction Utilisation de l’opérateur % sur des chaînes avec des formats ne correspondant pas aux arguments Utilisation de méthodes ou d’attributs inexistants Redéfinition d’une fonction/classe/méthode dans la même portée self n’est pas le premier argument d’une méthode Argument de méthode/fonction non utilisé (ignore self) Nommage des fonctions, des méthodes, des variables Complexité du code

Conclusion



images/billard.jpg

La programmation est une création de l'esprit. Elle n'est pas prévisible et donc vouée par essence au changement. L'assurance qualité vous permet de gérer au mieux ces changements en les anticipant et en contrôlant leurs effets.

La seule technique de validation et de vérifications sont les tests. Vous devez prévoir le développement de tel sorte qu'il reste testable. Il est donc important d'isoler les fonctionnalités et d'éviter les dépendances qui ajoutent des cas d'éxecution indésirables

Pour aller plus loin...

La mailing-liste 'Testing in Python'

PythonTestingToolsTaxonomy wiki page

How to misuse code coverage, Brian Marick (1997)

Working effectively with legacy code, Michael C. Feathers

Refactoring - Improving the design of existing code, Martin Fowler

Refactoring to Patterns, Joshua Kerievsky



Questions ?