¿Cómo ejecuto todas las pruebas unitarias de Python en un directorio?

Tengo un directorio que contiene mis pruebas unitarias de Python. Cada módulo de prueba de unidad tiene la forma de prueba _ *. Py . Estoy intentando crear un archivo llamado all_test.py que, adivinen , ejecutará todos los archivos en el formulario de prueba mencionado anteriormente y devolverá el resultado. He intentado dos métodos hasta ahora; ambos han fracasado Mostraré los dos métodos y espero que alguien sepa cómo hacerlo correctamente.

Para mi primer bash valeroso, pensé: “Si solo importo todos mis módulos de prueba en el archivo y luego llamo a esta unittest.main() doodad, funcionará, ¿verdad?” Bueno, resulta que estaba equivocado.

 import glob import unittest testSuite = unittest.TestSuite() test_file_strings = glob.glob('test_*.py') module_strings = [str[0:len(str)-3] for str in test_file_strings] if __name__ == "__main__": unittest.main() 

Esto no funcionó, el resultado que obtuve fue:

 $ python all_test.py ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK 

Para mi segundo bash, pensé, bueno, tal vez intentaré hacer todo esto de manera más “manual”. Así que intenté hacer eso a continuación:

 import glob import unittest testSuite = unittest.TestSuite() test_file_strings = glob.glob('test_*.py') module_strings = [str[0:len(str)-3] for str in test_file_strings] [__import__(str) for str in module_strings] suites = [unittest.TestLoader().loadTestsFromName(str) for str in module_strings] [testSuite.addTest(suite) for suite in suites] print testSuite result = unittest.TestResult() testSuite.run(result) print result #Ok, at this point I have a result #How do I display it as the normal unit test command line output? if __name__ == "__main__": unittest.main() 

Esto tampoco funcionó, pero parece tan cerca!

 $ python all_test.py <unittest.TestSuite tests=[<unittest.TestSuite tests=[<unittest.TestSuite tests=[]>]>]>  ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK 

Parece que tengo una suite de algún tipo, y puedo ejecutar el resultado. Estoy un poco preocupado por el hecho de que dice que solo he run=1 , parece que debería run=2 , pero es progreso. Pero, ¿cómo paso y muestro el resultado a main? O, ¿cómo básicamente lo hago funcionar para que pueda ejecutar este archivo y, al hacerlo, ejecutar todas las pruebas de unidad en este directorio?

Podría usar un corredor de prueba que haría esto por usted. La nariz es muy buena por ejemplo. Cuando se ejecute, encontrará pruebas en el árbol actual y las ejecutará.

Actualizado:

Aquí hay un código de mis días anteriores a la nariz. Probablemente no desee la lista explícita de nombres de módulos, pero tal vez el rest le sea útil.

 testmodules = [ 'cogapp.test_makefiles', 'cogapp.test_whiteutils', 'cogapp.test_cogapp', ] suite = unittest.TestSuite() for t in testmodules: try: # If the module defines a suite() function, call it to get the suite. mod = __import__(t, globals(), locals(), ['suite']) suitefn = getattr(mod, 'suite') suite.addTest(suitefn()) except (ImportError, AttributeError): # else, just load all the test cases from the module. suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t)) unittest.TextTestRunner().run(suite) 

Con Python 2.7 y versiones posteriores, no tiene que escribir código nuevo ni usar herramientas de terceros para hacer esto; La ejecución de la prueba recursiva a través de la línea de comando está integrada.

 python -m unittest discover  # or python -m unittest discover -s  -p '*_test.py' 

Puede leer más en la documentación de la prueba de la unidad python 2.7 o python 3.x.

Esto ahora es posible directamente desde unittest: unittest.TestLoader.discover .

 import unittest loader = unittest.TestLoader() start_dir = 'path/to/your/test/files' suite = loader.discover(start_dir) runner = unittest.TextTestRunner() runner.run(suite) 

Bueno, al estudiar el código de arriba un poco (específicamente utilizando TextTestRunner y TextTestRunner ), pude acercarme bastante. Finalmente, solucioné mi código al pasar todas las suites de prueba a un solo constructor de suites, en lugar de agregarlas “manualmente”, lo que solucionó mis otros problemas. Así que aquí está mi solución.

 import glob import unittest test_files = glob.glob('test_*.py') module_strings = [test_file[0:len(test_file)-3] for test_file in test_files] suites = [unittest.defaultTestLoader.loadTestsFromName(test_file) for test_file in module_strings] test_suite = unittest.TestSuite(suites) test_runner = unittest.TextTestRunner().run(test_suite) 

Sí, probablemente es más fácil usar la nariz que hacer esto, pero eso no es lo importante.

En Python 3, si estás usando unittest.TestCase :

  • Debe tener un archivo vacío (o de otro modo) __init__.py en su directorio de test ( debe llamarse test/ )
  • Sus archivos de prueba dentro de test/ coinciden con la test_*.py patrón test_*.py . Pueden estar dentro de un subdirectorio bajo test/ , y esos subdirios pueden ser nombrados como cualquier cosa.

