Exécution d'unittest avec une structure de répertoire de test typique


701

La structure de répertoire très courante, même pour un simple module Python, semble être de séparer les tests unitaires dans leur propre testrépertoire:

new_project/
    antigravity/
        antigravity.py
    test/
        test_antigravity.py
    setup.py
    etc.

par exemple, consultez le guide du projet Python .

Ma question est simplement quelle est la manière habituelle d'exécuter les tests? Je soupçonne que cela est évident pour tout le monde sauf moi, mais vous ne pouvez pas simplement exécuter à python test_antigravity.pypartir du répertoire de test car son import antigravityéchouera car le module n'est pas sur le chemin.

Je sais que je pourrais modifier PYTHONPATH et d'autres astuces liées au chemin de recherche, mais je ne peux pas croire que ce soit le moyen le plus simple - c'est bien si vous êtes le développeur, mais pas réaliste de s'attendre à ce que vos utilisateurs l'utilisent s'ils veulent simplement vérifier que les tests sont qui passe.

L'autre alternative consiste simplement à copier le fichier de test dans l'autre répertoire, mais cela semble un peu stupide et manque le point de les avoir dans un répertoire séparé pour commencer.

Donc, si vous veniez de télécharger la source sur mon nouveau projet, comment exécuteriez-vous les tests unitaires? Je préférerais une réponse qui me permettrait de dire à mes utilisateurs: "Pour exécuter les tests unitaires, faites X."


5
@EMP La solution appropriée lorsque vous devez définir le chemin de recherche consiste à ... définir le chemin de recherche. Quelle sorte de solution attendiez-vous?
Carl Meyer

7
@CarlMeyer une autre meilleure solution consiste à utiliser l' unittestinterface de ligne de commande comme décrit dans ma réponse ci-dessous afin que vous n'ayez pas à ajouter le répertoire au chemin.
Pierre

13
Pareil ici. Je viens de commencer à écrire mes tout premiers tests unitaires pour un petit projet Python et j'ai pris plusieurs jours à essayer de raisonner avec le fait que je ne peux pas facilement exécuter un test tout en conservant mes sources dans un répertoire src et les tests dans un répertoire de test, apparemment avec l'un des cadres de test existants. Je finirai par accepter des choses, trouver un moyen; mais cela a été une introduction très frustrante. (Et je suis un vétéran des tests unitaires en dehors de Python.)
Ates Goral

Réponses:


657

La meilleure solution à mon avis est d'utiliser l' unittest interface de ligne de commande qui ajoutera le répertoire au sys.pathafin que vous n'ayez pas à le faire (fait dans la TestLoaderclasse).

Par exemple pour une structure de répertoires comme celle-ci:

new_project
├── antigravity.py
└── test_antigravity.py

Vous pouvez simplement exécuter:

$ cd new_project
$ python -m unittest test_antigravity

Pour une structure de répertoires comme la vôtre:

new_project
├── antigravity
   ├── __init__.py         # make it a package
   └── antigravity.py
└── test
    ├── __init__.py         # also make test a package
    └── test_antigravity.py

Et dans les modules de test à l'intérieur du testpackage, vous pouvez importer le antigravitypackage et ses modules comme d'habitude:

# import the package
import antigravity

# import the antigravity module
from antigravity import antigravity

# or an object inside the antigravity module
from antigravity.antigravity import my_object

Exécution d'un seul module de test:

Pour exécuter un seul module de test, dans ce cas test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

Il vous suffit de référencer le module de test de la même manière que vous l'importez.

Exécution d'un scénario de test ou d'une méthode de test unique:

Vous pouvez également exécuter une seule TestCaseou une seule méthode de test:

$ python -m unittest test.test_antigravity.GravityTestCase
$ python -m unittest test.test_antigravity.GravityTestCase.test_method

Exécution de tous les tests:

