Saltar al contenido principal
InicioTutorialesPython

Cómo utilizar Pytest para pruebas unitarias

Explore qué es Pytest y para qué se utiliza mientras lo compara con otros métodos de prueba de software.
may 2024  · 17 min leer

Las pruebas son un tema de gran importancia en el desarrollo de software. Antes de que un producto de software llegue a manos de un usuario final, es probable que haya pasado por varias pruebas, como las de integración, las de sistemas y las de aceptación. La idea que subyace a estas pruebas tan enérgicas es garantizar que el comportamiento de la aplicación funciona como se espera desde el punto de vista de los usuarios finales. Este enfoque de las pruebas se conoce como desarrollo basado en el comportamiento (BDD).   

Más recientemente, el interés por el desarrollo basado en pruebas (TDD) ha crecido significativamente entre los desarrolladores. Sumergirse en sus profundidades puede ser una tarea compleja para este artículo, pero la idea general es que el proceso tradicional de desarrollo y pruebas se invierte: primero se escriben las pruebas unitarias y luego se implementan los cambios en el código hasta que las pruebas pasan. 

En este artículo, nos centraremos en las pruebas unitarias y, en concreto, en cómo realizarlas utilizando un popular framework de pruebas de Python llamado Pytest. 

¿Qué son las pruebas unitarias?

Las pruebas unitarias son una forma de pruebas automatizadas, lo que significa simplemente que el plan de pruebas es ejecutado por un script en lugar de manualmente por un humano. Constituyen el primer nivel de las pruebas de software y suelen escribirse en forma de funciones que validan el comportamiento de diversas funcionalidades dentro de un programa de software. 

Los niveles de las pruebas de software

Los niveles de las pruebas de software

La idea de estas pruebas es permitir a los desarrolladores aislar la unidad de código más pequeña que tenga sentido lógico y comprobar que se comporta como se espera. En otras palabras, las pruebas unitarias validan que cada uno de los componentes de un programa de software funciona tal y como pretendían los desarrolladores. 

Lo ideal es que estas pruebas sean bastante pequeñas; cuanto más pequeñas sean, mejor. Una de las razones para construir pruebas más pequeñas es que la prueba será más eficiente, ya que probar unidades más pequeñas permitirá que el código de prueba se ejecute mucho más rápido. Otra razón para probar componentes más pequeños es que permite comprender mejor cómo se comporta el código granular cuando se fusiona. 

¿Por qué necesitamos pruebas unitarias? 

La justificación global de por qué es esencial realizar pruebas unitarias es que los desarrolladores deben asegurarse de que el código que escriben cumple las normas de calidad antes de permitir que entre en un entorno de producción. Sin embargo, hay otros factores que contribuyen a la necesidad de las pruebas unitarias. Profundicemos en algunas de esas razones.

Preserva los recursos 

La realización de pruebas unitarias ayuda a los desarrolladores a detectar errores en el código durante la fase de construcción del software, evitando que se produzcan en fases más profundas del ciclo de vida del desarrollo. De este modo se preservan los recursos, ya que los desarrolladores no tendrían que pagar el coste de corregir errores más adelante en el desarrollo; también significa que los usuarios finales tienen menos probabilidades de tener que lidiar con código defectuoso.  

Documentación adicional 

Otra gran justificación para realizar pruebas unitarias es que sirven como una capa adicional de documentación viva para su producto de software. Los desarrolladores pueden consultar las pruebas unitarias para tener una visión global del sistema, ya que detallan cómo deben comportarse los componentes de menor importancia. 

Aumento de la confianza

Es extremadamente sencillo cometer errores sutiles en tu código mientras escribes alguna funcionalidad. Sin embargo, la mayoría de los desarrolladores estarán de acuerdo en que es mucho mejor identificar los puntos de ruptura dentro de una base de código antes de ponerla en un entorno de producción: las pruebas unitarias ofrecen a los desarrolladores esta oportunidad. 

