Familiaridad con las pruebas en Python. Parte 1

Buen dia a todos!

De nuestra mesa a la suya ... Es decir, de nuestro curso de Desarrollador de Python, a pesar del año nuevo que se acerca rápidamente, hemos preparado una traducción interesante para usted sobre varios métodos de prueba en Python.

Esta guía es para aquellos que ya han escrito una aplicación genial de Python pero aún no han escrito para
ellos pruebas.

Las pruebas en Python son un tema extenso con muchas sutilezas, pero no es necesario complicar las cosas. En unos pocos pasos simples, puede crear pruebas simples para la aplicación, aumentando gradualmente la complejidad en función de ellas.

En esta guía, aprenderá cómo crear una prueba básica, ejecutarla y encontrar todos los errores antes de que los usuarios lo hagan. Aprenderá sobre las herramientas disponibles para escribir y ejecutar pruebas, verificar el rendimiento de la aplicación e incluso ver los problemas de seguridad.



Prueba de código

Puede probar el código de muchas maneras. En esta guía, aprenderá acerca de los métodos, desde el más simple hasta el más avanzado.

Automatizado vs. Prueba manual

Buenas noticias! Lo más probable es que ya haya realizado la prueba, pero aún no se haya dado cuenta. ¿Recuerdas cómo empezaste la aplicación y la usaste? ¿Has probado las funciones y experimentado con ellas? Este proceso se llama prueba exploratoria, y es una forma de prueba manual.

Pruebas de investigación: pruebas que se realizan sin un plan. Durante las pruebas de investigación, investigas la aplicación.

Para crear una lista completa de pruebas manuales, es suficiente hacer una lista de todas las funciones de la aplicación, varios tipos de entrada que acepta y los resultados esperados. Ahora, cada vez que cambie algo en el código, debe volver a verificar cada uno de los elementos de esta lista.

Suena sombrío, ¿verdad?

Por lo tanto, se necesitan pruebas automáticas. Pruebas automáticas: ejecución del plan de pruebas (partes de la aplicación que requieren pruebas, el orden de las pruebas y los resultados esperados) utilizando un script y no por manos humanas. Python ya tiene un conjunto de herramientas y bibliotecas para ayudarlo a crear pruebas automatizadas para su aplicación. Veamos estas herramientas y bibliotecas en nuestro tutorial.

Pruebas unitarias VS. Pruebas de integración

El mundo de las pruebas está lleno de términos, y ahora, conociendo la diferencia entre las pruebas manuales y automatizadas, profundizaremos.

¿Piensa en cómo puede probar los faros de un automóvil? Enciende los faros (llamémoslo el paso de prueba), salga del automóvil usted mismo o pídale a un amigo que verifique que los faros estén encendidos (y esta es una propuesta de prueba). La prueba de múltiples componentes se llama prueba de integración.

Piense en todas las cosas que deberían funcionar correctamente para que una tarea simple produzca el resultado correcto. Estos componentes son similares a partes de su aplicación: todas esas clases, funciones, módulos que escribió.

La principal dificultad de las pruebas de integración surge cuando la prueba de integración no da el resultado correcto. Es difícil evaluar el problema, no poder aislar la parte rota del sistema. Si los faros no están encendidos, las bombillas pueden estar rotas. ¿O tal vez la batería está baja? ¿O tal vez el problema está en el generador? ¿O incluso un choque en la computadora de la máquina?

Los autos modernos le notificarán sobre una bombilla rota. Esto se determina usando una prueba unitaria.

La prueba unitaria (prueba unitaria) es una prueba pequeña que verifica el funcionamiento correcto de un componente individual. La prueba de la unidad ayuda a aislar el desglose y arreglarlo más rápido.

Hablamos de dos tipos de pruebas:

  1. Una prueba de integración que verifica los componentes del sistema y su interacción entre ellos;
  2. Una prueba unitaria que prueba un solo componente de una aplicación.
  3. Puede crear ambas pruebas en Python. Para escribir una prueba para la función sum () incorporada, debe comparar la salida de sum () con valores conocidos.

Por ejemplo, de esta manera puede verificar que la suma de los números (1, 2, 3) es 6:

>>> assert sum([1, 2, 3]) == 6, "Should be 6" 

Los valores son correctos, por lo que no se enviará nada a REPL. Si el resultado de sum() incorrecto, se lanzará un AssertionError con el mensaje "Debería ser 6". Verifique la declaración de la declaración nuevamente, pero ahora con valores no válidos para obtener un AssertionError :

 >>> assert sum([1, 1, 1]) == 6, "Should be 6" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: Should be 6 