Vous pouvez également utiliser la découverte de test qui découvrira et exécutera tous les tests pour vous, ils doivent être des modules ou des packages nommés test*.py(peuvent être modifiés avec l' -p, --patternindicateur):

$ cd new_project
$ python -m unittest discover
$ # Also works without discover for Python 3
$ # as suggested by @Burrito in the comments
$ python -m unittest

Cela exécutera tous les test*.pymodules à l'intérieur du testpackage.


53
python -m unittest discoverva rechercher et exécuter des tests dans le testrépertoire s'ils sont nommés test*.py. Si vous avez nommé le sous-répertoire tests, utilisez python -m unittest discover -s testset si vous avez nommé les fichiers de test antigravity_test.py, utilisez python -m unittest discover -s tests -p '*test.py' Les noms de fichiers peuvent utiliser des traits de soulignement mais pas des tirets.
Mike3d0g

10
Cela échoue pour moi sur Python 3 avec l'erreur en ImportError: No module named 'test.test_antigravity'raison d'un conflit avec le sous-module de test de la bibliothèque unittest. Peut-être qu'un expert peut confirmer et changer le nom du sous-répertoire de réponse par exemple, «tests» (pluriel).
expz

9
Mon test_antigravity.pylance toujours une erreur d'importation pour les deux import antigravityet from antigravity import antigravity, ainsi. J'ai les deux __init_.pyfichiers et j'appelle python3 -m unittest discoverdepuis le new projectrépertoire. Quoi d'autre pourrait-être faux?
imrek

19
fichier test/__init__.pyest crucial ici, même s'il est vide
François

3
@ Mike3d0g ne sais pas si vous vouliez dire que le nom du répertoire testest spécial ... mais juste pour mémoire, ce n'est pas le cas. : P python -m unittest discoverfonctionne avec des fichiers de test dans tests/tout aussi bien que test/.
ryan

49

La solution la plus simple pour vos utilisateurs consiste à fournir un script exécutable ( runtests.pyou quelque chose de ce genre) qui amorce l'environnement de test nécessaire, y compris, si nécessaire, en ajoutant sys.pathtemporairement votre répertoire de projet racine à . Cela ne nécessite pas que les utilisateurs définissent des variables d'environnement, quelque chose comme ça fonctionne bien dans un script d'amorçage:

import sys, os

sys.path.insert(0, os.path.dirname(__file__))

Vos instructions à vos utilisateurs peuvent alors être aussi simples que " python runtests.py".

Bien sûr, si le chemin dont vous avez besoin est vraiment os.path.dirname(__file__), vous n'avez pas du tout besoin de l'ajouter sys.path; Python place toujours le répertoire du script en cours d'exécution au début de sys.path, donc selon votre structure de répertoire, il suffit de localiser votre runtests.pyau bon endroit.

De plus, le module unittest dans Python 2.7+ (qui est rétroporté comme unittest2 pour Python 2.6 et versions antérieures) a désormais une détection de test intégrée, donc Nose n'est plus nécessaire si vous voulez une détection de test automatisée: vos instructions utilisateur peuvent être aussi simples que python -m unittest discover.


J'ai mis quelques tests dans un sous-dossier comme "Major Major". Ils peuvent s'exécuter avec python -m unittest discovery mais comment puis-je choisir d'exécuter un seul d'entre eux. Si j'exécute python -m unittest tests / testxxxxx, cela échoue pour le problème de chemin. Puisque le mode de découverte résout tout, je m'attends à ce qu'il y ait une autre astuce pour résoudre le problème de chemin sans correction de chemin de codage manuel que vous suggérez au premier point
Frederic Bazin

2
@FredericBazin N'utilisez pas la découverte si vous ne voulez qu'un seul test ou fichier de test, nommez simplement le module que vous souhaitez exécuter. Si vous le nommez comme chemin pointé du module (plutôt que comme chemin de fichier), il peut comprendre le chemin de recherche correctement. Voir la réponse de Peter pour plus de détails.
Carl Meyer

Ce hack était utile dans un scénario où je devais exécuter quelque chose comme python -m pdb tests\test_antigravity.py. Dans pdb, j'ai exécuté sys.path.insert(0, "antigravity")ce qui a permis à l'instruction d'importation de se résoudre comme si j'exécutais le module.
ixe013

23

Je crée généralement un script "exécuter les tests" dans le répertoire du projet (celui qui est commun au répertoire source et test) qui charge ma suite "Tous les tests". Il s'agit généralement de code standard, donc je peux le réutiliser de projet en projet.

run_tests.py:

import unittest
import test.all_tests
testSuite = test.all_tests.create_test_suite()
text_runner = unittest.TextTestRunner().run(testSuite)

test / all_tests.py (de Comment puis-je exécuter tous les tests unitaires Python dans un répertoire? )

import glob
import unittest

def create_test_suite():
    test_file_strings = glob.glob('test/test_*.py')
    module_strings = ['test.'+str[5:len(str)-3] for str in test_file_strings]
    suites = [unittest.defaultTestLoader.loadTestsFromName(name) \
              for name in module_strings]
    testSuite = unittest.TestSuite(suites)
    return testSuite

Avec cette configuration, vous pouvez en effet juste include antigravitydans vos modules de test. L'inconvénient est que vous auriez besoin de plus de code de support pour exécuter un test particulier ... Je les exécute tous à chaque fois.


1
Je voulais aussi un run testsscript dans le répertoire du projet et j'ai trouvé un moyen beaucoup plus propre de le faire. Hautement recommandé.
z33k

18

De l'article que vous avez lié à:

Créez un fichier test_modulename.py et placez-y vos tests les plus simples. Étant donné que les modules de test se trouvent dans un répertoire distinct de votre code, vous devrez peut-être ajouter le répertoire parent de votre module à votre PYTHONPATH afin de les exécuter:

$ cd /path/to/googlemaps

$ export PYTHONPATH=$PYTHONPATH:/path/to/googlemaps/googlemaps

$ python test/test_googlemaps.py

Enfin, il existe un autre framework de tests unitaires populaire pour Python (c'est si important!), Nose. nose permet de simplifier et d'étendre le framework unittest intégré (il peut, par exemple, trouver automatiquement votre code de test et configurer votre PYTHONPATH pour vous), mais il n'est pas inclus dans la distribution Python standard.

Peut-être devriez-vous regarder le nez comme il le suggère?


3
Oui, cela fonctionne (pour moi), mais je demande vraiment les instructions les plus simples que je puisse donner aux utilisateurs de mon module pour les amener à exécuter les tests. Modifier le chemin est peut-être bien le cas, mais je cherche quelque chose de plus simple.
Major Major

4
Alors, à quoi ressemble votre chemin python après avoir travaillé sur une centaine de projets? Suis-je censé entrer manuellement et nettoyer mon chemin? Si c'est le cas, c'est une conception odieuse!
jeremyjjbrown

11

J'ai eu le même problème, avec un dossier de tests unitaires séparé. À partir des suggestions mentionnées, j'ajoute le chemin source absolu à sys.path.

L'avantage de la solution suivante est que l'on peut exécuter le fichier test/test_yourmodule.pysans changer d'abord dans le répertoire test:

import sys, os
testdir = os.path.dirname(__file__)
srcdir = '../antigravity'
sys.path.insert(0, os.path.abspath(os.path.join(testdir, srcdir)))

import antigravity
import unittest

9

si vous exécutez "python setup.py develop" alors le paquet sera dans le chemin. Mais vous ne voudrez peut-être pas faire cela car vous pourriez infecter votre installation de python système, c'est pourquoi des outils comme virtualenv et buildout existent.


7

Solution / Exemple pour le module Python unittest

Étant donné la structure de projet suivante:

ProjectName
 ├── project_name
 |    ├── models
 |    |    └── thing_1.py
 |    └── __main__.py
 └── test
      ├── models
      |    └── test_thing_1.py
      └── __main__.py

Vous pouvez exécuter votre projet à partir du répertoire racine avec python project_name, qui appelle ProjectName/project_name/__main__.py.


Pour exécuter vos tests avec python testune exécution efficace ProjectName/test/__main__.py, vous devez procéder comme suit:

1) Transformez votre test/modelsrépertoire en package en ajoutant un __init__.pyfichier. Cela rend les cas de test dans le sous-répertoire accessibles à partir du testrépertoire parent .

# ProjectName/test/models/__init__.py

from .test_thing_1 import Thing1TestCase        

2) Modifiez votre chemin d'accès système test/__main__.pypour inclure le project_namerépertoire.

