¿Ejecutar / depurar las pruebas de unidad de una aplicación Django desde el menú contextual del botón derecho del ratón en PyCharm Community Edition?

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

Busqué mi problema con Google y (sorprendentemente) no obtuve ninguna respuesta (por supuesto, no excluyo la posibilidad de que pueda haber algunas, pero simplemente 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 con el botón derecho del ratón (en 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  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  from polls.models import Question File "E:\Work\Dev\Django\Tutorials\proj0\src\polls\models.py", line 9, in  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.

Ejecutar Django Unit Test en RClick

1. Información de fondo

  • Solo estoy trabajando con Django por ~ 3 meses
  • En cuanto a PyCharm , trabajé con él durante algunos años, pero solo como un IDE (como PyCharm para dummies ), así que no entré en su materia avanzada

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

De vuelta a la pregunta: hice mis pruebas / investigación en un proyecto que consiste en el Tutorial de Django ( [DjangoProject]: Escribiendo tu primera aplicación de Django ) + algunas partes del Tutorial de Marco de Descanso de Django ( [DRF]: Inicio rápido ). Como ejemplo, voy a intentar ejecutar polls / tests.py : QuestionViewTests.test_index_view_with_no_questions()

Como nota, la configuración de DJANGO_SETTINGS_MODULE como la excepción indica, activa otra , y así sucesivamente …

2. Creando una configuración de Python

Aunque esta no es una respuesta a la pregunta (solo está relacionada de forma remota), la estoy publicando de todas formas (estoy seguro de que mucha gente ya lo hizo):

  • Haga clic en el menú Ejecutar -> Editar configuraciones …
  • En el cuadro de diálogo Ejecutar / Depurar configuraciones :
    • Agrega 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 “). De forma predeterminada, esto también agregará la ruta en la búsqueda de módulos de Python
    • Establezca el Script a su 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. Ajustando PyCharm para hacer lo que queremos

Solo se debe tener en cuenta que no veo esto como una verdadera solución, es más como una solución alternativa (lame) ( gainarie ), y también es intrusivo.

Comencemos observando qué sucede cuando hacemos clic en una prueba ( 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í, se 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 se preocupa por Django (en realidad, hay un código de manejo de Django , pero eso no nos ayuda).

Unas pocas palabras en las configuraciones Run / Debug de PyCharm :

  • Al hacer clic en una prueba , PyCharm crea automáticamente una nueva configuración de ejecución (que podrá guardar), tal como lo haría en 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 automáticamente se activa con el usuario )
  • Al crear una configuración de ejecución en general, PyCharm “copia” las configuraciones de ese tipo de configuración predeterminadas (se pueden ver en el cuadro de diálogo Configuraciones de ejecución / depuración ), en la nueva configuración, y llena las demás con datos específicos. Una cosa importante acerca de 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 otros proyectos (como temí por primera vez)

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 Predeterminados / Pruebas de Python / Unittests :

  • Establecer el directorio de trabajo al igual que 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 involucrando intrusión): parchear a 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 manera conjunta – yo prefiero (y usaré) 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 simple código de Python ):

  • Todo lo que se encuentra 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)
  • El bloque principal se modificó, de modo que si se define la variable env DJANGO_TEST_MODE_GAINARIE (y no está vacía), 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 . Ej: fileToMod("E:\Work\Dev\Django\Tutorials\proj0\src\polls\tests.py", "E:\Work\Dev\Django\Tutorials\proj0\src") , devolverá polls.tests
    • utrunnerArgToDjangoTest : utiliza 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 ) en formato polls.tests.QuestionViewTests.test_index_view_with_no_questions ( polls.tests.QuestionViewTests.test_index_view_with_no_questions )
    • flushBuffers : escribe un eoln char y elimina los buffers stdout y stderr (esto es necesario porque noté que a veces las salidas de PyCharm y Django están intercaladas, y el resultado final está mal)
    • runModAsMain : por lo general, todo el código de manage.py relevante está bajo if __name__ == "__main__": Esta función “engaña” a Python haciéndole creer que manage.py se ejecutó como su primer argumento