Es justo decir que "el código cubierto con pruebas unitarias puede considerarse más fiable que el que no lo está". Las futuras roturas en el código pueden descubrirse mucho más rápido que el código sin cobertura de pruebas, lo que ahorra tiempo y dinero. Los desarrolladores también se benefician de la documentación adicional, que les permite entender el código más rápidamente, y de la confianza añadida de saber que si cometen un error en el código, será detectado por las pruebas unitarias y no por el usuario final. 

Marcos de pruebas en Python

La popularidad de Python ha crecido enormemente a lo largo de los años. Como parte del crecimiento de Python, también ha aumentado el número de marcos de pruebas, lo que se traduce en una gran cantidad de herramientas disponibles para ayudarle a probar su código Python. Entrar en el meollo de cada herramienta está más allá del alcance de este artículo, pero vamos a tocar algunos de los frameworks de pruebas de Python más comunes disponibles. 

unittest

Unittest es un framework integrado en Python para pruebas unitarias. Se inspiró en un marco de pruebas unitarias llamado JUnit del lenguaje de programación Java. Como viene de fábrica con el lenguaje Python, no hay que instalar ningún módulo adicional, y la mayoría de los desarrolladores lo utilizan para empezar a aprender a realizar pruebas.  

Pytest

Pytest es posiblemente el marco de pruebas de Python más utilizado, lo que significa que cuenta con una gran comunidad que le ayudará cuando se quede atascado. Se trata de un marco de trabajo de código abierto que permite a los desarrolladores escribir conjuntos de pruebas sencillos y compactos a la vez que admite pruebas unitarias, pruebas funcionales y pruebas de API.

doctest

El marco doctest fusiona dos componentes básicos de la ingeniería de software: la documentación y las pruebas. Esta funcionalidad garantiza que todos los programas de software se documentan y prueban exhaustivamente para asegurar que funcionan como deberían. doctest viene con la biblioteca estándar de Python y es bastante sencillo de aprender. 

nose2

Nose2, el sucesor del regimiento nose, es esencialmente unittest con plugins. La gente a menudo se refiere a nose2 como "pruebas unitarias extendidas" o "pruebas unitarias con un plugin" debido a sus estrechos vínculos con el marco de pruebas unitarias incorporado en Python. Dado que es prácticamente una extensión del framework unittest, nose2 es increíblemente fácil de adoptar para aquellos familiarizados con unittest.

Testifique

Testify, un framework de Python para pruebas unitarias, de integración y de sistemas, es popularmente conocido como el framework que se diseñó para sustituir a unittest y nose. El framework está repleto de extensos plugins y tiene una curva de aprendizaje bastante suave si ya estás familiarizado con unittest.

Hipótesis

Hypothesis permite a los desarrolladores crear pruebas unitarias más sencillas de escribir y potentes cuando se ejecutan. Dado que el marco se ha creado para apoyar proyectos de ciencia de datos, ayuda a encontrar casos extremos que no son tan evidentes mientras creas tus pruebas generando ejemplos de entradas que se alinean con propiedades específicas que defines.

Para nuestro tutorial, utilizaremos pytest. Eche un vistazo a la siguiente sección para ver por qué puede optar por Pytest en lugar de los otros que hemos enumerado. 

¿Por qué utilizar Pytest? 

Más allá de su vasta comunidad de apoyo, pytest tiene varios factores que la convierten en una de las mejores herramientas para llevar a cabo su suite de pruebas automatizadas en Python. La filosofía y las características de Pytest están pensadas para hacer de las pruebas de software una experiencia mucho mejor para los desarrolladores. Una forma en que los creadores de Pytest lograron este objetivo es reduciendo significativamente la cantidad de código necesario para realizar tareas comunes y haciendo posible la realización de tareas avanzadas con extensos comandos y plug-ins. 

Otras razones para utilizar Pytest son las siguientes: 

Fácil de aprender 

