python django unit-testing pycharm

python - Ejecutar/depurar las pruebas unitarias de una aplicación Django desde el menú contextual del botón derecho del mouse en PyCharm Community Edition?



unit-testing (1)

1. Información de fondo

  • Solo trabajo con Django durante ~ 3 meses
  • Con respecto a PyCharm , trabajé con él durante algunos años, pero solo como IDE (como PyCharm para tontos ), por lo que no me metí en sus cosas avanzadas

Teniendo en cuenta lo anterior, algunas (o todas) partes de la solución pueden parecer engorrosas / estúpidas para algunos usuarios avanzados, así que tengan paciencia conmigo. Incorporaré cualquier comentario posible que agregue valor a la solución.

Volviendo a la pregunta: hice mis pruebas / investigaciones en un proyecto que consiste en el Tutorial Django ( [DjangoProject]: Escribiendo su primera aplicación Django ) + algunas partes del Tutorial Django Rest Framework ( [DRF]: Inicio rápido ). Como ejemplo, voy a intentar ejecutar sondeos / tests.py : QuestionViewTests.test_index_view_with_no_questions()

Como nota, establecer DJANGO_SETTINGS_MODULE como la excepción indica, activa otro , y así sucesivamente ...

2. Crear una configuración de Python

Aunque esta no es una respuesta a la pregunta (solo está relacionada remotamente), la estoy publicando de todos modos (estoy seguro de que muchas personas ya lo hicieron):

  • Haga clic en el menú Ejecutar -> Editar configuraciones ...
  • En el cuadro de diálogo Ejecutar / Depurar configuraciones :
    • Agregue una nueva configuración que tenga el tipo: Python
    • Establezca el directorio de trabajo en la ruta raíz de su proyecto (para mí es " E: / Work / Dev / Django / Tutorials / proj0 / src "). Por defecto, esto también agregará la ruta en las rutas de búsqueda de módulos de Python
    • Establezca el script en el script de inicio del proyecto Django ( manage.py )
    • Establezca los parámetros de Script en los parámetros de prueba ( test QuestionViewTests.test_index_view_with_no_questions )
    • Asigne un nombre a su configuración (opcional) y haga clic en Aceptar . Ahora, podrás ejecutar esta prueba

Por supuesto, tener que hacer esto para cada caso de prueba (y sus métodos) no es el camino a seguir (es realmente molesto), por lo que este enfoque no es escalable.

3. Ajustar PyCharm para hacer lo que queramos

Solo para tener en cuenta que no veo esto como una solución verdadera, es más como una solución (poco convincente ) ( gainarie ), y también es intrusiva.

Comencemos por ver qué sucede cuando hacemos RClick en una prueba (voy a usar este término en general, podría significar Caso de prueba o método o archivo de prueba completo, a menos que se especifique lo contrario). Para mí, está ejecutando el siguiente comando:

"E:/Work/Dev/VEnvs/py2713x64-django/Scripts/python.exe" "C:/Install/PyCharm Community Edition/2016.3.2/helpers/pycharm/utrunner.py" E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py::QuestionViewTests::test_index_view_with_no_questions true

Como puede ver, está lanzando " C: / Install / PyCharm Community Edition / 2016.3.2 / helpers / pycharm / utrunner.py " (me referiré a él como utrunner ) con un montón de argumentos (el primero nos importa, ya que es la especificación de prueba). utrunner utiliza un marco de ejecución de prueba que no le importa Django (en realidad, hay un código de manejo de Django , pero eso no nos ayuda).

Algunas palabras sobre las configuraciones de ejecución / depuración de PyCharm :

  • Cuando RClick -ing en una prueba , PyCharm crea automáticamente una nueva configuración de ejecución (que podrá guardar), tal como lo haría desde el cuadro de diálogo Configuraciones de ejecución / depuración . Una cosa importante a tener en cuenta es el tipo de configuración que es Python tests / Unittests (que dispara automáticamente utrunner )
  • Al crear una configuración Ejecutar en general, PyCharm "copia" la configuración de ese tipo de configuración Predeterminado (se puede ver en el cuadro de diálogo Configuraciones Ejecutar / Depurar ), en la nueva configuración, y llena los demás con datos específicos. Una cosa importante sobre las configuraciones predeterminadas es que están basadas en proyectos : residen en la carpeta .idea ( workspace.xml ) del proyecto, por lo que modificarlas no afectaría a otros proyectos (como temía)