En REPL, verá AssertionError ya que el valor sum() no es 6.

En lugar de REPL, coloque esto en un nuevo archivo de Python llamado test_sum.py y ejecútelo nuevamente:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" if __name__ == "__main__": test_sum() print("Everything passed") 

Ahora tiene un caso de prueba escrito (caso de prueba), declaración y punto de entrada (línea de comando). Ahora esto se puede hacer en la línea de comando:

 $ python test_sum.py Everything passed 

Usted ve el resultado exitoso, "Todo pasó".

sum() en Python acepta cualquier iterable como primer argumento. Has revisado la lista. Intentemos probar la tupla. Cree un nuevo archivo llamado test_sum_2.py con el siguiente código:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6" if __name__ == "__main__": test_sum() test_sum_tuple() print("Everything passed") 

test_sum_2.py , el script test_sum_2.py error, ya que s um() (1, 2, 2) debe ser 5, no 6. Como resultado, el script muestra un mensaje de error, una línea de código y un rastreo:

 $ python test_sum_2.py Traceback (most recent call last): File "test_sum_2.py", line 9, in <module> test_sum_tuple() File "test_sum_2.py", line 5, in test_sum_tuple assert sum((1, 2, 2)) == 6, "Should be 6" AssertionError: Should be 6 

Puede ver cómo un error en el código causa un error en la consola con información sobre dónde ocurrió y cuál fue el resultado esperado.

Tales pruebas son adecuadas para una verificación simple, pero ¿qué pasa si hay más errores que en uno? Los corredores de prueba vienen al rescate. Test Executor es una aplicación especial diseñada para realizar pruebas, verificar datos de salida y proporcionar herramientas para depurar y diagnosticar pruebas y aplicaciones.

Elegir un ejecutor de prueba

Hay muchos corredores de prueba disponibles para Python. Por ejemplo, unittest está integrado en la biblioteca estándar de Python. En esta guía, utilizaremos casos de prueba y ejecutores de pruebas unitarias. Los principios operativos de Unittest se adaptan fácilmente a otros marcos. Enumeramos los ejecutores de prueba más populares:

  • prueba de unidad;
  • nariz o nariz2;
  • Pytest.

Es importante elegir un contratista de prueba que cumpla con sus requisitos y experiencia.

prueba de unidad

unittest se ha integrado en la biblioteca estándar de Python desde la versión 2.1. Probablemente lo encontrará en aplicaciones comerciales de Python y proyectos de código abierto.
Unittest tiene un marco de prueba y un corredor de prueba. Al escribir y ejecutar pruebas, debe seguir algunos requisitos importantes.

unittest requiere:

  • Poner pruebas en clases como métodos;
  • Use métodos de aprobación especiales. La clase TestCase en lugar de la expresión de aserción incorporada habitual.


Para convertir un ejemplo escrito anteriormente en un caso de prueba de prueba de unidad, debe:

  1. Importar unittest desde la biblioteca estándar;
  2. Cree una clase llamada TestSum que heredará la clase TestCase ;
  3. Convierta las funciones de prueba en métodos agregando self como primer argumento;
  4. Modifique las declaraciones agregando el uso del método self.assertEqual() en la clase TestCase ;
  5. Cambie el punto de entrada en la línea de comando para llamar a unittest.main() .

Siguiendo estos pasos, cree un nuevo archivo test_sum_unittest.py con este código:

 import unittest class TestSum(unittest.TestCase): def test_sum(self): self.assertEqual(sum([1, 2, 3]), 6, "Should be 6") def test_sum_tuple(self): self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") if __name__ == '__main__': unittest.main() 

