Ejecución de unittest con estructura de directorio de prueba típica

701

La estructura de directorio muy común, incluso para un módulo Python simple, parece ser separar las pruebas unitarias en su propio testdirectorio:

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

por ejemplo, vea este tutorial sobre proyectos de Python .

Mi pregunta es simplemente ¿Cuál es la forma habitual de ejecutar realmente las pruebas? Sospecho que esto es obvio para todos, excepto para mí, pero no puede simplemente ejecutar python test_antigravity.pydesde el directorio de prueba ya import antigravityque fallará ya que el módulo no está en la ruta.

Sé que podría modificar PYTHONPATH y otros trucos relacionados con la ruta de búsqueda, pero no puedo creer que sea la forma más simple: está bien si usted es el desarrollador, pero no es realista esperar que sus usuarios lo usen si solo quieren verificar si las pruebas son paso.

La otra alternativa es simplemente copiar el archivo de prueba en el otro directorio, pero parece un poco tonto y pierde el punto de tenerlos en un directorio separado para comenzar.

Entonces, si acabara de descargar la fuente a mi nuevo proyecto, ¿cómo ejecutaría las pruebas unitarias? Prefiero una respuesta que me permita decirles a mis usuarios: "Para ejecutar las pruebas unitarias, haga X".

Mayor mayor
fuente
55
@EMP La solución adecuada cuando necesita establecer la ruta de búsqueda es ... establecer la ruta de búsqueda. ¿Qué tipo de solución esperabas?
Carl Meyer
77
@CarlMeyer otra mejor solución es usar la unittestinterfaz de línea de comandos como se describe en mi respuesta a continuación para que no tenga que agregar el directorio a la ruta.
Pierre
13
Igual que aquí. Me embarqué en escribir mis primeras pruebas unitarias para un pequeño proyecto de Python y me tomé varios días tratando de razonar con el hecho de que no puedo ejecutar una prueba fácilmente mientras mantengo mis fuentes en un directorio src y las pruebas en un directorio de prueba, aparentemente con cualquiera de los marcos de prueba existentes. Eventualmente aceptaré cosas, descubriré una manera; Pero esta ha sido una introducción muy frustrante. (Y soy un veterano de pruebas de unidades fuera de Python.)
Ates Goral

Respuestas:

657

La mejor solución en mi opinión es usar la unittest interfaz de línea de comando que agregará el directorio al sys.pathpara que no tenga que hacerlo (hecho en la TestLoaderclase).

Por ejemplo, para una estructura de directorios como esta:

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

Solo puedes ejecutar:

$ cd new_project
$ python -m unittest test_antigravity

Para una estructura de directorio como la suya:

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

Y en los módulos de prueba dentro del testpaquete, puede importar el antigravitypaquete y sus módulos como de costumbre:

# 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

Ejecutando un solo módulo de prueba:

Para ejecutar un solo módulo de prueba, en este caso test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

Simplemente haga referencia al módulo de prueba de la misma manera que lo importa.

Ejecutar un único caso de prueba o método de prueba:

También puede ejecutar un TestCasemétodo de prueba único o único:

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

Ejecutando todas las pruebas:

También puede usar el descubrimiento de pruebas, que descubrirá y ejecutará todas las pruebas por usted, deben ser módulos o paquetes nombrados test*.py(se pueden cambiar con el -p, --patternindicador):

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

Esto ejecutará todos los test*.pymódulos dentro del testpaquete.

Pierre
fuente
53
python -m unittest discoverbuscará y ejecutará pruebas en el testdirectorio si se nombran test*.py. Si nombró el subdirectorio tests, use python -m unittest discover -s tests, y si nombró los archivos de prueba antigravity_test.py, use python -m unittest discover -s tests -p '*test.py' Los nombres de archivo pueden usar guiones bajos pero no guiones.
Mike3d0g
10
Esto me falla en Python 3 con el error ImportError: No module named 'test.test_antigravity'debido a un conflicto con el submódulo de prueba de la biblioteca unittest. Tal vez un experto pueda confirmar y cambiar el nombre del subdirectorio de respuesta a, por ejemplo, 'pruebas' (plural).
expz
99
Mi test_antigravity.pytodavía arroja un error de importación para ambos import antigravityy from antigravity import antigravity, también. Tengo ambos __init_.pyarchivos y estoy llamando python3 -m unittest discoverdesde el new projectdirectorio. ¿Qué más podría estar mal?
Imrek
19
el archivo test/__init__.pyes crucial aquí, incluso si está vacío
Francois
3
@ Mike3d0g no estoy seguro de si querías decir que el nombre del directorio testes especial ... pero solo para el registro, no lo es. : P python -m unittest discoverfunciona con archivos de prueba tests/tan bien como test/.
Ryan
49