Con lo anterior en mente, procedamos:

Lo primero que debe hacer es: desde el cuadro de diálogo Ejecutar / Depurar configuraciones (menú: Ejecutar -> Editar configuraciones ... ), edite la configuración de Valores predeterminados / Pruebas de Python / Pruebas de unidad :

  • Establecer el directorio de trabajo como en el enfoque anterior
  • En las variables de entorno, agregue una nueva llamada DJANGO_TEST_MODE_GAINARIE y configúrela en cualquier cadena (que no sea vacía / nula )

Lo segundo y lo más complicado (también involucra intrusión): parchear utrunner .

utrunner.patch :

--- utrunner.py.orig 2016-12-28 19:06:22.000000000 +0200 +++ utrunner.py 2017-03-23 15:20:13.643084400 +0200 @@ -113,7 +113,74 @@ except: pass -if __name__ == "__main__": + +def fileToMod(filePath, basePath): + if os.path.exists(filePath) and filePath.startswith(basePath): + modList = filePath[len(basePath):].split(os.path.sep) + mods = ".".join([os.path.splitext(item)[0] for item in modList if item]) + return mods + else: + return None + + +def utrunnerArgToDjangoTest(arg, basePath): + if arg.strip() and not arg.startswith("--"): + testData = arg.split("::") + mods = fileToMod(testData[0], basePath) + if mods: + testData[0] = mods + return ".".join(testData) + else: + return None + else: + return None + + +def flushBuffers(): + sys.stdout.write(os.linesep) + sys.stdout.flush() + sys.stderr.write(os.linesep) + sys.stderr.flush() + + +def runModAsMain(argv, codeGlobals): + with open(argv[0]) as f: + codeStr = f.read() + sys.argv = argv + code = compile(codeStr, os.path.basename(argv[0]), "exec") + codeGlobals.update({ + "__name__": "__main__", + "__file__": argv[0] + }) + exec(code, codeGlobals) + + +def djangoMain(): + djangoTests = list() + basePath = os.getcwd() + for arg in sys.argv[1: -1]: + djangoTest = utrunnerArgToDjangoTest(arg, basePath) + if djangoTest: + djangoTests.append(djangoTest) + if not djangoTests: + debug("/ [DJANGO MODE] Invalid arguments: " + sys.argv[1: -1]) + startupTestArgs = [item for item in os.getenv("DJANGO_STARTUP_TEST_ARGS", "").split(" ") if item] + startupFullName = os.path.join(basePath, os.getenv("DJANGO_STARTUP_NAME", "manage.py")) + if not os.path.isfile(startupFullName): + debug("/ [DJANGO MODE] Invalid startup file: " + startupFullName) + return + djangoStartupArgs = [startupFullName, "test"] + djangoStartupArgs.extend(startupTestArgs) + djangoStartupArgs.extend(djangoTests) + additionalGlobalsStr = os.getenv("DJANGO_STARTUP_ADDITIONAL_GLOBALS", "{}") + import ast + additionalGlobals = ast.literal_eval(additionalGlobalsStr) + flushBuffers() + runModAsMain(djangoStartupArgs, additionalGlobals) + flushBuffers() + + +def main(): arg = sys.argv[-1] if arg == "true": import unittest @@ -186,3 +253,10 @@ debug("/ Loaded " + str(all.countTestCases()) + " tests") TeamcityTestRunner().run(all, **options) + + +if __name__ == "__main__": + if os.getenv("DJANGO_TEST_MODE_GAINARIE"): + djangoMain() + else: + main()

Lo anterior es un diff ( [man7]: DIFF (1) ) (o un parche ; los nombres se pueden usar de forma conjunta; prefiero (y usaré) el parche ): muestra las diferencias entre utrunner.py.orig (el original archivo: que guardé antes de comenzar a modificar, no es necesario que lo haga) y utrunner.py (la versión actual que contiene los cambios). El comando que utilicé es diff --binary -uN utrunner.py.orig utrunner.py (obviamente, en la carpeta de utrunner ). Como comentario personal, el parche es la forma preferida de alterar el código fuente de terceros (para mantener los cambios bajo control y por separado).