Al hacer esto en la línea de comando, obtendrá una finalización exitosa (indicada por.) Y una incorrecta (indicada por F):

 $ python test_sum_unittest.py .F ====================================================================== FAIL: test_sum_tuple (__main__.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") AssertionError: Should be 6 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

Por lo tanto, realizó dos pruebas con el corredor de prueba unittest.

Nota: Si está escribiendo casos de prueba para Python 2 y 3, tenga cuidado. En las versiones de Python 2.7 y posteriores, unittest se llama unittest 2. Cuando importe desde unittest, obtendrá diferentes versiones con diferentes funciones en Python 2 y Python 3.

Para obtener más información sobre unittest, lea la documentación de unittest .

nariz

Con el tiempo, después de escribir cientos o incluso miles de pruebas para una aplicación, se vuelve cada vez más difícil de entender y usar los datos de salida de la prueba unitaria.

nose es compatible con todas las pruebas escritas con unittest framework y puede reemplazar a su ejecutor de pruebas. El desarrollo de nose, como una aplicación de código abierto, comenzó a disminuir, y se creó nose2. Si está comenzando desde cero, se recomienda que use nose2.

Para comenzar con nose2 necesita instalarlo desde PyPl y ejecutarlo en la línea de comando. nose2 intentará encontrar todos los scripts de test*.py con test*.py en el nombre y todos los casos de prueba heredados de unittest.TestCase en su directorio actual:

 $ pip install nose2 $ python -m nose2 .F ====================================================================== FAIL: test_sum_tuple (__main__.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_sum_unittest.py", line 9, in test_sum_tuple self.assertEqual(sum((1, 2, 2)), 6, "Should be 6") AssertionError: Should be 6 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

Así es como test_sum_unittest.py la prueba creada en test_sum_unittest.py desde el corredor de prueba nose2. nose2 proporciona muchos indicadores de línea de comando para filtrar pruebas ejecutables. Para obtener más información, consulte la documentación de Nose 2 .

pytest

pytest admite casos de prueba unittest. Pero la verdadera ventaja de pytest son sus casos de prueba. Los casos de prueba de pytest son una serie de funciones en un archivo de Python con test_ al comienzo del nombre.

Hay otras características útiles en él:

  • Soporte para expresiones de aserción incorporadas en lugar de usar métodos especiales self.assert * ();
  • Soporte para filtrar casos de prueba;
  • La capacidad de reiniciar desde la última prueba fallida;
  • Un ecosistema de cientos de complementos que amplían la funcionalidad.

Un ejemplo de caso de prueba TestSum para pytest se verá así:

 def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6" 

Se deshizo de TestCase, utilizando clases y puntos de entrada de línea de comando.
Se puede encontrar más información en el sitio de documentación de Pytest .

Escribir la primera prueba

Combina todo lo que ya aprendimos y, en lugar de la función incorporada sum() , probamos una implementación simple con los mismos requisitos.

Cree una nueva carpeta para el proyecto, dentro de la cual cree una nueva carpeta llamada my_sum. Dentro de my_sum, cree un archivo vacío llamado _init_.py . La presencia de este archivo significa que la carpeta my_sum se puede importar como un módulo desde el directorio principal.

La estructura de la carpeta se verá así:

project/

└── my_sum/
└── __init__.py


Abra my_sum/__init__.py y cree una nueva función llamada sum() , que toma entradas my_sum/__init__.py (list, tuple, set) y agrega los valores.

 def sum(arg): total = 0 for val in arg: total += val return total 

Este ejemplo crea una variable llamada total , itera sobre todos los valores en arg y agrega al total . Luego, al finalizar la iteración, se devuelve el resultado.

Dónde escribir una prueba

Puede comenzar a escribir una prueba creando un archivo test.py que contendrá su primer caso de prueba. Para las pruebas, el archivo debe poder importar su aplicación, por lo tanto, coloque test.py en la carpeta que se encuentra sobre el paquete. El árbol de directorios se verá así:

project/

├── my_sum/
│ └── __init__.py
|
└── test.py


Notará que a medida que agrega nuevas pruebas, su archivo se vuelve más engorroso y difícil de mantener, por lo que recomendamos crear las tests/ carpetas y dividir las pruebas en varios archivos. Asegúrese de que los nombres de todos los archivos comiencen con test_ , para que los test_ prueba entiendan que los archivos de Python contienen pruebas que deben ejecutarse. En proyectos grandes, las pruebas se dividen en varios directorios, según su propósito o uso.

Nota: ¿Y cuál es su aplicación es un script único?
Puede importar cualquier atributo de script: clases, funciones o variables, utilizando la función incorporada __import__() . En lugar de from my_sum import sum escriba lo siguiente:

 target = __import__("my_sum.py") sum = target.sum 

Al usar __import__() no tiene que convertir la carpeta del proyecto en un paquete, y puede especificar el nombre del archivo. Esto es útil si el nombre del archivo entra en conflicto con los nombres de las bibliotecas de paquetes estándar. Por ejemplo, si math.py entra en conflicto con el módulo matemático.

Cómo estructurar una prueba simple

Antes de escribir pruebas, debe resolver algunas preguntas:

  1. ¿Qué quieres probar?
  2. ¿Estás escribiendo una prueba unitaria o una prueba de integración?

Actualmente estás probando sum() . Puede probar diferentes comportamientos para ello, por ejemplo:

  • ¿Es posible resumir una lista de enteros?
  • ¿Es posible resumir una tupla o un conjunto?
  • ¿Puedo resumir una lista de números de coma flotante?
  • ¿Qué sucede si le das un valor incorrecto a la entrada: un solo entero o una cadena?
  • ¿Qué sucede si uno de los valores es negativo?

La forma más fácil de probar es una lista de enteros. Cree un archivo test.py con el siguiente código:

 import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) if __name__ == '__main__': unittest.main() 

El código en este ejemplo:

  • Importa sum() del paquete my_sum() que creó;
  • Define una nueva clase de caso de prueba llamada TestSum que hereda unittest.TestCase ;
  • Define un .test_list_int() prueba .test_list_int() para probar una lista entera. El método .test_list_int() hará lo siguiente
:
  1. Declara una variable de data con una lista de valores (1, 2, 3) ;
  2. my_sum.sum(data) valor my_sum.sum(data) result variable;
  3. Determina que el valor del resultado es 6 utilizando el método .assertEqual() en la clase unittest.TestCase .

  • Define un punto de entrada de línea de comando que inicia el corredor de prueba unittest .main() .

Si no sabe qué es self o cómo se define .assertEqual() , puede actualizar sus conocimientos de programación orientada a objetos con Python 3 Object-Oriented Programming .

Cómo escribir declaraciones

El último paso para escribir una prueba es verificar que la salida coincida con los valores conocidos. Esto se llama una afirmación. Existen varias pautas generales para escribir declaraciones:

  • Verifique que las pruebas sean repetibles y ejecútelas varias veces para asegurarse de que den los mismos resultados cada vez;
  • Verifique y confirme los resultados que se aplican a su entrada; verifique que el resultado sea realmente la suma de los valores en el ejemplo sum() .

Unittest tiene muchos métodos para confirmar los valores, los tipos y la existencia de variables. Estos son algunos de los métodos más utilizados:

MétodoEquivalente
.assertEqual (a, b)a == b
.assertTrue (x)bool (x) es verdadero
.assertFalse (x)bool (x) es falso
.assertIs (a, b)a es b
.assertIsNone (x)x es Ninguno
.assertIn (a, b)a en b
.assertIsInstance (a, b)isinstance (a, b)


.assertIs() , .assertIsNone() , .assertIn() y .assertIsInstance() tienen métodos opuestos llamados .assertIsNot() y así sucesivamente.

Efectos secundarios

Escribir pruebas es más difícil que solo mirar el valor de retorno de una función. A menudo, la ejecución del código cambia otras partes del entorno: atributos de clase, archivos del sistema de archivos, valores en la base de datos. Esta es una parte importante de las pruebas llamadas efectos secundarios. Decida si está probando un efecto secundario antes de incluirlo en su lista de reclamos.

Si encuentra que hay muchos efectos secundarios en el bloque de código que desea probar, está violando el Principio de responsabilidad exclusiva . La violación del principio de responsabilidad exclusiva significa que un código hace demasiadas cosas y requiere una refactorización. Seguir el principio de responsabilidad exclusiva es una excelente manera de diseñar código para el cual no será difícil escribir pruebas unitarias simples y repetibles y, en última instancia, crear aplicaciones confiables.

Lanzamiento de la primera prueba

Creó la primera prueba y ahora debe intentar ejecutarla. Está claro que se aprobará, pero antes de crear pruebas más complejas, debe asegurarse de que incluso dichas pruebas sean exitosas.

Ejecución de ejecutores de prueba

Test Executor: una aplicación de Python que ejecuta código de prueba, valida aserciones y muestra los resultados de la prueba en la consola. Al final de test.py agregue este pequeño fragmento de código:

 if __name__ == '__main__': unittest.main() 

Este es el punto de entrada de la línea de comando. Si ejecuta este script ejecutando python test.py en la línea de comando, llamará unittest.main() . Esto inicia el unittest.TestCase prueba unittest.TestCase detectar todas las clases en este archivo que heredan de unittest.TestCase .

Esta es una de las muchas formas de ejecutar el corredor de prueba unittest. Si tiene un único archivo de prueba llamado test.py , llamar a python test.py es una excelente manera de comenzar.

Otra forma es usar la línea de comando unittest. Probemos

 $ python -m unittest test 

Esto ejecutará el mismo módulo de prueba (llamado test ) a través de la línea de comando. Puede agregar parámetros adicionales para cambiar la salida. Uno de ellos es -v para verbose. Probemos lo siguiente:

 $ python -m unittest -v test test_list_int (test.TestSum) ... ok ---------------------------------------------------------------------- Ran 1 tests in 0.000s 

Ejecutamos una prueba desde test.py y enviamos los resultados a la consola. El modo detallado enumeró los nombres de las pruebas realizadas y los resultados de cada una de ellas.

En lugar de proporcionar el nombre del módulo que contiene las pruebas, puede solicitar el descubrimiento automático utilizando lo siguiente:

 $ python -m unittest discover 

Este comando buscará en el directorio actual archivos con test*.py en el nombre para probarlos.

Si tiene varios archivos de prueba y sigue el patrón de nomenclatura de test*.py , puede pasar el nombre del directorio utilizando la bandera -s y el nombre de la carpeta.

 $ python -m unittest discover -s tests 

unittest ejecutará todas las pruebas en un solo plan de prueba y producirá los resultados.
Finalmente, si su código fuente no está en el directorio raíz, sino en un subdirectorio, por ejemplo, en una carpeta llamada src /, puede decirle a unittest dónde ejecutar las pruebas usando el indicador -t para importar correctamente los módulos:

 $ python -m unittest discover -s tests -t src 

unittest encontrará todos test*.py archivos test*.py en el directorio src/ dentro de las tests y luego los ejecutará.

Comprender los resultados de la prueba

Este fue un ejemplo muy simple en el que todo salió bien, así que intentemos comprender el resultado de una prueba fallida.

sum() debe aceptar otras listas de tipo numérico, por ejemplo fracciones.

Al comienzo del código en test.py agregue una expresión para importar el tipo de fractions módulo de fractions de la biblioteca estándar.

 from fractions import Fraction 

Ahora agregue una prueba con una declaración, esperando un valor incorrecto. En nuestro caso, esperamos que la suma de ¼, ¼ y ⅖ sea igual a 1:

 import unittest from my_sum import sum class TestSum(unittest.TestCase): def test_list_int(self): """ Test that it can sum a list of integers """ data = [1, 2, 3] result = sum(data) self.assertEqual(result, 6) def test_list_fraction(self): """ Test that it can sum a list of fractions """ data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)] result = sum(data) self.assertEqual(result, 1) if __name__ == '__main__': unittest.main() 