Pytest es extremadamente fácil de aprender: si entiendes cómo funciona la palabra clave assert de Python, ya estás en el buen camino para dominar el framework. Las pruebas que utilizan pytest son funciones de Python con "test_" antepuesto o "_test" añadido al nombre de la función, aunque puede utilizar una clase para agrupar varias pruebas. En general, la curva de aprendizaje de pytest es mucho menor que la de unittest, ya que no es necesario aprender nuevas construcciones. 

Prueba de filtrado

Es posible que no desee ejecutar todas sus pruebas en cada ejecución, lo que puede ocurrir a medida que crece su conjunto de pruebas. A veces, es posible que desee aislar algunas pruebas en una nueva característica para obtener información rápida, mientras que usted está desarrollando, a continuación, ejecute el conjunto completo una vez que esté seguro de que todo está funcionando según lo previsto. Pytest tiene tres formas de aislar las pruebas: 1) filtrado basado en nombres, que indica a pytest que sólo ejecute las pruebas cuyos nombres coincidan con el patrón proporcionado 2) alcance de directorio, que es una configuración predeterminada que indica a pytest que sólo ejecute las pruebas que se encuentren en el directorio actual o debajo de él y 3) categorización de pruebas, que permite definir categorías para las pruebas que pytest debe incluir o excluir. 

Parametrización

Pytest tiene un decorador incorporado llamado parametrize que permite la parametrización de argumentos para una función de prueba. Así, si las funciones que está probando procesan datos o realizan una transformación genérica, no es necesario que escriba varias pruebas similares. Más adelante hablaremos de la parametrización

Nos detendremos aquí, pero la lista de por qué pytest es una gran opción de herramientas para su conjunto de pruebas automatizadas continúa. 

Pytest frente a unittest

A pesar de todas las razones que hemos cubierto anteriormente, uno todavía puede discutir la idea de utilizar pytest por el simple hecho de que es un framework de terceros - "¿qué sentido tiene instalar un framework si ya hay uno incorporado?" Es un buen argumento, pero para cubrirnos las espaldas en esa disputa, le daremos algunas cosas a tener en cuenta. 

Nota: Si ya estás convencido de pytest, pasa a la siguiente sección, donde veremos cómo utilizar el framework. 

Menos repeticiones

Unittest requiere que los desarrolladores creen clases derivadas del módulo TestCase y luego definan los casos de prueba como métodos en la clase. 

"""
An example test case with unittest.
See: https://docs.python.org/3/library/unittest.html
"""
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

Pytest, por otro lado, sólo requiere que defina una función con "test_" antepuesto y que utilice las condiciones assert dentro de ellas. 

"""
An example test case with pytest.
See: https://docs.pytest.org/en/6.2.x/index.html
"""
# content of test_sample.py
def inc(x):
    return x + 1


def test_answer():
    assert inc(3) == 5

Observe la diferencia en la cantidad de código requerido; unittest tiene una cantidad significativa de código boilerplate requerido, que sirve como requisito mínimo para cualquier prueba que desee realizar. Esto significa que es muy probable que acabes escribiendo el mismo código varias veces. Pytest, por otro lado, tiene ricas características incorporadas que simplifican este flujo de trabajo mediante la reducción de la cantidad de código necesario para escribir casos de prueba. 

Salida

Los resultados que ofrece cada marco son muy diferentes. He aquí un ejemplo de ejecución de pytest: 

"""
See: https://docs.pytest.org/en/6.2.x/index.html
"""
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert inc(3) == 5
E       assert 4 == 5
E        +  where 4 = inc(3)

test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================

El caso de prueba anterior ha fallado, pero fíjate en lo detallado que es el desglose del fallo. Esto facilita a los desarrolladores la identificación de los errores en su código, lo que resulta muy útil a la hora de depurar. Como añadido, también hay un informe de estado general del conjunto de pruebas, que nos indica el número de pruebas que han fallado y el tiempo que han tardado. 

Veamos un ejemplo de caso de prueba fallido con unittest.