La solución más simple para sus usuarios es proporcionar un script ejecutable ( runtests.pyo algo similar) que inicie el entorno de prueba necesario, incluyendo, si es necesario, agregar sys.pathtemporalmente el directorio del proyecto raíz . Esto no requiere que los usuarios establezcan variables de entorno, algo como esto funciona bien en un script de arranque:

import sys, os

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

Entonces sus instrucciones para sus usuarios pueden ser tan simples como " python runtests.py".

Por supuesto, si el camino que necesita realmente es os.path.dirname(__file__), entonces no necesita agregarlo sys.pathen absoluto; Python siempre coloca el directorio de la secuencia de comandos que se está ejecutando al principio sys.path, por lo que, dependiendo de la estructura de su directorio, lo runtests.pyúnico que necesita es ubicarlo en el lugar correcto.

Además, el módulo unittest en Python 2.7+ (que está respaldado como unittest2 para Python 2.6 y versiones anteriores) ahora tiene el descubrimiento de prueba incorporado, por lo que ya no es necesario la nariz si desea un descubrimiento de prueba automatizado: sus instrucciones de usuario pueden ser tan simples como python -m unittest discover.

Carl Meyer
fuente
Puse algunas pruebas en una subcarpeta como "Major Major". Pueden ejecutarse con python -m unittest discover, pero ¿cómo puedo seleccionar ejecutar solo uno de ellos? Si ejecuto python -m unittest tests / testxxxxx, entonces falla por problema de ruta. Dado que el modo de descubrimiento resuelve todo, esperaría que haya otro truco para resolver el problema de la ruta sin la corrección manual de la ruta que sugiere en el primer punto
Frederic Bazin
2
@FredericBazin No use el descubrimiento si solo desea una única prueba o archivo de prueba, solo asigne un nombre al módulo que desea ejecutar. Si lo nombra como una ruta de puntos de módulo (en lugar de una ruta de archivo), puede encontrar la ruta de búsqueda correctamente. Vea la respuesta de Peter para más detalles.
Carl Meyer
Este truco fue útil en un escenario en el que tuve que ejecutar algo como python -m pdb tests\test_antigravity.py. Dentro de pdb, ejecuté lo sys.path.insert(0, "antigravity")que permitió que la declaración de importación se resolviera como si estuviera ejecutando el módulo.
ixe013
23

En general, creo un script de "ejecución de pruebas" en el directorio del proyecto (el que es común tanto para el directorio de origen como test) que carga mi suite "Todas las pruebas". Este suele ser un código repetitivo, por lo que puedo reutilizarlo de un proyecto a otro.

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 ¿Cómo ejecuto todas las pruebas unitarias de Python en un directorio? )

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

Con esta configuración, puede de hecho solo include antigravityen sus módulos de prueba. La desventaja es que necesitaría más código de soporte para ejecutar una prueba en particular ... Simplemente los ejecuto todo el tiempo.

stw_dev
fuente
1
También quería un run testsscript en el directorio del proyecto y encontré una forma mucho más limpia de hacerlo. Muy recomendable.
z33k
18

Del artículo que vinculó a:

Cree un archivo test_modulename.py y coloque sus pruebas unittest en él. Dado que los módulos de prueba están en un directorio separado de su código, es posible que deba agregar el directorio principal de su módulo a su PYTHONPATH para ejecutarlos:

$ cd /path/to/googlemaps

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

$ python test/test_googlemaps.py

Finalmente, hay un marco de prueba de unidad más popular para Python (¡es tan importante!), Nose. nose ayuda a simplificar y ampliar el marco de prueba de unidad incorporado (puede, por ejemplo, encontrar automáticamente su código de prueba y configurar su PYTHONPATH por usted), pero no está incluido en la distribución estándar de Python.

¿Quizás deberías mirar la nariz como sugiere?

Mark Byers
fuente
3
Sí, esto funciona (para mí), pero realmente estoy pidiendo las instrucciones más simples que puedo dar a los usuarios a mi módulo para que ejecuten las pruebas. En realidad, modificar el camino podría serlo, pero estoy buscando algo más directo.
Major Major
44
Entonces, ¿cómo se ve su ruta de Python después de haber trabajado en un centenar de proyectos? ¿Se supone que debo entrar manualmente y limpiar mi camino? Si es así, ¡este es un diseño odioso!
jeremyjjbrown
11

Tuve el mismo problema, con una carpeta de pruebas unitarias separada. De las sugerencias mencionadas agrego la ruta de origen absoluta a sys.path.

