¿Cómo ejecutar unittest discover desde "python setup.py test"?

82

Estoy tratando de averiguar cómo python setup.py testejecutar el equivalente de python -m unittest discover. No quiero usar un script run_tests.py y no quiero usar ninguna herramienta de prueba externa (como noseo py.test). Está bien si la solución solo funciona en Python 2.7.

En setup.py, creo que necesito agregar algo a los campos test_suitey / o test_loaderen la configuración, pero parece que no puedo encontrar una combinación que funcione correctamente:

config = {
    'name': name,
    'version': version,
    'url': url,
    'test_suite': '???',
    'test_loader': '???',
}

¿Es esto posible usando solo unittestconstruido en Python 2.7?

FYI, la estructura de mi proyecto se ve así:

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  run_tests.py <- I want to delete this
  setup.py

Actualización : esto es posible con unittest2pero quiero encontrar algo equivalente usando solounittest

De https://pypi.python.org/pypi/unittest2

unittest2 incluye un colector de pruebas compatible con setuptools muy básico. Especifique test_suite = 'unittest2.collector' en su setup.py. Esto inicia el descubrimiento de pruebas con los parámetros predeterminados del directorio que contiene setup.py, por lo que quizás sea más útil como ejemplo (consulte unittest2 / collector.py).

Por ahora, solo estoy usando un script llamado run_tests.py, pero espero poder deshacerme de esto moviéndome a una solución que solo use python setup.py test.

Aquí está el run_tests.pyque espero eliminar:

import unittest

if __name__ == '__main__':

    # use the default shared TestLoader instance
    test_loader = unittest.defaultTestLoader

    # use the basic test runner that outputs to sys.stderr
    test_runner = unittest.TextTestRunner()

    # automatically discover all tests in the current dir of the form test*.py
    # NOTE: only works for python 2.7 and later
    test_suite = test_loader.discover('.')

    # run the test suite
    test_runner.run(test_suite)
cdwilson
fuente
Solo una advertencia para cualquiera que venga aquí. La prueba setup.py se considera un "olor" de código y también está en desuso. github.com/pytest-dev/pytest-runner/issues/50
Yashash Gaurav

Respuestas:

44

Si usa py27 + o py32 +, la solución es bastante simple:

test_suite="tests",
saschpe
fuente
1
Ojalá esto funcionara mejor, me encontré con este problema: stackoverflow.com/questions/6164004/… "Los nombres de las pruebas deben coincidir con los nombres de los módulos. Si hay una prueba" foo_test.py ", debe haber un módulo correspondiente foo.py . "
Charles L.
1
Estoy de acuerdo. En mi caso, donde estoy probando un Python externo donde literalmente no existe un módulo de Python con un .py, parece que no hay una buena manera de lograrlo.
Tom Swirly
2
Ésta es la solución correcta. No tuve el problema @CharlesL. tenido. Todas mis pruebas tienen nombre test_*.py. Además, descubrí que en realidad buscará recursivamente a través del directorio dado para encontrar cualquier clase que se extienda unittest.TestCast. Esto es extremadamente útil si tiene una estructura de directorio en la que tiene tests/first_batch/test_*.pyy tests/second_batch/test_*.py. Simplemente puede especificar test_suite="tests",y recogerá todo de forma recursiva. Tenga en cuenta que cada directorio anidado deberá tener un __init__.pyarchivo.
dcmm88
39

De la creación y distribución de paquetes con herramientas de configuración (el énfasis es mío):

Banco de pruebas

Una cadena que nombra una subclase unittest.TestCase (o un paquete o módulo que contiene uno o más de ellos, o un método de dicha subclase), o que nombra una función que se puede llamar sin argumentos y devuelve unittest.TestSuite .

Por lo tanto, setup.pyagregaría una función que devuelva un TestSuite:

import unittest
def my_test_suite():
    test_loader = unittest.TestLoader()
    test_suite = test_loader.discover('tests', pattern='test_*.py')
    return test_suite

Luego, especificaría el comando de la setupsiguiente manera:

setup(
    ...
    test_suite='setup.my_test_suite',
    ...
)
Michael G
fuente
3
Hay un problema con esta solución, porque crea 2 "niveles" de unittest. Lo que significa que setuptools creará un comando de 'prueba' que intentará crear un TestSuite desde setup.my_test_suite, lo que lo obligará a importar setup.py, que ejecutará setup () nuevamente. Esta segunda vez creará un nuevo comando de prueba (anidado) que ejecuta la prueba deseada. Es posible que esto no sea notorio para la mayoría de las personas, pero si intenta extender el comando de prueba (necesitaba modificarlo porque no puedo ejecutar mis pruebas 'en el lugar'), es posible que se encuentre con problemas extraños. Utilice stackoverflow.com/a/21726329/3272850 en su lugar
dcmm88
2
Esto hace que las pruebas se ejecuten dos veces por las razones mencionadas anteriormente. Se corrigió moviendo la función a la __init__.pycarpeta de pruebas y haciendo referencia a eso.
Anónimo
3
El problema con las pruebas que se ejecutan dos veces se puede solucionar fácilmente ejecutando la setup()función dentro del if __name__ == '__main__':bloque en el setup.pyscript. La primera vez que se ejecuta el script de configuración, por lo que se llamará al bloque if; la segunda vez, el script de configuración se importará como un módulo, por lo que no se llamará al bloque if.
hoefling
Hmm, me doy cuenta de que mi setup.py NO contiene ese test_suiteparámetro en absoluto, pero "python setup.py test" todavía funciona bien para mí. Eso es diferente de lo que dice la documentación : "Si no configuró un test_suite en su llamada setup () y no proporciona una opción --test-suite, se producirá un error". ¿Alguna idea?
RayLuo
21

No necesita configuración para que esto funcione. Básicamente, hay dos formas principales de hacerlo:

La forma rapida

Cambie el nombre de su test_module.pya module_test.py(básicamente agregue _testcomo sufijo a las pruebas para un módulo en particular), y Python lo encontrará automáticamente. Solo asegúrate de agregar esto a setup.py:

from setuptools import setup, find_packages

setup(
    ...
    test_suite = 'tests',
    ...
)

El largo camino

A continuación, le indicamos cómo hacerlo con su estructura de directorio actual:

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  run_tests.py <- I want to delete this
  setup.py

En tests/__init__.py, desea importar el unittesty su script de prueba unitaria test_moduley luego crear una función para ejecutar las pruebas. En tests/__init__.py, escriba algo como esto:

import unittest
import test_module

def my_module_suite():
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromModule(test_module)
    return suite

La TestLoaderclase tiene otras funciones además loadTestsFromModule. Puedes correr dir(unittest.TestLoader)para ver los demás, pero este es el más sencillo de usar.

Dado que la estructura de su directorio es tal, probablemente querrá test_moduleque pueda importar su modulescript. Es posible que ya lo haya hecho, pero en caso de que no lo haya hecho, puede incluir la ruta principal para poder importar el packagemódulo y el modulescript. En la parte superior de tu test_module.py, escribe:

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

import unittest
import package.module
...

Luego, finalmente, setup.pyincluya el testsmódulo y ejecute el comando que creó my_module_suite:

from setuptools import setup, find_packages

setup(
    ...
    test_suite = 'tests.my_module_suite',
    ...
)

Entonces simplemente corre python setup.py test.

Aquí hay una muestra que alguien hizo como referencia.

antimateria
fuente
2
La pregunta era cómo hacer que "python setup.py test" usara la capacidad de descubrimiento de unittest. Esto no aborda eso en absoluto.
mikenerone
Ugh ... sí, pensé completamente que la pregunta era hacer algo diferente. No estoy seguro de cómo sucedió eso, debo estar perdiendo la cabeza :(
antimateria
5

Una posible solución es simplemente extender el testcomando para distutilsy setuptools/ distribute. Esto parece un kluge total y mucho más complicado de lo que preferiría, pero parece descubrir y ejecutar correctamente todas las pruebas en mi paquete al ejecutarse python setup.py test. Estoy esperando seleccionar esto como la respuesta a mi pregunta con la esperanza de que alguien brinde una solución más elegante :)