import unittest

def square(n):
    return n*n

def cube(n):
    return n*n*n

class TestCase(unittest.TestCase):
    def test_square(self):
        self.asserEquals(square(4), 16)

    def test_cube(self):
        self.assertEquals(cube(4), 16)

Cuando ejecutamos el script, obtenemos la siguiente salida: 

---------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (errors=1)

La salida anterior nos dice que dos pruebas se ejecutaron en 0,001s y una falló, pero no mucho más. En última instancia, pytest proporciona mucha más información, lo que resulta muy útil a la hora de depurar. 

En definitiva, tanto pytest como unittest son excelentes herramientas para realizar pruebas automatizadas en Python. Varios desarrolladores Python pueden inclinarse más hacia pytest sobre sus contrapartes debido a su compacidad y eficiencia. También es muy fácil de adoptar y hay varias funciones que puedes utilizar para crear un conjunto de pruebas eficaz. 

Pasemos ahora a la parte principal de este artículo. Hemos discutido qué son las pruebas unitarias y por qué pytest es una gran herramienta para pruebas automatizadas en Python. Veamos ahora cómo utilizar la herramienta. 

Tutorial de Pytest

Veamos cómo funciona este marco de pruebas de Python del que hemos estado hablando. 

El primer paso es instalar el paquete, lo que puede hacerse con un simple comando pip. 

Nota: Los creadores de pytest recomiendan que utilices venv para el desarrollo y pip para instalar tu aplicación, dependencias y el propio pytest. 

pip install -U pytest

A continuación, compruebe que el framework se ha instalado mediante el siguiente comando: 

>>>> pytest --version

pytest 7.1.2

Todo está instalado. Ya está listo para empezar a realizar algunas pruebas. 

Creación de una prueba sencilla

Crear una prueba es sencillo con Pytest. Para demostrar esta funcionalidad, hemos creado un script llamado calcualte_age.py. Este script sólo tiene una función, get_age, que se encarga de calcular la edad de un usuario, dada su fecha de nacimiento. 

import datetime

def get_age(yyyy:int, mm:int, dd:int) -> int:
    dob = datetime.date(yyyy, mm, dd)
    today = datetime.date.today()
    age = round((today - dob).days / 365.25)
    return age

Pytest ejecutará todos los archivos python que tengan el nombre test_ antepuesto o _test añadido al nombre del script. Para ser más específicos, pytest sigue las siguientes convenciones para el descubrimiento de pruebas [fuente: documentación]: 

  • Si no se especifican argumentos, la colección de pytest comenzará en testpaths si están configurados: testpaths es una lista de directorios en los que pytest buscará cuando no se proporcionen directorios, archivos o ids de prueba específicos. 
  • Pytest buscará en directorios a menos que le hayas dicho que no lo haga configurando norecursedirs; Buscará ficheros que empiecen por test_*.py o terminen en *_test.py
  • En esos archivos, pytest recogería los elementos de prueba en el siguiente orden:
    • Funciones o métodos de prueba prefijados fuera de clase
    • Funciones o métodos de prueba prefijados dentro de clases de prueba prefijadas que no tienen un método __init__. 

No hemos especificado ningún argumento, pero hemos creado otro script en el mismo directorio llamado prueba_calcular_edad.py: así, cuando se recursen los directorios se descubrirá la prueba.  En este script, tenemos una única prueba, test_get_age, para validar que nuestra función funciona correctamente. 

Nota: Usted puede decidir poner sus pruebas en un directorio adicional fuera de su aplicación, lo cual es una buena idea si tiene varias pruebas funcionales o si desea mantener el código de pruebas y el código de la aplicación separados por alguna otra razón. 

from calculate_age import get_age

def test_get_age():
    # Given.
    yyyy, mm, dd = map(int, "1996/07/11".split(""))   
    # When.
    age = get_age(yyyy, mm, dd)
    # Then.
    assert age == 26 