Luego, puedes ejecutar todas las pruebas con:

 python -m unittest 

¡Hecho! Una solución de menos de 100 líneas. Esperemos que otro principiante de python ahorre tiempo al encontrar esto.

Si desea ejecutar todas las pruebas de varias clases de casos de prueba y está feliz de especificarlas explícitamente, puede hacerlo de la siguiente manera:

 from unittest import TestLoader, TextTestRunner, TestSuite from uclid.test.test_symbols import TestSymbols from uclid.test.test_patterns import TestPatterns if __name__ == "__main__": loader = TestLoader() tests = [ loader.loadTestsFromTestCase(test) for test in (TestSymbols, TestPatterns) ] suite = TestSuite(tests) runner = TextTestRunner(verbosity=2) runner.run(suite) 

donde uclid es mi proyecto y TestSymbols y TestPatterns son subclases de TestCase .

He utilizado el método de discover y una sobrecarga de load_tests para lograr este resultado en un número de líneas de código (mínimo, creo):

 def load_tests(loader, tests, pattern): ''' Discover and load all unit tests in all files named ``*_test.py`` in ``./src/`` ''' suite = TestSuite() for all_test_suite in unittest.defaultTestLoader.discover('src', pattern='*_tests.py'): for test_suite in all_test_suite: suite.addTests(test_suite) return suite if __name__ == '__main__': unittest.main() 

Ejecución en fives algo así como

 Ran 27 tests in 0.187s OK 

Intenté varios enfoques, pero todos parecen defectuosos o tengo que maquillar un código, eso es molesto. Pero hay una forma conveniente en Linux, que es simplemente encontrar cada prueba a través de cierto patrón y luego invocarlas una por una.

 find . -name 'Test*py' -exec python '{}' \; 

y lo más importante, definitivamente funciona.

En el caso de una biblioteca o aplicación empaquetada , no desea hacerlo. setuptools lo hará por ti .

Para usar este comando, las pruebas de su proyecto deben envolverse en un conjunto de pruebas de prueba de unidad mediante una función, una clase o método TestCase, o un módulo o paquete que TestCase clases TestCase . Si la suite nombrada es un módulo, y el módulo tiene una función additional_tests() , se llama y el resultado (que debe ser una prueba de unittest.TestSuite . unittest.TestSuite ) se agrega a las pruebas que se ejecutarán. Si la suite nombrada es un paquete, todos los submódulos y subpaquetes se agregan recursivamente a la suite de prueba general .