El beneficio de la siguiente solución es que uno puede ejecutar el archivo test/test_yourmodule.pysin cambiar al principio al directorio de prueba:

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
andpei
fuente
9

si ejecuta "python setup.py development", el paquete estará en la ruta. Pero es posible que no desee hacerlo porque podría infectar la instalación de Python de su sistema, razón por la cual existen herramientas como virtualenv y buildout .

Tom Willis
fuente
7

Solución / Ejemplo para el módulo de prueba unitaria de Python

Dada la siguiente estructura del proyecto:

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

Puede ejecutar su proyecto desde el directorio raíz con python project_name, que llama ProjectName/project_name/__main__.py.


Para ejecutar sus pruebas con python testuna ejecución efectiva ProjectName/test/__main__.py, debe hacer lo siguiente:

1) Convierta su test/modelsdirectorio en un paquete agregando un __init__.pyarchivo. Esto hace que los casos de prueba dentro del subdirectorio sean accesibles desde el testdirectorio principal .

# ProjectName/test/models/__init__.py

from .test_thing_1 import Thing1TestCase        

2) Modifique la ruta de su sistema test/__main__.pypara incluir el project_namedirectorio.

# 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)

Ahora puede importar cosas con éxito project_nameen sus pruebas.

# 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)
Derek Soike
fuente
5

Utilícelo setup.py developpara que su directorio de trabajo sea parte del entorno Python instalado, luego ejecute las pruebas.

Ned Batchelder
fuente
Esto me da una invalid command 'develop'y esta opción no se menciona si lo pido setup.py --help-commands. ¿Es necesario que haya algo en setup.pysí mismo para que esto funcione?
Mayor mayor
Está bien - el problema era que faltaba una import setuptoolsde mi setup.pyarchivo. Pero supongo que eso demuestra que esto no funcionará todo el tiempo para los módulos de otras personas.
Mayor mayor
1
Si tiene pip , puede usarlo para instalar su paquete en modo "editable" : pip install -e .Esto también agrega el paquete al entorno de Python sin copiar la fuente, lo que le permite continuar editándolo donde se encuentra.
Eric Smith
pip install -e .es exactamente lo mismo, ya python setup.py developque solo usa parches setup.pypara usar las herramientas de configuración, incluso si en realidad no lo hace, por lo que funciona de cualquier manera.
Carl Meyer
5

Si usa VS Code y sus pruebas se encuentran en el mismo nivel que su proyecto, la ejecución y depuración de su código no funciona de inmediato. Lo que puede hacer es cambiar su archivo 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 línea clave aquí es envFile

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

En la raíz de su proyecto, agregue el archivo .env

Dentro de su archivo .env, agregue la ruta a la raíz de su proyecto. Esto agregará temporalmente

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

ruta de acceso a su proyecto y podrá utilizar pruebas de unidad de depuración de VS Code

Vlad Bezden
fuente
5

Noté que si ejecuta la interfaz de línea de comando unittest desde su directorio "src", las importaciones funcionan correctamente sin modificación.

python -m unittest discover -s ../test

Si desea poner eso en un archivo por lotes en el directorio de su proyecto, puede hacer esto:

setlocal & cd src & python -m unittest discover -s ../test
Alan L
fuente
5

He tenido el mismo problema durante mucho tiempo. Lo que elegí recientemente es la siguiente estructura de directorios:

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

y en el __init__.pyscript de la carpeta de prueba, escribo lo siguiente:

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

Súper importante para compartir el proyecto es el Makefile, porque obliga a ejecutar los scripts correctamente. Aquí está el comando que puse en el Makefile:

run_tests:
    python -m unittest discover .

El Makefile es importante no solo por el comando que ejecuta sino también por el lugar desde donde lo ejecuta . Si hiciera un cd en las pruebas y lo hiciera python -m unittest discover ., no funcionaría porque el script de inicio en unit_tests llama a os.getcwd (), que luego apuntaría a la ruta absoluta incorrecta (que se agregaría a sys.path y faltaría) tu carpeta de origen). Los scripts se ejecutarían desde que find encuentra todas las pruebas, pero no se ejecutarán correctamente. Así que el Makefile está ahí para evitar tener que recordar este problema.

Realmente me gusta este enfoque porque no tengo que tocar mi carpeta src, mis pruebas unitarias o mis variables de entorno y todo funciona sin problemas.

Avísame si les gusta.

Espero que ayude,

Patrick Da Silva
fuente
4

La siguiente es la estructura de mi proyecto:

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

Me pareció mejor importar en el método 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()
rolika
fuente
4

¿Cuál es la forma habitual de ejecutar realmente las pruebas?

Yo uso Python 3.6.2

cd new_project

pytest test/test_antigravity.py