Si vuelve a ejecutar las pruebas con la prueba de prueba de unidad python -m, obtenga lo siguiente:

 $ python -m unittest test F. ====================================================================== FAIL: test_list_fraction (test.TestSum) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 21, in test_list_fraction self.assertEqual(result, 1) AssertionError: Fraction(9, 10) != 1 ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1) 

En esta salida, verá lo siguiente:

  • La primera línea muestra los resultados de todas las pruebas: una falló (F), una pasó (.);
  • FAIL muestra algunos detalles de la prueba fallida:

  1. El nombre del método de prueba ( test_list_fraction );
  2. Módulo de test ( test ) y caso de prueba ( TestSum );
  3. Rastreo de cadenas con un error;
  4. Detalles de la declaración con el resultado esperado (1) y el resultado real (Fracción (9, 10))

Recuerde, puede agregar información adicional a la salida de prueba usando el indicador -v al python -m unittest .

Ejecución de pruebas desde PyCharm

Si está utilizando PyCharm IDE, puede ejecutar unittest o pytest siguiendo estos pasos:

  1. En la ventana de la herramienta Proyecto, seleccione el directorio de pruebas.
  2. En el menú contextual, seleccione el comando de ejecución unittest. Por ejemplo, 'Pruebas unitarias en mis pruebas ...'.

Esto ejecutará unittest en la ventana de prueba y devolverá los resultados en PyCharm:



Hay más información disponible en el sitio web de PyCharm .

Ejecución de pruebas desde el código de Visual Studio

Si usa el IDE de Microsoft Visual Studio Code, la compatibilidad con unittest, nose y pytest ya está integrada en el complemento de Python.

Si lo tiene instalado, puede configurar la configuración de prueba abriendo Command Palette con Ctrl + Shift + P y escribiendo "Prueba de Python". Verá una lista de opciones:



Seleccione Debug All Unit Tests, después de lo cual VSCode enviará una solicitud para configurar el marco de prueba. Haga clic en el engranaje para seleccionar el corredor de prueba (unittest) y el directorio de inicio (.).

Al finalizar la configuración, verá el estado de las pruebas en la parte inferior de la pantalla y podrá acceder rápidamente a los registros de prueba y reiniciar las pruebas haciendo clic en los iconos:



Vemos que las pruebas se están realizando, pero algunas de ellas han fallado.

El fin

En la siguiente parte del artículo, examinaremos las pruebas para marcos como Django y Flask.

Estamos esperando sus preguntas y comentarios aquí y, como siempre, puede ir a Stanislav en un día abierto .

Segunda parte

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


All Articles