Solo dile dónde está tu paquete de prueba de raíz, como:

 setup( # ... test_suite = 'somepkg.test' ) 

Y ejecute python setup.py test .

El descubrimiento basado en archivos puede ser problemático en Python 3, a menos que evite las importaciones relativas en su conjunto de pruebas, porque el discover utiliza la importación de archivos. A pesar de que soporta top_level_dir opcional, pero tuve algunos errores de recursión infinitos. Por lo tanto, una solución simple para un código no empaquetado es colocar lo siguiente en __init__.py de su paquete de prueba (ver Protocolo load_tests ).

 import unittest from . import foo, bar def load_tests(loader, tests, pattern): suite = unittest.TestSuite() suite.addTests(loader.loadTestsFromModule(foo)) suite.addTests(loader.loadTestsFromModule(bar)) return suite 

Uso PyDev / LiClipse y realmente no he descubierto cómo ejecutar todas las pruebas a la vez desde la GUI. (Editar: hace clic derecho en la carpeta de prueba raíz y selecciona Run as -> Python unit-test

Esta es mi solución actual:

 import unittest def load_tests(loader, tests, pattern): return loader.discover('.') if __name__ == '__main__': unittest.main() 

Puse este código en un módulo llamado all en mi directorio de prueba. Si ejecuto este módulo como una prueba de unidad de LiClipse, se ejecutan todas las pruebas. Si le pido que repita solo pruebas específicas o fallidas, entonces solo se ejecutan esas pruebas. Tampoco interfiere con mi corredor de prueba de la línea de comandos (pruebas), se ignora.

Es posible que deba cambiar los argumentos para discover según la configuración de su proyecto.

Basándome en la respuesta de Stephen Cagle , agregué soporte para módulos de prueba nesteds.

 import fnmatch import os import unittest def all_test_modules(root_dir, pattern): test_file_names = all_files_in(root_dir, pattern) return [path_to_module(str) for str in test_file_names] def all_files_in(root_dir, pattern): matches = [] for root, dirnames, filenames in os.walk(root_dir): for filename in fnmatch.filter(filenames, pattern): matches.append(os.path.join(root, filename)) return matches def path_to_module(py_file): return strip_leading_dots( \ replace_slash_by_dot( \ strip_extension(py_file))) def strip_extension(py_file): return py_file[0:len(py_file) - len('.py')] def replace_slash_by_dot(str): return str.replace('\\', '.').replace('/', '.') def strip_leading_dots(str): while str.startswith('.'): str = str[1:len(str)] return str module_names = all_test_modules('.', '*Tests.py') suites = [unittest.defaultTestLoader.loadTestsFromName(mname) for mname in module_names] testSuite = unittest.TestSuite(suites) runner = unittest.TextTestRunner(verbosity=1) runner.run(testSuite) 

El código busca en todos los subdirectorios de . para *Tests.py archivos que luego se cargan. Se espera que cada *Tests.py contenga una sola clase *Tests(unittest.TestCase) que se cargan y se ejecutan una tras otra.

Esto funciona con anidación profunda y arbitraria de directorios / módulos, pero cada directorio intermedio debe contener al menos un archivo __init__.py vacío. Esto permite que la prueba cargue los módulos nesteds al reemplazar las barras inclinadas (o barras diagonales inversas) por puntos (vea replace_slash_by_dot ).

Esta secuencia de comandos BASH ejecutará el directorio de prueba de prueba de python desde CUALQUIER LUGAR en el sistema de archivos, sin importar en qué directorio de trabajo se encuentre: su directorio de trabajo siempre estará donde se encuentre ese directorio de test .

TODAS LAS PRUEBAS, $ PWD independientes

El módulo Python de unittest es sensible a su directorio actual, a menos que le diga dónde (usando la opción discover -s ).

Esto es útil cuando se mantiene en el directorio de trabajo ./src o ./example y necesita una prueba de unidad general rápida:

 #!/bin/bash this_program="$0" dirname="`dirname $this_program`" readlink="`readlink -e $dirname`" python -m unittest discover -s "$readlink"/test -v 

PRUEBAS SELECCIONADAS, $ PWD independientes

runone.py este archivo de utilidad: runone.py y lo uso así:

 runone.py  
 #!/bin/bash this_program="$0" dirname="`dirname $this_program`" readlink="`readlink -e $dirname`" (cd "$dirname"/test; python -m unittest $1) 

No es necesario un archivo de test/__init__.py para cargar su paquete / sobrecarga de memoria durante la producción.

Debido a que el descubrimiento de prueba parece ser un tema completo, hay un marco dedicado para probar el descubrimiento:

  • nariz
  • Py.Test

Más información aquí: https://wiki.python.org/moin/PythonTestingToolsTaxonomy

Este es mi enfoque al crear un contenedor para ejecutar pruebas desde la línea de comandos:

 #!/usr/bin/env python3 import os, sys, unittest, argparse, inspect, logging if __name__ == '__main__': # Parse arguments. parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-?", "--help", action="help", help="show this help message and exit" ) parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="increase output verbosity" ) parser.add_argument("-d", "--debug", action="store_true", dest="debug", help="show debug messages" ) parser.add_argument("-h", "--host", action="store", dest="host", help="Destination host" ) parser.add_argument("-b", "--browser", action="store", dest="browser", help="Browser driver.", choices=["Firefox", "Chrome", "IE", "Opera", "PhantomJS"] ) parser.add_argument("-r", "--reports-dir", action="store", dest="dir", help="Directory to save screenshots.", default="reports") parser.add_argument('files', nargs='*') args = parser.parse_args() # Load files from the arguments. for filename in args.files: exec(open(filename).read()) # See: http://codereview.stackexchange.com/q/88655/15346 def make_suite(tc_class): testloader = unittest.TestLoader() testnames = testloader.getTestCaseNames(tc_class) suite = unittest.TestSuite() for name in testnames: suite.addTest(tc_class(name, cargs=args)) return suite # Add all tests. alltests = unittest.TestSuite() for name, obj in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(obj) and name.startswith("FooTest"): alltests.addTest(make_suite(obj)) # Set-up logger verbose = bool(os.environ.get('VERBOSE', args.verbose)) debug = bool(os.environ.get('DEBUG', args.debug)) if verbose or debug: logging.basicConfig( stream=sys.stdout ) root = logging.getLogger() root.setLevel(logging.INFO if verbose else logging.DEBUG) ch = logging.StreamHandler(sys.stdout) ch.setLevel(logging.INFO if verbose else logging.DEBUG) ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s')) root.addHandler(ch) else: logging.basicConfig(stream=sys.stderr) # Run tests. result = unittest.TextTestRunner(verbosity=2).run(alltests) sys.exit(not result.wasSuccessful()) 

Por motivos de simplicidad, disculpe mis estándares de encoding que no son PEP8 .

Luego, puede crear la clase BaseTest para componentes comunes para todas sus pruebas, de modo que cada una de sus pruebas simplemente se vería así:

 from BaseTest import BaseTest class FooTestPagesBasic(BaseTest): def test_foo(self): driver = self.driver driver.get(self.base_url + "/") 

Para ejecutar, simplemente especifique pruebas como parte de los argumentos de la línea de comando, por ejemplo:

 ./run_tests.py -h http://example.com/ tests/**/*.py