Prueba de Python con pytest. Usando pytest con otras herramientas, CAPÍTULO 7

Volver


Por lo general, pytest no se usa de forma independiente, sino en un entorno de prueba con otras herramientas. Este capítulo trata sobre otras herramientas que a menudo se usan junto con pytest para realizar pruebas efectivas y eficientes. Aunque de ninguna manera se trata de una lista exhaustiva, las herramientas discutidas aquí le darán una idea del sabor del poder de mezclar Pytest con otras herramientas.



Los ejemplos en este libro están escritos usando Python 3.6 y pytest 3.2. pytest 3.2 es compatible con Python 2.6, 2.7 y Python 3.3+.


El código fuente para el proyecto Tareas, así como para todas las pruebas que se muestran en este libro, está disponible en el enlace en la página web del libro en pragprog.com . No necesita descargar el código fuente para comprender el código de prueba; El código de prueba se presenta de forma conveniente en los ejemplos. Pero para seguir las tareas del proyecto o adaptar ejemplos de prueba para probar su propio proyecto (¡sus manos están desatadas!), Debe ir a la página web del libro y descargar el trabajo. Allí, en la página web del libro, hay un enlace para mensajes de erratas y un foro de discusión .

Debajo del spoiler hay una lista de artículos en esta serie.



pdb: errores de prueba de depuración


El módulo pdb es un depurador de Python en la biblioteca estándar. Utiliza --pdb para que pytest inicie una sesión de depuración en el punto de falla. Veamos pdb en acción en el contexto del proyecto Tareas.


En “Parametrización de accesorios” en la página 64, dejamos el proyecto Tareas con algunos errores:


 $ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds 

Antes de ver cómo pdb puede ayudarnos a depurar esta prueba, echemos un vistazo a las opciones de pytest disponibles para acelerar la depuración de los errores de prueba, que primero examinamos en la sección "Uso de opciones" en la página 9:


  • --tb=[auto/long/short/line/native/no] : controla el estilo de rastreo.
  • -v / --verbose : muestra todos los nombres de prueba que pasaron o fallaron.
  • -l / --showlocals : muestra las variables locales junto al seguimiento de la pila.
  • -lf / --last-failed : ejecuta solo pruebas que fallan.
  • -x / --exitfirst : detiene la sesión de prueba en el primer error.
  • --pdb : inicia una sesión de depuración interactiva en el punto de falla.



Instalar MongoDB




Como se mencionó en el Capítulo 3, “Accesorios de Pytest”, en la página 49, se requiere la instalación de MongoDB y pymongo para ejecutar las pruebas MongoDB.


Probé la versión de Community Server que se encuentra en https://www.mongodb.com/download-center . pymongo se instala con pip : pip install pymongo . Sin embargo, este es el último ejemplo en un libro que usa MongoDB. Para probar el depurador sin usar MongoDB, puede ejecutar los comandos pytest desde el code/ch2/ , ya que este directorio también contiene varias pruebas fallidas.




Acabamos de ejecutar las pruebas del code/ch3/c para asegurarnos de que algunas de ellas no funcionen. No vimos trazas o nombres de prueba porque --tb=no deshabilita el rastreo y no teníamos --verbose habilitado. Repitamos los errores (no más de tres) con el texto detallado:


 $ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ==================== 

Ahora sabemos qué pruebas fallaron. Veamos solo uno de ellos, usando -x , activando el rastreo, no usando --tb=no , y mostrando variables locales con -l :


 $ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============ 

Muy a menudo, esto es suficiente para entender por qué falló la prueba. En este caso particular, está bastante claro que task_id no task_id un entero, es una instancia de ObjectId. ObjectId es el tipo utilizado por MongoDB para los identificadores de objetos en la base de datos. Mi intención con la capa tasksdb_pymongo.py era ocultar ciertos detalles de la implementación de MongoDB del resto del sistema. Está claro que en este caso no funcionó.


Sin embargo, queremos ver cómo usar pdb con pytest, así que imaginemos que no está claro por qué esta prueba falló. Podemos hacer que pytest inicie una sesión de depuración y comience justo en el punto de falla usando --pdb :


 $ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb) 

Ahora que estamos en el indicador (Pdb), tenemos acceso a todas las funciones interactivas de depuración de pdb. Cuando veo bloqueos, uso regularmente estos comandos:


  • p/print expr : imprime el valor de exp.
  • pp expr : Pretty imprime el valor de expr.
  • l/list : l/list el punto de falla y cinco líneas de código arriba y abajo.
  • l/list begin,end : enumera números de línea específicos.
  • a/args : Imprime los argumentos de la función actual con sus valores.
  • u/up : mueve un nivel hacia arriba en la ruta de la pila.
  • d/down : baja un nivel en la traza de la pila.
  • q/quit : finaliza una sesión de depuración.