Lo que hace el código en el parche (probablemente sea más difícil de seguir que el código Python simple):

  • Todo debajo del bloque principal ( if __name__ == "__main__": o el comportamiento actual) se ha movido a una función llamada main (para mantenerlo separado y evitar alterarlo por error)
  • Se modificó el bloque principal , de modo que si se define el entorno var DJANGO_TEST_MODE_GAINARIE (y no está vacío), seguirá la nueva implementación (función djangoMain ), de lo contrario actuará normalmente . La nueva implementación:
    • fileToMod resta basePath de filePath y convierte la diferencia al estilo del paquete Python . Por fileToMod("E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py", "E:/Work/Dev/Django/Tutorials/proj0/src") : fileToMod("E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py", "E:/Work/Dev/Django/Tutorials/proj0/src") , devolverá polls.tests
    • utrunnerArgToDjangoTest : usa la función anterior y luego agrega el nombre de la clase ( QuestionViewTests ) y (opcionalmente) el nombre del método ( test_index_view_with_no_questions ), por lo que al final convierte la especificación de prueba del formato utrunner ( E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py::QuestionViewTests::test_index_view_with_no_questions ) al formato polls.tests.QuestionViewTests.test_index_view_with_no_questions ( polls.tests.QuestionViewTests.test_index_view_with_no_questions )
    • flushBuffers : escribe un charol Eoln y vacía los buffers stdout y stderr (esto es necesario porque noté que a veces los resultados de PyCharm y Django están intercalados, y el resultado final está desordenado)
    • runModAsMain : normalmente, todo el código manage.py relevante está debajo if __name__ == "__main__": Esta función "engaña" a Python haciéndole creer que manage.py se ejecutó como su 1er argumento

Utrunner parcheado :

  • Hice estas modificaciones por mi cuenta (no busqué versiones con integración de Django e inspiré desde allí)
  • utrunner es parte de PyCharm . Es obvio por qué los chicos de JetBrains no incluyeron ninguna integración de Django en la Edición de la Comunidad : para hacer que la gente compre la Edición Profesional . Esto les pisa los pies. No estoy al tanto de las implicaciones legales de modificar utrunner , pero de todos modos si lo reparas , lo estás haciendo bajo tu propia responsabilidad y riesgo
  • Estilo de codificación: apesta (al menos por nombrar / sangrar PoV ), pero es coherente con el resto del archivo (el único caso en el que se debe permitir que el estilo de codificación succione). [Python]: PEP 8 - Guía de estilo para Python Code contiene las pautas de estilo de codificación para Python
  • El parche se aplica en el archivo original ( utrunner.py ), con las siguientes propiedades (aún válido para v 2019.1 (última comprobación: 20190716 )):
    • tamaño: 5865
    • sha256sum: db98d1043125ce2af9a9c49a1f933969678470icsoft63f791c2460fe090c2948a0
  • Aplicando el parche :
    • utrunner se encuentra en " $ {PYCHARM_INSTALL_DIR} / helpers / pycharm "
    • Por lo general, $ {PYCHARM_INSTALL_DIR} apunta a:
      • Nix : / usr / lib / pycharm-community
      • Win : " C: / Archivos de programa (x86) / JetBrains / PyCharm 2016.3 " (adapte a su número de versión)
    • Guarde el contenido del parche (en un archivo llamado, por ejemplo, utrunner.patch , supongamos que está bajo / tmp )
    • Nix : las cosas son fáciles, solo ( cd a la carpeta de utrunner y) ejecuta patch -i /tmp/utrunner.patch . [man7]: PATCH (1) es una utilidad que se instala por defecto (parte del parche dpkg en Ubtu ). Tenga en cuenta que dado que utrunner.py es propiedad de root , para este paso necesitaría sudo
    • Win : pasos similares a seguir, pero las cosas son más complicadas ya que no hay una utilidad de parche nativa. Sin embargo, hay soluciones alternativas:
      • Usa Cygwin . Como en el caso de Nix ( Lnx ), la utilidad de parche está disponible, pero no se instala de manera predeterminada . El paquete parche debe instalarse explícitamente desde la configuración de Cygwin . Intenté esto y funciona
      • Hay alternativas (no las probé):
      • Al igual que en el caso de Nix , uno de los Administradores debería (probablemente) parchear el archivo. Además, tenga cuidado con las rutas de archivo, asegúrese de (dbl) citarlas si contienen espacios
    • Revertir el parche :
      • Las copias de seguridad no son dañinas (excepto desde el PoV del espacio libre en el disco, o cuando comienzan a acumularse, administrarlas se vuelve un dolor). No hay necesidad de ellos en nuestro caso. Para revertir los cambios, simplemente ejecute el comando en el archivo modificado: patch -Ri /tmp/utrunner.patch , y lo cambiará de nuevo a su contenido original (también creará un archivo utrunner.py.orig con el contenido modificado; en realidad cambiará los archivos .py y .py.orig ).

