16.7. Assembler les pièces

Vous en avez assez appris pour déconstruire les sept premières lignes du code d'exemple de ce chapitre : lire un répertoire et importer des modules sélectionnés parmi ceux qu'il contient.

Exemple 16.16. La fonction regressionTest


def regressionTest():
    path = os.path.abspath(os.path.dirname(sys.argv[0]))   
    files = os.listdir(path)                               
    test = re.compile("test\.py$", re.IGNORECASE)          
    files = filter(test.search, files)                     
    filenameToModuleName = lambda f: os.path.splitext(f)[0]
    moduleNames = map(filenameToModuleName, files)         
    modules = map(__import__, moduleNames)                 
load = unittest.defaultTestLoader.loadTestsFromModule  
return unittest.TestSuite(map(load, modules))          

Regardons cela ligne par ligne. Supposons que le répertoire en cours est c:\diveintopython\py, qui contient les exemples du livre, y compris le script de ce chapitre. Comme vous l'avez vu à la Section 16.2, «Trouver le chemin», le répertoire du script est assigné à la variable path, commençons donc à cette étape.

Exemple 16.17. Etape 1 : Obtenir la liste des fichiers

>>> import sys, os, re, unittest
>>> path = r'c:\diveintopython\py'
>>> files = os.listdir(path)                               
>>> files 1
['BaseHTMLProcessor.py', 'LICENSE.txt', 'apihelper.py', 'apihelpertest.py',
'argecho.py', 'autosize.py', 'builddialectexamples.py', 'dialect.py',
'fileinfo.py', 'fullpath.py', 'kgptest.py', 'makerealworddoc.py',
'odbchelper.py', 'odbchelpertest.py', 'parsephone.py', 'piglatin.py',
'plural.py', 'pluraltest.py', 'pyfontify.py', 'regression.py', 'roman.py', 'romantest.py',
'uncurly.py', 'unicode2koi8r.py', 'urllister.py', 'kgp', 'plural', 'roman',
'colorize.py']
1 files est une liste de tous les fichiers et les répertoires du répertoire du script (si vous avez déjà exécuté certains exemples, vous verrez également des fichiers .pyc).

Exemple 16.18. Etape 2 : Filtrage des fichiers

>>> test = re.compile("test\.py$", re.IGNORECASE)           1
>>> files = filter(test.search, files)                      2
>>> files                                                   3
['apihelpertest.py', 'kgptest.py', 'odbchelpertest.py', 'pluraltest.py', 'romantest.py']
1 Cette expression régulière reconnaît toutes les chaînes qui finissent par test.py. Notez que nous devons utiliser le caractère d'échappement pour le point, un point dans une expression régulière signifiant «n'importe quel caractère», ce que nous voulons c'est bien un point.
2 L'expression régulière compilée agit comme une fonction, nous pouvons donc l'utiliser pour filtrer la liste de fichiers et de répertoires.
3 Ce qu'il reste est la liste des scripts de tests unitaires puisque ce sont les seuls nommés QUELQUECHOSEtest.py.

Exemple 16.19. Etape 3 : Mutation des noms de fichiers en noms de modules

>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] 1
>>> filenameToModuleName('romantest.py')                    2
'romantest'
>>> filenameToModuleName('odchelpertest.py')
'odbchelpertest'
>>> moduleNames = map(filenameToModuleName, files)          3
>>> moduleNames                                             4
['apihelpertest', 'kgptest', 'odbchelpertest', 'pluraltest', 'romantest']
1 Comme vous l'avez vu à la Section 4.7, «Utiliser des fonctions lambda», lambda est une manière rapide de créer des fonctions incluses d'une ligne. Celle-ci prend un nom de fichier avec une extension et le retourne sans son extension en utilisant la fonction de la bibliothèque standard os.path.splitext que vous avez vu à l'Exemple 6.17, «Division de noms de chemins».
2 filenameToModuleName est une fonction. Il n'y a rien qui différencie les fonctions lambda des fonctions habituelles définies par l'instruction def. Nous pouvons appeler la fonction filenameToModuleName comme n'importe quelle autre et elle fait exactement ce que nous voulons qu'elle fasse : enlever l'extension du nom de fichier passé en argument.
3 Maintenant nous pouvons appliquer cette fonction à chaque nom de fichier de la liste de fichier de tests unitaires à l'aide de map.
4 Le résultat est bien ce que nous souhaitons : une liste de modules sous forme de chaînes.