Para ejecutar la prueba, ejecute el siguiente comando desde el símbolo del sistema: 

py -m pytest 

La salida de una ejecución exitosa de pytest

La salida de una ejecución exitosa de pytest.

Y eso es todo. 

Pero, ¿y si necesitamos proporcionar algunos datos para que nuestras pruebas pasen? Saluda a los accesorios de pytest. 

Fixtures de Pytest 

La inestimable función de fixtures de Pytest permite a los desarrolladores introducir datos en las pruebas. Son esencialmente funciones que se ejecutan antes de cada función de prueba para gestionar el estado de nuestras pruebas. Por ejemplo, digamos que tenemos varias pruebas que hacen uso de los mismos datos, entonces podemos utilizar un fixture para extraer los datos repetidos utilizando una única función. 

import pytest
from sklearn.model_selection import train_test_split
from fraud_detection_model.config.core import config
from fraud_detection_model.processing.data_manager import (

load_datasets, 

load_interim_data

) 


@pytest.fixture(scope="session")
def pipeline_inputs():
    # import the training dataset
    dataset = load_datasets(transaction=config.app_config.train_transaction, identity=config.app_config.train_identity, )
    # divide train and test
    X_train, X_test, y_train, y_test = train_test_split(

dataset[config.model_config.all_features], dataset[config.model_config.target],
test_size=config.model_config.test_size,
stratify=dataset[config.model_config.target],
random_state=config.model_config.random_state)
    return X_train, X_test, y_train, y_test

En el código anterior, hemos creado un fixture utilizando el decorador pytest.fixture. Observe que el ámbito se establece en "sesión" para informar a pytest que queremos que el fixture sea destruido al final de la sesión de prueba.

Nota: nuestros fixtures se almacenan en conftest.py.

El código utiliza una función que hemos importado de otro módulo para cargar dos archivos csv en memoria y fusionarlos en un único conjunto de datos. A continuación, el conjunto de datos se divide en conjuntos de entrenamiento y de prueba y se devuelve desde la función. 

Para utilizar este fixture en nuestras pruebas, debemos pasarlo como parámetro a nuestra función de prueba. En este ejemplo, utilice nuestro fixture en nuestro script de prueba test_pipeline.py de la siguiente manera: 

import pandas as pd
from pandas.api.types import is_categorical_dtype, is_object_dtype
from fraud_detection_model import pipeline
from fraud_detection_model.config.core import configfrom fraud_detection_model.processing.validation import validate_inputs 