Un par de palabras sobre este enfoque :

  • El código puede manejar entornos (opcionales) (que no sean DJANGO_TEST_MODE_GAINARIE , que es obligatorio):

    • DJANGO_STARTUP_NAME : en caso de que manage.py tenga otro nombre (¿por alguna razón?), O se encuentre en otra carpeta que no sea el directorio de trabajo . Una cosa importante aquí: cuando especifique rutas de archivos, use el separador de ruta específico de la plataforma: barra ( / ) para Nix , barra de corte ( / ) para Win
    • DJANGO_STARTUP_TEST_ARGS : argumentos adicionales que acepta la manage.py test (ejecute manage.py test --help para obtener la lista completa). Aquí, tengo que insistir en -k / --keepdb que conserva la base de datos de prueba ( prueba _ $ {REGULAR_DB_NAME} por defecto o establecida en la configuración en el diccionario TEST ) entre ejecuciones. Al ejecutar una sola prueba, crear el DB (y aplicar todas las migraciones) y destruirlo puede llevar mucho tiempo (y también ser muy molesto). Este indicador garantiza que la base de datos no se elimine al final y se reutilizará en la próxima ejecución de prueba
    • DJANGO_STARTUP_ADDITIONAL_GLOBALS : esto debe tener la representación de cadena de un dict Python . Los valores que, por alguna razón, requieren que manage.py esté presente en el diccionario globals() , deben colocarse aquí
  • Al modificar una configuración predeterminada , todas las configuraciones creadas previamente que la hereden no se actualizarán , por lo que deben eliminarse manualmente (y los nuevos RClick s las volverán a crear automáticamente en sus pruebas )

RHaga clic en la misma prueba (después de eliminar su configuración anterior: d) y voilà :

E:/Work/Dev/VEnvs/py2713x64-django/Scripts/python.exe "C:/Install/PyCharm Community Edition/2016.3.2/helpers/pycharm/utrunner.py" E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py::QuestionViewTests::test_index_view_with_no_questions true Testing started at 01:38 ... Using existing test database for alias ''default''... . ---------------------------------------------------------------------- Ran 1 test in 0.390s OK Preserving test database for alias ''default''... Process finished with exit code 0

La depuración también funciona (puntos de interrupción, etc.).

Advertencias (hasta ahora identifiqué 2 de ellas):

  • Esto es benigno, es solo un problema de interfaz de usuario : utrunner (muy probablemente) tiene alguna inicialización que PyCharm espera que tenga lugar, lo que obviamente no tiene en nuestro caso. Entonces, incluso si la prueba finalizó con éxito, desde el punto de vista de PyCharm no lo hicieron y, por lo tanto, la ventana Salida contendrá una advertencia: " El marco de prueba se cerró inesperadamente "
  • Esta es desagradable, y no pude llegar al fondo (todavía). Aparentemente, en utrunner cualquier llamada de input ( raw_input ) no se maneja muy bien; el texto de solicitud: " Escriba ''yes'' si desea intentar eliminar la base de datos de prueba ''test_tut-proj0'', o ''no'' para cancelar: " (que aparece si la ejecución de la prueba anterior se bloqueó y su DB no se destruyó en el final) no se muestra y el programa se congela (esto no sucede fuera de utrunner ), sin permitir que el usuario ingrese texto (¿tal vez hay hilos en la mezcla?). La única forma de recuperarse es detener la ejecución de la prueba, eliminar la base de datos y volver a ejecutar la prueba. Nuevamente, tengo que promover el manage.py test -k que solucionará este problema