Otros comandos de navegación, como step y next, no son muy útiles, ya que estamos sentados en la declaración de aserción. También puede simplemente ingresar nombres de variables y obtener valores.


Puede usar p/print expr manera similar a la -l/--showlocals para ver los valores en una función:


 (Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb) 

Ahora puede salir del depurador y continuar con las pruebas.


 (Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds =========== 

Si no usáramos - , pytest abriría nuevamente Pdb en la próxima prueba. Más información sobre el uso del módulo pdb está disponible en la documentación de Python .


Coverage.py: determinación de la cantidad de código de prueba


La cobertura del código es un indicador del porcentaje de código probado que es probado por un conjunto de pruebas. Cuando ejecuta pruebas para el proyecto Tareas, algunas funciones de Tareas se ejecutan con cada prueba, pero no con todas.


Las herramientas de cobertura de código son excelentes para hacerle saber qué partes del sistema son completamente ignoradas por las pruebas.


Coverage.py es la herramienta de cobertura Python preferida que mide la cobertura del código.


Lo usará para verificar el código del proyecto Tareas con pytest.


Para usar coverage.py necesitas instalarlo. No pytest-cov daño instalar un complemento llamado pytest-cov , que le permite llamar a coverage.py desde pytest con algunas opciones adicionales de pytest. Dado que la coverage es una de las dependencias de pytest-cov , simplemente instale pytest-cov y tomará coverage.py :


 $ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1 

Ejecutemos el informe de cobertura para la segunda versión de la tarea. Si todavía tiene instalada la primera versión del proyecto Tareas, desinstálela e instale la versión 2:


 $ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ... 

Ahora que está instalada la próxima versión de las tareas, puede ejecutar el informe de cobertura base:


 $ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds =================== 

Dado que el directorio actual es tasks_proj_v2 , y el código fuente bajo prueba está en src, agregar la opción --cov=src genera un informe de cobertura solo para ese directorio.


Como puede ver, algunos archivos tienen una cobertura bastante baja e incluso 0%. Estos son recordatorios útiles: tasksdb_pymongo.py 0% porque hemos deshabilitado las pruebas para MongoDB en esta versión. Algunos de ellos son bastante bajos. El proyecto ciertamente tendrá que entregar pruebas para todas estas áreas antes de que esté listo para el horario estelar.


Creo que varios archivos tienen un mayor porcentaje de cobertura: api.py y tasksdb_tinydb.py . Echemos un vistazo a tasksdb_tinydb.py y veamos qué falta. Creo que la mejor manera de hacer esto es usar informes HTML.


Si vuelve a ejecutar coverage.py con la --cov-report=html , se generará un --cov-report=html :


 $ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds =================== 

Luego puede abrir htmlcov/index.html en un navegador que muestra el resultado en la siguiente pantalla:



Al hacer clic en tasksdb_tinydb.py se mostrará un informe para un archivo. El porcentaje de líneas cubiertas se muestra en la parte superior del informe, más cuántas líneas están cubiertas y cuántas no, como se muestra en la siguiente pantalla:



Al desplazarse hacia abajo, puede ver las líneas que faltan, como se muestra en la siguiente pantalla:



Incluso si esta pantalla no es una página completa para este archivo, esto es suficiente para decirnos que:


  1. No probamos list_tasks() con el conjunto de propietarios.
  2. No probamos update() ni delete() .
  3. Quizás no estamos probando a fondo unique_id() .

Genial Podemos incluirlos en nuestra lista de pruebas TO-DO junto con probar el sistema de configuración.


Aunque las herramientas de cobertura de código son extremadamente útiles, luchar por una cobertura del 100% puede ser peligroso. Cuando vea código que no se está probando, puede significar que se necesita una prueba. Pero también puede significar que hay algunas funciones del sistema que no son necesarias y pueden eliminarse. Como todas las herramientas de desarrollo de software, el análisis de cobertura de código no reemplaza el pensamiento.


Consulte la coverage.py y pytest-cov obtener más detalles.


simulacro: sustitución de partes del sistema


El paquete simulado se usa para reemplazar partes del sistema para aislar partes del código de prueba del resto del sistema. Simulacro: los objetos a veces se llaman dobles de prueba, espías, falsificaciones o trozos.


Entre su propio accesorio pytest monkeypatch (descrito en Uso de monkeypatch en la página 85) y simulacro, debe tener toda la funcionalidad de prueba dual necesaria.


Atencion Simulacro y muy raro
Si es la primera vez que te encuentras con gemelos de prueba como simulacros, trozos y espías, ¡prepárate! Será muy extraño, muy rápido, divertido, aunque muy impresionante.

El paquete mock viene con la biblioteca estándar de Python, como unittest.mock desde Python 3.3. En versiones anteriores, está disponible como un paquete separado instalado a través de PyPI. Esto significa que puede usar la versión mock PyPI de Python 2.6 a la última versión de Python y obtener la misma funcionalidad que la última versión mock Python. Sin embargo, para usar con pytest, un complemento llamado pytest-mock tiene algunas características que lo convierten en mi interfaz preferida para el sistema simulado.


Para el proyecto Tareas, usaremos mock para ayudarnos a probar la interfaz de línea de comandos. En Coverage.py: al determinar cuánto código se está probando, en la página 129 vio que nuestro archivo cli.py no se probó en absoluto. Comenzaremos a arreglarlo ahora. Pero hablemos primero de estrategia.


La primera solución en el proyecto Tareas fue hacer la mayoría de las pruebas de funcionalidad a través de api.py Por lo tanto, una solución razonable es que la prueba de línea de comando no tiene que ser una prueba funcional completa. Podemos estar seguros de que el sistema funcionará a través de la CLI si obtenemos un nivel API húmedo durante las pruebas de CLI. También es una solución conveniente que nos permite ver moki en esta sección.


La implementación de Tareas CLI utiliza un paquete de interfaz de línea de comando Click de terceros. Existen muchas alternativas para implementar la interfaz de línea de comandos, incluido un módulo integrado en Python argparse . Una de las razones por las que elegí Click es porque incluye un motor de prueba que nos ayuda a probar las aplicaciones Click. Sin embargo, el código en cli.py , aunque esperamos que sea típico de las aplicaciones Click, no es obvio.


Reduzcamos la velocidad e instalemos la tercera versión de Tareas:


 $ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks 

En el resto de esta sección, desarrollará varias pruebas para probar la funcionalidad de "lista".
Vamos a verlo en acción para entender lo que vamos a verificar:


Nota del traductor: Al usar la plataforma Windows, encontré varios problemas al probar la sesión a continuación.
  1. Se debe crear una carpeta para la base de datos llamada tasks_db en la carpeta de su usuario. Por ejemplo c:\Users\User_1\tasks_db\
    De lo contrario, obtenemos - >> FileNotFoundError: [Errno 2] No existe tal archivo o directorio: 'c: \ Users \ User_1 // tareas_db // tareas_db.json'
  2. Use comillas dobles en lugar de un apóstrofe. De lo contrario, obtenga un error
    'hacer algo genial'
    Uso: las tareas agregan [OPCIONES] RESUMEN
    Intente "tareas agregar -h" para obtener ayuda.

    Error: Tengo argumentos extra inesperados (algo genial ')


 $ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat 

Se ve bastante simple. El comando de tasks list muestra una lista de todas las tareas bajo el encabezado.
El título se imprime incluso si la lista está vacía. El comando solo muestra datos de un propietario, si --owner -o o --owner . ¿Y cómo verificamos esto? Hay muchas formas, pero vamos a usar moki.


Las pruebas que usan MOK son necesariamente pruebas de caja blanca , y debemos analizar el código para decidir qué y dónde vamos a batear. El punto de entrada principal está aquí:


ch7 / task_proj_v2 / src / tareas / cli.py

 if __name__ == '__main__': tasks_cli() 

Esto es solo una llamada a tasks_cli() :


ch7 / task_proj_v2 / src / tareas / cli.py

 @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass 

Obviamente? No Pero espera, se pone bueno (o malo, dependiendo de tu punto de vista). Aquí está uno de los comandos de la list :


ch7 / task_proj_v2 / src / tareas / cli.py

 @tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """    .   ,      . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary)) 

Cuando se acostumbre a escribir el código Click, asegúrese de que este código no sea tan malo. No voy a explicar aquí qué y cómo funciona en esta función, ya que el desarrollo del código de línea de comandos no es el enfoque del libro; Sin embargo, aunque estoy casi absolutamente seguro de que tengo este código correcto, de todos modos, siempre hay mucho espacio para el error humano. Es por eso que un buen conjunto de pruebas automatizadas es importante para garantizar que esta función funcione correctamente.
Esta función list_tasks(owner) depende de varias otras funciones: tasks_db() , que es el administrador de contexto, y tasks.list_tasks(owner) , que es la función API.


Vamos a utilizar mock para poner funciones falsas en tasks_db() y tasks.list_tasks() . Luego podemos llamar al método list_tasks través de la interfaz de línea de comandos y asegurarnos de que llame a la función tasks.list_tasks() , que funciona correctamente y procesa correctamente el valor de retorno.
Para ahogar tasks_db() , veamos una implementación real:


Volver

Source: https://habr.com/ru/post/448798/


All Articles