Para instalar pytest :sudo pip install pytest

No configuré ninguna variable de ruta y mis importaciones no fallan con la misma estructura de proyecto de "prueba".

Comenté estas cosas: if __name__ == '__main__'así:

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()
aliopi
fuente
4

Es posible usar el contenedor que ejecuta las pruebas seleccionadas o todas.

Por ejemplo:

./run_tests antigravity/*.py

o para ejecutar todas las pruebas de forma recursiva use globbing ( tests/**/*.py) (activar por shopt -s globstar).

El contenedor básicamente puede usarse argparsepara analizar los argumentos como:

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

Luego cargue todas las pruebas:

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

luego agréguelos a su conjunto de pruebas (usando 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))

y ejecutarlos:

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

Mira este ejemplo para más detalles.

Consulte también: ¿Cómo ejecutar todas las pruebas unitarias de Python en un directorio?

kenorb
fuente
4

Python 3+

Agregando a @Pierre

Usando la unittestestructura de directorios de esta manera:

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

Para ejecutar el módulo de prueba test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

O un solo TestCase

$ python -m unittest test.test_antigravity.GravityTestCase

Obligatorio , no olvide que __init__.pyincluso si está vacío, de lo contrario no funcionará.

eusoubrasileiro
fuente
2

No puede importar desde el directorio principal sin un poco de vudú. Aquí hay otra forma que funciona con al menos Python 3.6.

Primero, tenga un archivo test / context.py con el siguiente contenido:

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

Luego tenga la siguiente importación en el archivo test / test_antigravity.py:

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

Tenga en cuenta que la razón de esta cláusula try-except es que

  • importar test.context falla cuando se ejecuta con "python test_antigravity.py" y
  • el contexto de importación falla cuando se ejecuta con "python -m unittest" desde el directorio new_project.

Con este truco, ambos trabajan.

Ahora puede ejecutar todos los archivos de prueba dentro del directorio de prueba con:

$ pwd
/projects/new_project
$ python -m unittest

o ejecutar un archivo de prueba individual con:

$ cd test
$ python test_antigravity

Ok, no es mucho más bonito que tener el contenido de context.py dentro de test_antigravity.py, pero quizás un poco. Las sugerencias son bienvenidas.

tjk
fuente
2

Si tiene varios directorios en su directorio de prueba, entonces debe agregar a cada directorio un __init__.pyarchivo.

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

Luego, para ejecutar cada prueba a la vez, ejecute:

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

Fuente: 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)
Qlimax
fuente
1

Este script de BASH ejecutará el directorio de prueba de prueba de unidad de Python desde cualquier parte del sistema de archivos, sin importar en qué directorio de trabajo se encuentre.

Esto es útil cuando se está en el directorio ./srco en el ./exampletrabajo y necesita una prueba de unidad rápida:

#!/bin/bash

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

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

No es necesario que un test/__init__.pyarchivo cargue su paquete / sobrecarga de memoria durante la producción.

John Greene
fuente
1

De esta manera, podrá ejecutar los scripts de prueba desde donde desee sin perder el tiempo con las variables del sistema desde la línea de comandos.

Esto agrega la carpeta principal del proyecto a la ruta de Python, con la ubicación encontrada en relación con el script en sí, no en relación con el directorio de trabajo actual.

import sys, os

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

Agregue eso a la parte superior de todos sus scripts de prueba. Eso agregará la carpeta principal del proyecto a la ruta del sistema, por lo que cualquier importación de módulos que funcione desde allí ahora funcionará. Y no importa desde dónde ejecute las pruebas.

Obviamente, puede cambiar el archivo project_path_hack para que coincida con la ubicación de su carpeta principal del proyecto.

chasmani
fuente
0

Si está buscando una solución de línea de comandos solamente:

Basado en la siguiente estructura de directorios (generalizada con un directorio fuente dedicado):

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

Windows : (en new_project)

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

Vea esta pregunta si desea usar esto en un lote for-loop.

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

Con este enfoque, también es posible agregar más directorios a PYTHONPATH si es necesario.

pj.dewitte
fuente
0

Realmente deberías usar la herramienta pip.

Úselo pip install -e .para instalar su paquete en modo de desarrollo. Esta es una muy buena práctica, recomendada por pytest (consulte su documentación de buenas prácticas , donde también puede encontrar dos diseños de proyectos para seguir).

calamar
fuente
¿Por qué rechazar esta respuesta? Leí la respuesta aceptada y, aunque no estuvo mal, pytestes mucho mejor ejecutar pruebas, debido a la salida de la consola que obtienes, en color, con información de seguimiento de la pila e información detallada de error de aserción.
aliopi