He trabajado / probado en los siguientes entornos :

  • Nix ( Lnx ):
    • Ubtu 16.04 x64
    • PyCharm Community Edition 2016.3.3
    • Python 3.4.4 ( VEnv )
    • Django 1.9.5
  • Ganar :
    • W10 x64
    • PyCharm Community Edition 2016.3.2
    • Python 2.7.13 ( VEnv )
    • Django 1.10.6

Notas :

  • Continuaré investigando los problemas actuales (al menos el )
  • Una solución limpia sería anular de alguna manera en PyCharm la Prueba unitaria que ejecuta la configuración predeterminada (lo que hice desde el código), pero no pude encontrar ningún archivo de configuración (¿probablemente esté en los tarros de PyCharm ?)
  • Noté que muchos archivos / carpetas que son específicos de Django en la carpeta de ayudantes ( padre de utrunner ), tal vez esos también se puedan usar, tendrán que verificar

Como dije al principio, cualquier sugerencia es más que bienvenida.

@ EDIT0 :

  • Como respondí al comentario de @ Udi, esta es una alternativa para las personas que no pueden pagar (o las empresas que no están dispuestas) a pagar la tarifa de licencia de PyCharm Professional Edition (en una exploración rápida parece que es ~ 100 $ -200 $ / año para cada instancia)

Debo enfatizar en PyCharm Community Edition que no tiene ninguna integración de Django ( v 2016.3.2 en el momento de la pregunta).

Tengo Google d mi problema y (sorprendentemente) no obtuve ninguna respuesta (por supuesto, no excluyo la posibilidad de que pueda haber algunas, pero las extrañé).

La pregunta es simple, en PyCharm , uno puede ejecutar (depurar) una prueba de unidad ( TestCase o uno de sus métodos) con un simple clic derecho del mouse (desde el menú contextual) como en la imagen a continuación:

Desafortunadamente, eso produce una excepción:

Traceback (most recent call last): File "C:/Install/PyCharm Community Edition/2016.3.2/helpers/pycharm/utrunner.py", line 254, in <module> main() File "C:/Install/PyCharm Community Edition/2016.3.2/helpers/pycharm/utrunner.py", line 232, in main module = loadSource(a[0]) File "C:/Install/PyCharm Community Edition/2016.3.2/helpers/pycharm/utrunner.py", line 65, in loadSource module = imp.load_source(moduleName, fileName) File "E:/Work/Dev/Django/Tutorials/proj0/src/polls/tests.py", line 7, in <module> from polls.models import Question File "E:/Work/Dev/Django/Tutorials/proj0/src/polls/models.py", line 9, in <module> class Question(models.Model): File "E:/Work/Dev/Django/Tutorials/proj0/src/polls/models.py", line 10, in Question question_text = models.CharField(max_length=200) File "E:/Work/Dev/VEnvs/py2713x64-django/lib/site-packages/django/db/models/fields/__init__.py", line 1043, in __init__ super(CharField, self).__init__(*args, **kwargs) File "E:/Work/Dev/VEnvs/py2713x64-django/lib/site-packages/django/db/models/fields/__init__.py", line 166, in __init__ self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE File "E:/Work/Dev/VEnvs/py2713x64-django/lib/site-packages/django/conf/__init__.py", line 53, in __getattr__ self._setup(name) File "E:/Work/Dev/VEnvs/py2713x64-django/lib/site-packages/django/conf/__init__.py", line 39, in _setup % (desc, ENVIRONMENT_VARIABLE)) django.core.exceptions.ImproperlyConfigured: Requested setting DEFAULT_INDEX_TABLESPACE, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.

Nota : Solo agregué la pregunta para proporcionar una respuesta que podría ser útil para alguien.