samedi 29 octobre 2022

Refactorisations assistées par des DOCTEST

 Python permet facilement de passer d'un programme structuré en fonction à un programme utilisant des classes et des objets.

C'est à mon avis une des forces de Python. Pas besoin de tout réécrire le gros du travail est conservé. 

Aussi , Python favorise la  refactorisation de son code au fur et à mesure de la progression du développement.

(le notebook est disponible sur : https://github.com/germanlinux/notebooks

(https://github.com/germanlinux/notebooks/blob/main/init/refactorisation.ipynb)

La refactorisation en  objet et classe consiste à :

  • Ajouter le paramètre 'self' dans les déclarations des fonctions 
  • Préfixer les variables locales par 'self'
  • Créer un cadre pour la classe
  • Améliorer le nommage des méthodes


Pour sécuriser ces opérations de refactorisation, j'utilise le dispositif  du DOCTEST pour m'assurer de la non régression du programme.

Le DOCTEST permet d'inclure des tests dans la documentation des méthodes ou des fonctions.

Il utilise pour cela le motif '>>>'  pour insérer des instructions Python dans la documentation. Le resultat de ces appels est comparé avec la ligne suivante sans préfixe.

Exemple:





Ces insertions n'auront aucun impact sur le fonctionnement des méthodes ou des fonctions.

Pour activer les tests il suffit d'ajouter l'appel au module de DOCTEST.


Dans cet exemple, la fonction à tester est indiquée. Il est possible de lancer tous les test présents dans un programme en remplaçant la methode run_docstring_examples(...) par testmod(..)

Lancement des tests 
sous condition 





Ci dessous un exemple complet (projet pycobol) 

class ZoneGroupe:
    ''' Cette classe prend en charge la creation d'une zone groupe. 
    Par nature cette zone est de type ALN.
    Sa longueur est la somme des longueurs des composants qui la composent
    >>> obj = ZoneGroupe('zoneessai', 1, 0)
    >>> obj.nom
    'zoneessai'
    >>> print(obj)
    >>> objfils = ZoneGroupe('zonefils', 2)
    >>> obj.ajout_fils_groupe(objfils)
    >>> len(obj.fils)
    1
    >>> objfils.pere # doctest: +ELLIPSIS
    ZoneGroupe(nom='zoneessai'...
    >>> objfils2 = ZoneGroupe('zonefils2', 1)
    >>> obj.ajout_fils_groupe(objfils2)  # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    >>> objfilsimp = ZoneFilsSimple('essaifils', 5, picture = '999')
    >>> obj.ajout_fils_simple(objfilsimp)
    >>> obj.longueur_utile
    3
    '''

La directive #doctest: +ELLIPSIS  permet de signaler au module DOCTEST d'arrêter la comparaison du résultat après les trois points (...) . C'est très utile pour tester une exception: la sortie complète de l'exception (trace de la pile d'exécution) sera  ignorée. Il est aussi utilisé ici pour des contenus un peu longs comme les objets.


class ZoneGroupe(builtins.object)
 |  ZoneGroupe(nom: str, rang: int, pere: int = 0, fils: list[int] = <factory>, son_type: str = 'GRP', usage: str = 'DISPLAY', longueur_utile: int = 0, valeur_interne: str = '', valeur_externe: str = '', section: str = 'NON RENSEIGNE') -> None
 |
 |  Cette classe prend en charge la creation d'une zone groupe
 |  par nature cette zone est de type ALN.
 |  Sa longueur est la somme des longueurs des composants qui la composent
 |  >>> obj = ZoneGroupe('zoneessai', 1, 0)
 |  >>> obj.nom
 |  'zoneessai'
 |  >>> print(obj)
 |  >>> objfils = ZoneGroupe('zonefils', 2)
 |  >>> obj.ajout_fils_groupe(objfils)
 |  >>> len(obj.fils)
 |  1
 |  >>> objfils.pere # doctest: +ELLIPSIS
 |  ZoneGroupe(nom='zoneessai'...
 |  >>> objfils2 = ZoneGroupe('zonefils2', 1)
 |  >>> obj.ajout_fils_groupe(objfils2)  # doctest: +ELLIPSIS
 |  Traceback (most recent call last):
 |  ...
 |  >>> objfilsimp = ZoneFilsSimple('essaifils', 5, picture = '999')
 |  >>> obj.ajout_fils_simple(objfilsimp)
 |  >>> obj.longueur_utile
 |  3

Résultat de l'instruction help 

Ci-dessous un résultat partiel d'exécution des tests

Trying:
    objfils = ZoneGroupe('zonefils', 2)
Expecting nothing
ok
Trying:
    obj.ajout_fils_groupe(objfils)
Expecting nothing
ok
Trying:
    len(obj.fils)
Expecting:
    1
ok
Trying:
    objfils.pere # doctest: +ELLIPSIS
Expecting:
    ZoneGroupe(nom='zoneessai'...
ok
Trying:
    objfils2 = ZoneGroupe('zonefils2', 1)
Expecting nothing
ok

Résultats des tests.

En conclusion.

Python nous donne la possibilité de se tromper et d'améliorer notre ouvrage par petites touches. 

Ces tests intégrés sont à compléter par des tests unitaires avec une couverture du code mesuré avec la commande coverage.