(Inspirado en https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner )

Ejemplo setup.py:

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

def discover_and_run_tests():
    import os
    import sys
    import unittest

    # get setup.py directory
    setup_file = sys.modules['__main__'].__file__
    setup_dir = os.path.abspath(os.path.dirname(setup_file))

    # use the default shared TestLoader instance
    test_loader = unittest.defaultTestLoader

    # use the basic test runner that outputs to sys.stderr
    test_runner = unittest.TextTestRunner()

    # automatically discover all tests
    # NOTE: only works for python 2.7 and later
    test_suite = test_loader.discover(setup_dir)

    # run the test suite
    test_runner.run(test_suite)

try:
    from setuptools.command.test import test

    class DiscoverTest(test):

        def finalize_options(self):
            test.finalize_options(self)
            self.test_args = []
            self.test_suite = True

        def run_tests(self):
            discover_and_run_tests()

except ImportError:
    from distutils.core import Command

    class DiscoverTest(Command):
        user_options = []

        def initialize_options(self):
                pass

        def finalize_options(self):
            pass

        def run(self):
            discover_and_run_tests()

config = {
    'name': 'name',
    'version': 'version',
    'url': 'http://example.com',
    'cmdclass': {'test': DiscoverTest},
}

setup(**config)
cdwilson
fuente
3

Otra solución menos que ideal ligeramente inspirada en http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py

Agregue un módulo que devuelva una TestSuitede las pruebas descubiertas. Luego configure el programa de instalación para llamar a ese módulo.

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  discover_tests.py
  setup.py

Aquí está discover_tests.py:

import os
import sys
import unittest

def additional_tests():
    setup_file = sys.modules['__main__'].__file__
    setup_dir = os.path.abspath(os.path.dirname(setup_file))
    return unittest.defaultTestLoader.discover(setup_dir)

Y aqui esta setup.py:

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

config = {
    'name': 'name',
    'version': 'version',
    'url': 'http://example.com',
    'test_suite': 'discover_tests',
}

setup(**config)
cdwilson
fuente
3

El unittestmódulo de biblioteca estándar de Python admite el descubrimiento (en Python 2.7 y posterior, y Python 3.2 y posterior). Si puede asumir esas versiones mínimas, simplemente puede agregar el discoverargumento de la línea de comando al unittestcomando.

Solo se necesita un pequeño ajuste para setup.py:

import setuptools.command.test
from setuptools import (find_packages, setup)

class TestCommand(setuptools.command.test.test):
    """ Setuptools test command explicitly using test discovery. """

    def _test_args(self):
        yield 'discover'
        for arg in super(TestCommand, self)._test_args():
            yield arg

setup(
    ...
    cmdclass={
        'test': TestCommand,
    },
)
mikenerone
fuente
Por cierto, supongo anteriormente que solo está apuntando a las versiones de Python que admiten el descubrimiento real (2.7 y 3.2+), ya que la pregunta es sobre esta característica específicamente. Por supuesto, podría envolver el inserto en una verificación de versión si también desea seguir siendo compatible con versiones anteriores (por lo tanto, utilizando el cargador estándar de setuptools en esos casos).
mikenerone
0

Esto no eliminará run_tests.py, pero hará que funcione con setuptools. Añadir:

class Loader(unittest.TestLoader):
    def loadTestsFromNames(self, names, _=None):
        return self.discover(names[0])

Luego, en setup.py: (supongo que estás haciendo algo como setup(**config))

config = {
    ...
    'test_loader': 'run_tests:Loader',
    'test_suite': '.', # your start_dir for discover()
}

El único inconveniente que veo es que está doblando la semántica de loadTestsFromNames, pero el comando de prueba de setuptools es el único consumidor y lo llama de una manera específica .

jwelsh
fuente