# ProjectName/test/__main__.py

import sys
import unittest

sys.path.append('../project_name')

loader = unittest.TestLoader()
testSuite = loader.discover('test')
testRunner = unittest.TextTestRunner(verbosity=2)
testRunner.run(testSuite)

Vous pouvez désormais importer des éléments avec succès project_namedans vos tests.

# ProjectName/test/models/test_thing_1.py    

import unittest
from project_name.models import Thing1  # this doesn't work without 'sys.path.append' per step 2 above

class Thing1TestCase(unittest.TestCase):

    def test_thing_1_init(self):
        thing_id = 'ABC'
        thing1 = Thing1(thing_id)
        self.assertEqual(thing_id, thing.id)

5

Utilisez setup.py developpour que votre répertoire de travail fasse partie de l'environnement Python installé, puis exécutez les tests.


Cela m'obtient un invalid command 'develop'et cette option n'est pas mentionnée si je demande setup.py --help-commands. Faut-il quelque chose en setup.pysoi pour que cela fonctionne?
Major Major

C'est OK - le problème était qu'il me manquait un import setuptoolsdans mon setup.pyfichier. Mais je suppose que cela montre que cela ne fonctionnera pas tout le temps pour les modules d'autres personnes.
Major Major

1
Si vous avez pip , vous pouvez l'utiliser pour installer votre package en mode "modifiable" : pip install -e .cela ajoute également le package à l'environnement Python sans copier la source, vous permettant de continuer à le modifier où il se trouve.
Eric Smith