Exemple 16.20. Etape 4 : Mutation des noms de modules en modules

>>> modules = map(__import__, moduleNames)                  1
>>> modules                                                 2
[<module 'apihelpertest' from 'apihelpertest.py'>,
<module 'kgptest' from 'kgptest.py'>,
<module 'odbchelpertest' from 'odbchelpertest.py'>,
<module 'pluraltest' from 'pluraltest.py'>,
<module 'romantest' from 'romantest.py'>]
>>> modules[-1]                                             3
<module 'romantest' from 'romantest.py'>
1 Comme vous l'avez vu à la Section 16.6, «Importation dynamique de modules», nous pouvons utiliser map et __import__ pour transformer une liste de noms de modules (sous forme de chaînes) en une liste de modules (que nous pouvons appeler comme n'importe quel autre module).
2 modules est maintenant une liste de modules, totalement accessibles comme tout autre module.
3 Le dernier module de la liste est le module romantest, comme si nous avions écrit import romantest.

Exemple 16.21. Etape 5 : Chargement des modules en une suite de tests

>>> load = unittest.defaultTestLoader.loadTestsFromModule  
>>> map(load, modules)                     1
[<unittest.TestSuite tests=[
  <unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>,
  <unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>,
  <unittest.TestSuite tests=[
    <apihelpertest.ParamChecks testMethod=testCollapse>, 
    <apihelpertest.ParamChecks testMethod=testSpacing>]>, 
    ...
  ]
]
>>> unittest.TestSuite(map(load, modules)) 2
1 Ce sont de véritable objets-modules. Nous pouvons non seulement y accéder comme à tout autre module, instancier des classes et appeler des fonctions, nous pouvons également utiliser l'instrospection pour déterminer quelles fonctions et classes il contient. C'est ce que la méthode loadTestsFromModule fait : elle utilise l'instrospection et retourne un objet unittest.TestSuite pour chaque module. Chaque objet TestSuite contient en fait une liste d'objets TestSuite, un pour chaque classe TestCase du module et chacun de ces objets TestSuite contient une liste de tests, un pour chaque méthode de test du module.
2 Finalement, nous regroupons la liste d'objets TestSuite en une seule suite de tests. Le module unittest n'a aucun mal à parcourir cet arbre de suites de tests imbriquées, il recherche une méthode de test, l'exécute, vérifie que le test passe ou échoue et continue de parcourir l'arbre jusqu'à la prochaine méthode de test.

Ce processus d'introspection est ce que le module unittest fait d'habitude pour nous. Vous vous rappelez de cette fonction magique unittest.main() que nos modules de test appelaient pour démarrer le processus ? unittest.main() crée en fait une instance de unittest.TestProgram, qui crée à son tour une instance de unittest.defaultTestLoader et le charge avec le module appelant (comment obtient-il une référence au module appelant sans qu'on lui en donne une ? En utilisant une instruction tout aussi magique, __import__('__main__'), qui importe dynamiquement le module en cours d'exécution. Je pourrais écrire un livre sur tous les trucs et les techniques utilisé dans le module unittest, mais dans ce cas je ne finirais jamais celui-ci).

Exemple 16.22. Etape 6 : Passage de la suite de tests à unittest


if __name__ == "__main__":                   
    unittest.main(defaultTest="regressionTest") 1
1 Au lieu de laisser le module unittest opérer sa magie pour nous, nous avons fait la majeure partie du travail nous-même. Nous avons créé une fonction (regressionTest) qui importe les modules, appelé unittest.defaultTestLoader et regroupé l'ensemble en une suite de tests. Maintenant, tout ce dont nous avons besoins est de dire à unittest qu'il doit, au lieu de rechercher des tests et de construire une suite de tests de la manière habituelle, appeler simplement la fonction regressionTest, qui retourne une TestSuite prête à l'emploi.