Parche utrunner :

  • 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 comunitaria : para que la gente compre la Edición Profesional . Esto un poco pisa sus dedos de los pies. No estoy al tanto de las implicaciones legales de modificar el utrunner , pero de todos modos, si lo soluciona , lo hace bajo su propia responsabilidad y riesgo.
  • Estilo de encoding: apesta (al menos de nombrar / sangrar PoV ), pero es coherente con el rest del archivo (el único caso cuando se debe permitir la succión al estilo de encoding). [Python]: PEP 8 – Guía de estilo para el código de Python contiene las pautas de estilo de encoding para Python
  • El parche se aplica en el archivo original ( utrunner.py ), con las siguientes propiedades (aún válidas para v 2018.3.5 (verificada por última vez: 20190323 )):
    • tamaño: 5865
    • sha256sum: db98d1043125ce2af9a9c49a1f933969678470 Zealand63f791c2460fe090c2948a0
  • Aplicando el parche :
    • utrunner se encuentra en ” $ {PYCHARM_INSTALL_DIR} / helpers / pycharm
    • Normalmente, $ {PYCHARM_INSTALL_DIR} apunta a:
      • Nix : / usr / lib / pycharm-community
      • Win : ” C: \ Archivos de progtwig (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) ejecute el patch -i /tmp/utrunner.patch . [man7]: PATCH (1) es una utilidad que se instala de forma predeterminada (parte del parche dpkg en Ubtu ). Tenga en cuenta que dado que utrunner.py es propiedad de root , para este paso necesitará 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:
      • Utilice Cygwin . Como en el caso de Nix ( Lnx ), la utilidad de parche está disponible, pero no se instala de forma predeterminada . El paquete de parches debe instalarse explícitamente desde la configuración de Cygwin . Probé esto y funciona
      • Hay alternativas (no las probé):
        • [SourceForge]: Parche para Windows
        • En teoría, [RedBean]: svn patch (cualquier cliente) debería poder aplicar un parche , pero no estoy seguro de que el archivo forme parte de una copia de trabajo .
        • Aplicando el parche manualmente (una opción menos deseada :))
      • Al igual que en el caso de Nix , el parcheo del archivo tendría que ser realizado por uno de los Administradores . Además, tenga cuidado con las rutas de los archivos, asegúrese de (dbl) citarlas si contienen espacios
    • Revirtiendo 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 convierte en una molestia). 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 (opcional) env vars (que no sean DJANGO_TEST_MODE_GAINARIE , lo cual 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 rutas específico de la plataforma: barra ( / ) para Nix , bkslash ( \ ) para Win
    • DJANGO_STARTUP_TEST_ARGS : argumentos adicionales que acepta la manage.py test (ejecute la 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} de forma predeterminada o establecida en la configuración en el diccionario TEST ) entre ejecuciones. Al ejecutar una única prueba, crear la base de datos (y aplicar todas las migraciones) y destruirla puede llevar mucho tiempo (y también puede 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 : debe tener la representación de cadena de un dict de Python . Cualquier valor que, por alguna razón, sea requerido por manage.py para estar presente en el diccionario globals() , debe colocarse aquí
  • Al modificar una configuración predeterminada , todas las configuraciones creadas previamente que la heredan no se actualizarán , por lo que deben eliminarse manualmente (y serán recreadas automáticamente por los nuevos RClick 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, y así sucesivamente …).

Advertencias (hasta ahora identifiqué 2 de ellas):

  • Esto es benigno, solo es un problema de la interfaz de usuario : utrunner (lo más probable) tiene alguna inicialización que PyCharm espera que tenga lugar, lo que obviamente no es así en nuestro caso. Entonces, incluso si la prueba terminó con éxito, desde el PoV de PyCharm no lo hicieron y, por lo tanto, la ventana de resultados contendrá una advertencia: ” El marco de prueba se cerró inesperadamente
  • Este es uno desagradable, y no pude llegar al fondo (todavía). Aparentemente, en el traductor, cualquier llamada de input ( raw_input ) no se maneja muy bien; el texto del mensaje: ” Escriba ‘sí’ si desea intentar eliminar la base de datos de prueba ‘test_tut-proj0’, o ‘no’ para cancelar: ” (que aparece si se ejecutó la prueba anterior y su DB no se destruyó en el final) no se muestra y el progtwig se congela (esto no ocurre 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. Una vez más, tengo que promover la 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 segundo)
  • Una solución limpia sería anular de alguna manera en PyCharm the Unit Test ejecutando las configuraciones predeterminadas (lo que hice desde el código), pero no pude encontrar ningún archivo de configuración (probablemente esté en los flasks de PyCharm ?)
  • Noté que muchos de los archivos / carpetas que son específicos de Django en la carpeta de los ayudantes ( padre del utrunner ), tal vez esos también puedan usarse, tendrán que verificar

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

@ EDIT0 :

  • Como respondí al comentario de @Uli, esta es una alternativa para las personas que no pueden pagar (o las compañías que no están dispuestas) a pagar la tarifa de la licencia de la Edición Profesional de PyCharm (en una búsqueda rápida, parece que es ~ 100 $ -200 $ / año para cada instancia)