pip install -e .est exactement la même chose que python setup.py develop, il monkeypres juste votresetup.py même si ce n'est pas le cas, donc cela fonctionne dans les deux cas.
Carl Meyer

5

Si vous utilisez VS Code et que vos tests sont situés au même niveau que votre projet, l'exécution et le débogage de votre code ne fonctionnent pas immédiatement. Vous pouvez modifier votre fichier launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python",
            "type": "python",
            "request": "launch",
            "stopOnEntry": false,
            "pythonPath": "${config:python.pythonPath}",
            "program": "${file}",
            "cwd": "${workspaceRoot}",
            "env": {},
            "envFile": "${workspaceRoot}/.env",
            "debugOptions": [
                "WaitOnAbnormalExit",
                "WaitOnNormalExit",
                "RedirectOutput"
            ]
        }    
    ]
}

La ligne clé ici est envFile

"envFile": "${workspaceRoot}/.env",

À la racine de votre projet, ajoutez le fichier .env

À l'intérieur de votre fichier .env, ajoutez le chemin d'accès à la racine de votre projet. Cela ajoutera temporairement

PYTHONPATH = C: \ YOUR \ PYTHON \ PROJECT \ ROOT_DIRECTORY

chemin d'accès à votre projet et vous pourrez utiliser les tests unitaires de débogage de VS Code


5

J'ai remarqué que si vous exécutez l'interface de ligne de commande la plus unitaire à partir de votre répertoire "src", les importations fonctionnent correctement sans modification.

python -m unittest discover -s ../test

Si vous souhaitez mettre cela dans un fichier batch dans votre répertoire de projet, vous pouvez le faire:

setlocal & cd src & python -m unittest discover -s ../test

5

J'ai le même problème depuis longtemps. Ce que j'ai récemment choisi, c'est la structure de répertoires suivante:

project_path
├── Makefile
├── src
   ├── script_1.py
   ├── script_2.py
   └── script_3.py
└── tests
    ├── __init__.py
    ├── test_script_1.py
    ├── test_script_2.py
    └── test_script_3.py

et dans le __init__.pyscript du dossier de test, j'écris ce qui suit:

import os
import sys
PROJECT_PATH = os.getcwd()
SOURCE_PATH = os.path.join(
    PROJECT_PATH,"src"
)
sys.path.append(SOURCE_PATH)

Le Makefile est extrêmement important pour partager le projet, car il applique correctement les scripts. Voici la commande que j'ai mise dans le Makefile:

run_tests:
    python -m unittest discover .

Le Makefile est important non seulement à cause de la commande qu'il exécute mais aussi à cause de l' endroit où il l'exécute . Si vous voulez faire un cd dans les tests et le faire python -m unittest discover ., cela ne fonctionnerait pas car le script init dans unit_tests appelle os.getcwd (), qui pointerait alors vers le chemin absolu incorrect (qui serait ajouté à sys.path et il vous manquerait votre dossier source). Les scripts s'exécuteraient depuis que find trouve tous les tests, mais ils ne fonctionneraient pas correctement. Le Makefile est donc là pour éviter d'avoir à se souvenir de ce problème.

J'aime vraiment cette approche car je n'ai pas à toucher mon dossier src, mes tests unitaires ou mes variables d'environnement et tout se passe bien.

Faites-moi savoir si vous les aimez.

J'espère que cela pourra aider,


4

Voici la structure de mon projet:

ProjectFolder:
 - project:
     - __init__.py
     - item.py
 - tests:
     - test_item.py

J'ai trouvé préférable d'importer dans la méthode setUp ():

import unittest
import sys    

class ItemTest(unittest.TestCase):

    def setUp(self):
        sys.path.insert(0, "../project")
        from project import item
        # further setup using this import

    def test_item_props(self):
        # do my assertions

if __name__ == "__main__":
    unittest.main()

4

Quelle est la manière habituelle d'exécuter les tests

J'utilise Python 3.6.2

cd new_project

pytest test/test_antigravity.py

Pour installer pytest :sudo pip install pytest

Je n'ai défini aucune variable de chemin et mes importations n'échouent pas avec la même structure de projet "test".

J'ai commenté ce truc: if __name__ == '__main__'comme ceci:

test_antigravity.py

import antigravity

class TestAntigravity(unittest.TestCase):

    def test_something(self):

        # ... test stuff here


# if __name__ == '__main__':
# 
#     if __package__ is None:
# 
#         import something
#         sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
#         from .. import antigravity
# 
#     else:
# 
#         from .. import antigravity
# 
#     unittest.main()

4

Il est possible d'utiliser un wrapper qui exécute certains tests ou tous les tests.

Par exemple:

./run_tests antigravity/*.py

ou pour exécuter tous les tests de manière récursive, utilisez globbing ( tests/**/*.py) (activé par shopt -s globstar).

Le wrapper peut essentiellement utiliser argparsepour analyser les arguments comme:

parser = argparse.ArgumentParser()
parser.add_argument('files', nargs='*')

Chargez ensuite tous les tests:

for filename in args.files:
    exec(open(filename).read())

puis ajoutez-les dans votre suite de tests (en utilisant inspect):

alltests = unittest.TestSuite()
for name, obj in inspect.getmembers(sys.modules[__name__]):
    if inspect.isclass(obj) and name.startswith("FooTest"):
        alltests.addTest(unittest.makeSuite(obj))

et exécutez-les:

result = unittest.TextTestRunner(verbosity=2).run(alltests)

Consultez cet exemple pour plus de détails.

Voir aussi: Comment exécuter tous les tests unitaires Python dans un répertoire?


4

Python 3+

Ajout à @Pierre

Utilisation d'une unitteststructure de répertoires comme celle-ci:

new_project
├── antigravity
   ├── __init__.py         # make it a package
   └── antigravity.py
└── test
    ├── __init__.py         # also make test a package
    └── test_antigravity.py

Pour exécuter le module de test test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

Ou un seul TestCase

$ python -m unittest test.test_antigravity.GravityTestCase

Obligatoire n'oubliez pas le __init__.pymême si vide sinon ne fonctionnera pas.


2

Vous ne pouvez pas importer du répertoire parent sans un peu de vaudou. Voici encore une autre méthode qui fonctionne avec au moins Python 3.6.

Tout d'abord, ayez un fichier test / context.py avec le contenu suivant:

import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

Ensuite, importez le fichier suivant dans le fichier test / test_antigravity.py:

import unittest
try:
    import context