def test_pipeline_most_frequent_imputer(pipeline_inputs):
    # Given
    X_train, _, _, _ = pipeline_inputs assert all( x in X_train.loc[:, X_train.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
    # When
    X_transformed = pipeline.fraud_detection_pipe[:1].fit_transform(X_train[:50])
    # Then
    assert all( x not in X_transformed.loc[:, X_transformed.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
def test_pipeline_aggregate_categorical(pipeline_inputs):
    # Given
    X_train, _, _, _ = pipeline_inputs  

    assert X_train["R_emaildomain"].nunique() == 60
    # When
    X_transformed = pipeline.fraud_detection_pipe[:2].fit_transform(X_train[:50])
    # Then
    assert X_transformed["R_emaildomain"].nunique() == 2

No te preocupes demasiado por lo que hace el código. Lo más importante que queremos destacar es cómo hemos reducido significativamente la necesidad de escribir código redundante porque hemos creado un fixture que pasamos como parámetro para extraer datos en nuestras pruebas. 

Sin embargo, hay situaciones en las que las luminarias pueden ser excesivas. Por ejemplo, si los datos que se introducen en sus pruebas deben procesarse de nuevo en cada caso de prueba, esto equivale prácticamente a ensuciar su código con varios objetos planos. Dicho todo esto, es probable que los fixtures desempeñen un papel fundamental en el conjunto de pruebas, pero discernir cuándo utilizarlos o evitarlos requerirá práctica y mucha reflexión.  

Parametrizar Pytest

Los dispositivos son ideales cuando se realizan varias pruebas con las mismas entradas. ¿Y si quieres probar una única función con ligeras variaciones en las entradas? Una solución es escribir varias pruebas diferentes con varios casos. 

def test_eval_addition():
    assert eval("2 + 2") == 4

def test_eval_subtraction():
    assert eval("2 - 2") == 0

def test_eval_multiplication():
    assert eval("2 * 2") == 4

def test_eval_division():
    assert eval("2 / 2") == 1.0

Aunque esta solución funciona, no es la más eficiente: para empezar, hay mucho código repetitivo. Una solución mejor es utilizar el decorador pytest.mark.parametrize() para habilitar la parametrización de argumentos para una función de prueba. Esto nos permitirá definir una única definición de prueba, y entonces pytest probará los distintos parámetros que especifiquemos por nosotros. 

Así es como reescribiríamos el código anterior si utilizáramos la parametrización: 

import pytest

@pytest.mark.parametrize("test_input, expected_output", [("2+2", 4), ("2-2", 0), ("2*2", 4), ("2/2", 1.0)])
def test_eval(test_input, expected_output):
    assert eval(test_input) == expected_output

El decorador @parametrize define cuatro entradas de prueba diferentes y valores esperados para que la función test_eval los ejecute - esto significa que la función se ejecutará cuatro veces usando cada uno por turno. 

Salida de pytest para la prueba de parametrización

La salida de pytest para la prueba parametrize

En este artículo tratamos los siguientes temas: 

  • qué son las pruebas unitarias
  • por qué necesitamos pruebas unitarias
  • diferentes marcos de pruebas en python
  • Por qué pytest es tan útil 
  • cómo utilizar pytest y dos de sus características clave (fixtures y parametrización)

Ahora ya sabes lo suficiente como para empezar a escribir tu propio test utilizando el framework pytest. Le recomendamos encarecidamente que lo haga, para que todo lo que ha aprendido en este artículo le sirva. También deberías consultar nuestro curso sobre pruebas unitarias para la ciencia de datos en python para obtener una explicación más detallada con ejemplos. 

Temas

Cursos para Python

Certificación disponible

Course

Pruebas unitarias para la ciencia de datos en Python

4 hr
30.4K
Aprenda a escribir pruebas unitarias para sus proyectos de Ciencia de Datos en Python usando pytest.
See DetailsRight Arrow
Start Course
Ver másRight Arrow
Relacionado

tutorial

Cómo recortar una cadena en Python: Tres métodos diferentes

Aprenda los fundamentos del recorte de caracteres iniciales y finales de una cadena en Python.
Adel Nehme's photo

Adel Nehme

5 min

tutorial

Pandas Profiling (ydata-profiling) en Python: Guía para principiantes

Aprenda a utilizar la biblioteca ydata-profiling en Python para generar informes detallados de conjuntos de datos con muchas características.
Satyam Tripathi's photo

Satyam Tripathi

9 min

tutorial

Guía completa de listas vacías en Python

Aprenda las principales operaciones con listas y los casos de uso de las listas vacías en Python.
Adel Nehme's photo

Adel Nehme

5 min

tutorial

Programación funcional frente a programación orientada a objetos en el análisis de datos

Explore dos de los paradigmas de programación más utilizados en la ciencia de datos: la programación orientada a objetos y la programación funcional.
Amberle McKee's photo

Amberle McKee

15 min

tutorial

Guía paso a paso para hacer mapas en Python usando la librería Plotly

Haz que tus datos destaquen con impresionantes mapas creados con Plotly en Python
Moez Ali's photo

Moez Ali

7 min

tutorial

Tutorial de Python Seaborn Line Plot: Crear visualizaciones de datos

Descubra cómo utilizar Seaborn, una popular biblioteca de visualización de datos de Python, para crear y personalizar gráficos de líneas en Python.
Elena Kosourova's photo

Elena Kosourova

12 min

See MoreSee More