except ModuleNotFoundError:
    import test.context    
import antigravity

Notez que la raison de cette clause try-except est que

  • l'importation test.context échoue lors de l'exécution avec "python test_antigravity.py" et
  • le contexte d'importation échoue lorsqu'il est exécuté avec "python -m unittest" à partir du répertoire new_project.

Avec cette ruse, ils travaillent tous les deux.

Vous pouvez maintenant exécuter tous les fichiers de test dans le répertoire de test avec:

$ pwd
/projects/new_project
$ python -m unittest

ou exécutez un fichier de test individuel avec:

$ cd test
$ python test_antigravity

Ok, ce n'est pas beaucoup plus joli que d'avoir le contenu de context.py dans test_antigravity.py, mais peut-être un peu. Les suggestions sont les bienvenues.


2

Si vous avez plusieurs répertoires dans votre répertoire de test, vous devez ajouter à chaque répertoire un __init__.pyfichier.

/home/johndoe/snakeoil
└── test
    ├── __init__.py        
    └── frontend
        └── __init__.py
        └── test_foo.py
    └── backend
        └── __init__.py
        └── test_bar.py

Ensuite, pour exécuter chaque test à la fois, exécutez:

python -m unittest discover -s /home/johndoe/snakeoil/test -t /home/johndoe/snakeoil

La source: python -m unittest -h

  -s START, --start-directory START
                        Directory to start discovery ('.' default)
  -t TOP, --top-level-directory TOP
                        Top level directory of project (defaults to start
                        directory)

1

Ce script BASH exécutera le répertoire de test python unittest de n'importe où dans le système de fichiers, quel que soit le répertoire de travail dans lequel vous vous trouvez.

Ceci est utile lorsque vous restez dans le répertoire de travail ./srcou ./exampleet vous avez besoin d'un test unitaire rapide:

#!/bin/bash

this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

python -m unittest discover -s "$readlink"/test -v

Pas besoin de test/__init__.pyfichier pour surcharger votre package / surcharge de mémoire pendant la production.


1

De cette façon, vous pourrez exécuter les scripts de test où vous le souhaitez sans vous soucier des variables système de la ligne de commande.

Cela ajoute le dossier principal du projet au chemin python, avec l'emplacement trouvé par rapport au script lui-même, et non par rapport au répertoire de travail actuel.

import sys, os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))

Ajoutez cela en haut de tous vos scripts de test. Cela ajoutera le dossier principal du projet au chemin système, donc tout module importé qui fonctionne à partir de là fonctionnera désormais. Et peu importe d'où vous exécutez les tests.

Vous pouvez évidemment modifier le fichier project_path_hack pour qu'il corresponde à l'emplacement de votre dossier de projet principal.


0

Si vous recherchez une solution en ligne de commande uniquement:

Basé sur la structure de répertoires suivante (généralisée avec un répertoire source dédié):

new_project/
    src/
        antigravity.py
    test/
        test_antigravity.py

Windows : (en new_project)

$ set PYTHONPATH=%PYTHONPATH%;%cd%\src
$ python -m unittest discover -s test

Consultez cette question si vous souhaitez l'utiliser dans une boucle for batch.

Linux : (en new_project)

$ export PYTHONPATH=$PYTHONPATH:$(pwd)/src  [I think - please edit this answer if you are a Linux user and you know this]
$ python -m unittest discover -s test

Avec cette approche, il est également possible d'ajouter plus de répertoires au PYTHONPATH si nécessaire.


0

Vous devriez vraiment utiliser l'outil pip.

Utilisez pip install -e .pour installer votre package en mode développement. Il s'agit d'une très bonne pratique, recommandée par pytest (voir leur documentation sur les bonnes pratiques , où vous pouvez également trouver deux dispositions de projet à suivre).


Pourquoi voter contre cette réponse? J'ai lu la réponse acceptée et bien qu'elle ne soit pas mauvaise, il pytestest préférable d'exécuter des tests, en raison de la sortie de la console que vous obtenez, en couleur, avec des informations de trace de pile et des informations détaillées sur les erreurs d'assertion.
aliopi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.