Desarrollo dirigido por pruebas con pytest

    Introducci贸n

    Un buen software es software probado. Probar nuestro c贸digo puede ayudarnos a detectar errores o comportamientos no deseados.

    Test Driven Development (TDD) es una pr谩ctica de desarrollo de software que nos obliga a escribir pruebas de forma incremental para las funciones que queremos agregar. Aprovecha las suites de prueba automatizadas, como pytest , un marco de prueba para programas de Python.

    • Pruebas automatizadas
    • El m贸dulo pytest
    • 驴Qu茅 es el desarrollo basado en pruebas?
    • 驴Por qu茅 utilizar TDD para crear aplicaciones?
    • Cobertura de c贸digo
    • Prueba unitaria frente a pruebas de integraci贸n
    • Ejemplo b谩sico: calcular la suma de n煤meros primos
    • Ejemplo avanzado: escribir un administrador de inventario
    • Conclusi贸n

    Pruebas automatizadas

    Los desarrolladores suelen escribir c贸digo, compilarlo si es necesario y luego ejecutar el c贸digo para ver si funciona. Este es un ejemplo de prueba manual. En este m茅todo exploramos qu茅 caracter铆sticas del programa funcionan. Si desea ser minucioso con sus pruebas, tendr谩 que recordar c贸mo probar los distintos resultados de cada funci贸n.

    驴Y si un nuevo desarrollador comenzara a agregar funciones al proyecto, tendr铆a que aprender sus funciones para probarlo tambi茅n? Las funciones nuevas a veces afectan a las funciones m谩s antiguas, 驴va a comprobar manualmente que todas las funciones anteriores siguen funcionando cuando agregue una nueva?

    Las pruebas manuales pueden darnos un r谩pido impulso de confianza para continuar con el desarrollo. Sin embargo, a medida que nuestra aplicaci贸n crece, se vuelve exponencialmente m谩s dif铆cil y tedioso probar continuamente nuestra base de c贸digo manualmente.

    Las pruebas automatizadas trasladan la carga de probar el c贸digo nosotros mismos y realizar un seguimiento de los resultados, para mantener los scripts que lo hacen por nosotros. Los scripts ejecutan m贸dulos del c贸digo con entradas definidas por el desarrollador y comparan la salida con las expectativas definidas por el desarrollador.

    El m贸dulo pytest

    La biblioteca est谩ndar de Python viene con un marco de prueba automatizado: la biblioteca unittest . Si bien la unittestbiblioteca es rica en funciones y efectiva en su tarea, la usaremos pytestcomo nuestra arma preferida en este art铆culo.

    La mayor铆a de los desarrolladores encuentran pytestm谩s f谩cil de usar que unittest. Una raz贸n simple es que pytestsolo requiere funciones para escribir pruebas, mientras que el unittestm贸dulo requiere clases.

    Para muchos desarrolladores nuevos, requerir clases para las pruebas puede resultar un poco desagradable. pytesttambi茅n incluye muchas otras caracter铆sticas que usaremos m谩s adelante en este tutorial que no est谩n presentes en el unittestm贸dulo.

    驴Qu茅 es el desarrollo basado en pruebas?

    El desarrollo basado en pruebas es una pr谩ctica de desarrollo de software simple que le indica a usted oa un equipo de codificadores que sigan estos pasos del 谩rbol para crear software:

    • Escribe una prueba para una funci贸n que falla
    • Escribe el c贸digo para que la prueba pase
    • Refactorice el c贸digo seg煤n sea necesario

    Este proceso se conoce com煤nmente como el ciclo Red-Green-Refactor :

    • Escribes una prueba automatizada sobre c贸mo deber铆a comportarse el nuevo c贸digo y ves que falla – Rojo
    • Escriba el c贸digo en la aplicaci贸n hasta que pase la prueba: verde
    • Refactorice el c贸digo para hacerlo legible y eficiente. No hay necesidad de preocuparse de que su refactorizaci贸n rompa la nueva funci贸n, simplemente necesita volver a ejecutar la prueba y asegurarse de que pase.

    Una funci贸n est谩 completa cuando ya no necesitamos escribir c贸digo para aprobar sus pruebas.

    驴Por qu茅 utilizar TDD para crear aplicaciones?

    La queja com煤n de usar TDD es que lleva demasiado tiempo.

    A medida que se vuelve m谩s eficiente con las pruebas de escritura, el tiempo que necesita para mantenerlas disminuye. Adem谩s, TDD ofrece los siguientes beneficios, que puede encontrar que valen la pena invertir en tiempo:

    • Las pruebas de escritura requieren que conozca las entradas y salidas para que la funci贸n funcione; TDD nos obliga a pensar en la interfaz de la aplicaci贸n antes de comenzar a codificar.
    • Mayor confianza en la base de c贸digo: al tener pruebas automatizadas para todas las funciones, los desarrolladores se sienten m谩s seguros al desarrollar nuevas funciones. Se vuelve trivial probar todo el sistema para ver si los nuevos cambios rompieron lo que exist铆a antes.
    • TDD no elimina todos los errores, pero la probabilidad de encontrarlos es menor. Cuando intentas corregir un error, puedes escribir una prueba para asegurarte de que est茅 corregido cuando termine de codificar.
    • Las pruebas se pueden utilizar como documentaci贸n adicional. Mientras escribimos las entradas y salidas de una caracter铆stica, un desarrollador puede mirar la prueba y ver c贸mo se debe usar la interfaz del c贸digo.

    Cobertura de c贸digo

    La cobertura de c贸digo es una m茅trica que mide la cantidad de c贸digo fuente que cubre su plan de prueba.

    Cobertura de c贸digo del 100% significa que todo el c贸digo que ha escrito ha sido utilizado por algunas pruebas. Las herramientas miden la cobertura del c贸digo de muchas formas diferentes; aqu铆 hay algunas m茅tricas populares:

    • L铆neas de c贸digo probadas
    • Cu谩ntas funciones definidas se prueban
    • Cu谩ntas ramas ( ifdeclaraciones, por ejemplo) se prueban

    Es importante que sepa qu茅 m茅tricas utiliza su herramienta de cobertura de c贸digo.

    A medida que hagamos un uso intensivo de pytest, usaremos el popular complemento pytest-cov para obtener cobertura de c贸digo.

    La alta cobertura de c贸digo no significa que su aplicaci贸n no tenga errores. Es m谩s que probable que el c贸digo no se haya probado en todos los escenarios posibles.

    Prueba unitaria frente a pruebas de integraci贸n

    Las pruebas unitarias se utilizan para garantizar que un m贸dulo individual se comporte como se espera, mientras que las pruebas de integraci贸n aseguran que una colecci贸n de m贸dulos interopere como esperamos.

    A medida que desarrollemos aplicaciones m谩s grandes, tendremos que desarrollar muchos componentes. Si bien estos componentes individuales pueden tener cada uno sus pruebas unitarias correspondientes, tambi茅n queremos una forma de asegurarnos de que estos componentes m煤ltiples cuando se usan juntos cumplen nuestras expectativas.

    TDD requiere que comencemos por escribir una 煤nica prueba que falle con el c贸digo base actual, luego trabajemos para completarla. No especifica que haya sido una prueba unitaria, su primera prueba puede ser una prueba de integraci贸n si lo desea.

    Cuando se escribe su primera prueba de integraci贸n fallida, podemos comenzar a desarrollar cada componente individual.

    La prueba de integraci贸n fallar谩 hasta que cada componente est茅 construido y pase sus pruebas. Cuando pase la prueba de integraci贸n, si se dise帽贸 correctamente, habr铆amos cumplido con un requisito del usuario para nuestro sistema.

    Ejemplo b谩sico: calcular la suma de n煤meros primos

    La mejor manera de entender TDD es ponerlo en pr谩ctica. Comenzaremos escribiendo un programa Python que devuelva la suma de todos los n煤meros en una secuencia que son n煤meros primos.

    Crearemos dos funciones para hacer esto, una que determina si un n煤mero es primo o no y otra que suma los n煤meros primos de una determinada secuencia de n煤meros.

    Cree un directorio llamado primesen un espacio de trabajo de su elecci贸n. Ahora agregue dos archivos: primes.py, test_primes.py. El primer archivo es donde escribiremos nuestro c贸digo de programa, el segundo archivo es donde estar谩n nuestras pruebas.

    pytestrequiere que nuestros archivos de prueba comiencen con “test_” o terminen con “_test.py” (por lo tanto, tambi茅n podr铆amos haber nombrado nuestro archivo de prueba primes_test.py).

    Ahora en nuestro primesdirectorio, configuremos nuestro entorno virtual:

    $ python3 -m venv env # Create a virtual environment for our modules
    $ . env/bin/activate # Activate our virtual environment
    $ pip install --upgrade pip # Upgrade pip
    $ pip install pytest # Install pytest
    

    Probando la funci贸n is_prime ()

    Un n煤mero primo es cualquier n煤mero natural mayor que 1 que solo es divisible por 1 y por s铆 mismo.

    Nuestra funci贸n deber铆a tomar un n煤mero y regresar Truesi es primo o Falseno.

    En nuestro test_primes.py, agreguemos nuestro primer caso de prueba:

    def test_prime_low_number():
        assert is_prime(1) == False
    

    La assert()declaraci贸n es una palabra clave en Python (y en muchos otros lenguajes) que inmediatamente arroja un error si falla una condici贸n. Esta palabra clave es 煤til al escribir pruebas porque se帽ala exactamente qu茅 condici贸n fall贸.

    Si ingresamos 1o un n煤mero menor que 1, entonces no puede ser primo.

    Ejecutemos ahora nuestra prueba. Ingrese lo siguiente en su l铆nea de comando:

    $ pytest
    

    Para una salida detallada, puede ejecutar pytest -v. Aseg煤rese de que su entorno virtual todav铆a est茅 activo (deber铆a ver (env)al principio de la l铆nea en su terminal).

    Deber铆a notar un resultado como este:

        def test_prime_low_number():
    >       assert is_prime(1) == False
    E       NameError: name 'is_prime' is not defined
    
    test_primes.py:2: NameError
    ========================================================= 1 failed in 0.12 seconds =========================================================
    

    Tiene sentido obtener un NameError, a煤n no hemos creado nuestra funci贸n. Este es el aspecto “rojo” del ciclo de refactorizaci贸n rojo-verde.

    pytestincluso registra las pruebas fallidas en el color rojo si su shell est谩 configurado para mostrar colores. Ahora agreguemos el c贸digo en nuestro primes.pyarchivo para que esta prueba pase:

    def is_prime(num):
        if num == 1:
            return False
    

    Nota : En general, es una buena pr谩ctica mantener sus pruebas en archivos separados de su c贸digo. Adem谩s de mejorar la legibilidad y la separaci贸n de preocupaciones a medida que crece su c贸digo base, tambi茅n mantiene al desarrollador de la prueba alejado del funcionamiento interno del c贸digo. Por lo tanto, las pruebas usan las interfaces de la aplicaci贸n de la misma manera que las usar铆a otro desarrollador.

    Ahora corramos pytestuna vez m谩s. Ahora deber铆amos ver un resultado como este:

    =========================================================== test session starts ============================================================
    platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
    rootdir: /Users/marcus/Pharos.sh/test-driven-development-with-pytest/primes
    plugins: cov-2.6.1
    collected 1 item
    
    test_primes.py .                                                                                                                     [100%]
    
    ========================================================= 1 passed in 0.04 seconds =========================================================
    

    隆Nuestra primera prueba pas贸! Sabemos que 1 no es primo, pero por definici贸n 0 no es primo ni ning煤n n煤mero negativo.

    Deber铆amos refactorizar nuestra aplicaci贸n para reflejar eso y cambiar is_prime()a:

    def is_prime(num):
        # Prime numbers must be greater than 1
        if num < 2:
            return False
    

    Si volvemos a correr pytest, nuestras pruebas a煤n pasar铆an.

    Ahora agreguemos un caso de prueba para un n煤mero primo, y test_primes.pyagreguemos lo siguiente despu茅s de nuestro primer caso de prueba:

    def test_prime_prime_number():
        assert is_prime(29)
    

    Y corramos pytestpara ver esta salida:

        def test_prime_prime_number():
    >       assert is_prime(29)
    E       assert None
    E        +  where None = is_prime(29)
    
    test_primes.py:9: AssertionError
    ============================================================= warnings summary =============================================================
    test_primes.py::test_prime_prime_number
      /Users/marcus/Pharos.sh/test-driven-development-with-pytest/primes/test_primes.py:9: PytestWarning: asserting the value None, please use "assert is None"
        assert is_prime(29)
    
    -- Docs: https://docs.pytest.org/en/latest/warnings.html
    ============================================== 1 failed, 1 passed, 1 warnings in 0.12 seconds ==============================================
    

    Tenga en cuenta que el pytestcomando ahora ejecuta las dos pruebas que hemos escrito.

    El nuevo caso falla porque en realidad no calculamos si el n煤mero es primo o no. La is_prime()funci贸n regresa Nonecomo lo hacen otras funciones por defecto para cualquier n煤mero mayor que 1.

    La salida a煤n falla, o vemos rojo en la salida.

    Pensemos en c贸mo determinamos d贸nde un n煤mero es primo o no. El m茅todo m谩s simple ser铆a hacer un ciclo desde 2 hasta uno menos que el n煤mero, dividiendo el n煤mero por el valor actual de la iteraci贸n.

    Para hacer esto m谩s eficiente, podemos verificar dividiendo n煤meros entre 2 y la ra铆z cuadrada del n煤mero.

    Si no hay resto de la divisi贸n, entonces tiene un divisor que no es ni 1 ni 茅l mismo, y por lo tanto no es primo. Si no encuentra un divisor en el bucle, debe ser primo.

    Actualicemos is_prime()con nuestra nueva l贸gica:

    import math
    
    def is_prime(num):
        # Prime numbers must be greater than 1
        if num < 2:
            return False
        for n in range(2, math.floor(math.sqrt(num) + 1)):
            if num % n == 0:
                return False
        return True
    

    Ahora corremos pytestpara ver si nuestra prueba pasa:

    =========================================================== test session starts ============================================================
    platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
    rootdir: /Users/marcus/Pharos.sh/test-driven-development-with-pytest/primes
    plugins: cov-2.6.1
    collected 2 items
    
    test_primes.py ..                                                                                                                    [100%]
    
    ========================================================= 2 passed in 0.04 seconds =========================================================
    

    Pas贸. Sabemos que esta funci贸n puede obtener un n煤mero primo y un n煤mero bajo. Agreguemos una prueba para asegurarnos de que devuelve Falseun n煤mero compuesto mayor que 1.

    Adem谩s, test_primes.pyagregue el siguiente caso de prueba a continuaci贸n:

    def test_prime_composite_number():
        assert is_prime(15) == False
    

    Si ejecutamos pytestveremos el siguiente resultado:

    =========================================================== test session starts ============================================================
    platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
    rootdir: /Users/marcus/Pharos.sh/test-driven-development-with-pytest/primes
    plugins: cov-2.6.1
    collected 3 items
    
    test_primes.py ...                                                                                                                   [100%]
    
    ========================================================= 3 passed in 0.04 seconds =========================================================
    

    Probando sum_of_primes ()

    Al igual que con is_prime(), pensemos en los resultados de esta funci贸n. Si la funci贸n tiene una lista vac铆a, entonces la suma debe ser cero.

    Eso garantiza que nuestra funci贸n siempre debe devolver un valor con una entrada v谩lida. Despu茅s, querremos probar que solo agrega n煤meros primos en una lista de n煤meros.

    Escribamos nuestra primera prueba fallida, agreguemos el siguiente c贸digo al final de test_primes.py:

    def test_sum_of_primes_empty_list():
        assert sum_of_primes([]) == 0
    

    Si ejecutamos pytest, obtendremos el conocido NameErrorerror de prueba, ya que a煤n no definimos la funci贸n. En nuestro primes.pyarchivo, agreguemos nuestra nueva funci贸n que simplemente devuelve la suma de una lista dada:

    def sum_of_primes(nums):
        return sum(nums)
    

    Ejecutar ahora pytestmostrar铆a que todas las pruebas pasan. Nuestra pr贸xima prueba deber铆a garantizar que solo se agreguen n煤meros primos.

    Mezclaremos n煤meros primos y compuestos y esperamos que la funci贸n solo sume los n煤meros primos:

    def test_sum_of_primes_mixed_list():
        assert sum_of_primes([11, 15, 17, 18, 20, 100]) == 28
    

    Los n煤meros primos de la lista que estamos probando son 11 y 17, que suman 28.

    Ejecutando pytestpara validar que la nueva prueba falla. Ahora modifiquemos nuestro sum_of_primes()para que solo se sumen n煤meros primos.

    Filtraremos los n煤meros primos con una Comprensi贸n de lista:

    def sum_of_primes(nums):
        return sum([x for x in nums if is_prime(x)])
    

    Como es de rutina, corremos pytestpara verificar que arreglamos la prueba fallida: todo pasa.

    Una vez completado, revisemos nuestra cobertura de c贸digo:

    $ pytest --cov=primes
    

    隆Para este paquete, nuestra cobertura de c贸digo es del 100%! Si no fue as铆, podemos dedicar un tiempo a agregar algunas pruebas m谩s a nuestro c贸digo para asegurarnos de que nuestro plan de prueba sea completo.

    Por ejemplo, si a nuestra is_prime()funci贸n se le diera un valor flotante, 驴arrojar铆a un error? Nuestro is_prime()m茅todo no aplica la regla de que un n煤mero primo debe ser un n煤mero natural, solo verifica que sea mayor que 1.

    Aunque tenemos una cobertura de c贸digo total, es posible que la funci贸n que se est谩 implementando no funcione correctamente en todas las situaciones.

    Ejemplo avanzado: escribir un administrador de inventario

    Ahora que comprendemos los conceptos b谩sicos de TDD, profundicemos en algunas caracter铆sticas 煤tiles pytestque nos permiten ser m谩s eficientes en la redacci贸n de pruebas.

    Al igual que antes en nuestro ejemplo b谩sico,, inventory.pyy un archivo de prueba test_inventory.py, ser谩n nuestros dos archivos principales.

    Funciones y planificaci贸n de pruebas

    Una tienda de ropa y calzado quisiera trasladar la administraci贸n de sus art铆culos del papel a una computadora nueva que compr贸 el propietario.
    Si bien al propietario le gustar铆a muchas funciones, est谩 contenta con un software que podr铆a realizar las siguientes tareas pr贸ximas de inmediato.

    • Registre las 10 nuevas zapatillas Nike que compr贸 recientemente. Cada uno vale $ 50,00.
    • Agrega 5 pantalones deportivos Adidas m谩s que cuestan $ 70.00 cada uno.
    • Ella espera que un cliente compre 2 de las zapatillas Nike
    • Ella espera que otro cliente compre uno de los pantalones deportivos.

    Podemos utilizar estos requisitos para crear nuestra primera prueba de integraci贸n. Antes de comenzar a escribirlo, desarrollemos un poco los componentes m谩s peque帽os para averiguar cu谩les ser铆an nuestras entradas y salidas, firmas de funciones y otros elementos de dise帽o del sistema.

    Cada art铆culo de stock tendr谩 nombre, precio y cantidad. Podremos agregar nuevos art铆culos, agregar stock a los art铆culos existentes y, por supuesto, eliminar stock.

    Cuando creamos una instancia de un Inventoryobjeto, queremos que el usuario proporcione un limit. El limittendr谩 un valor por defecto de 100. Nuestra primera prueba ser铆a comprobar el limitcuando una instancia de un objeto. Para asegurarnos de que no superamos nuestro l铆mite, tendremos que realizar un seguimiento del total_itemscontador. Cuando se inicializa, debe ser 0.

    Necesitaremos agregar 10 zapatillas Nike y los 5 pantalones deportivos Adidas al sistema. Podemos crear un add_new_stock()m茅todo que acepta una name, pricey quantity.

    Debemos probar que podemos agregar un art铆culo a nuestro objeto de inventario. No deber铆amos poder agregar un art铆culo con una cantidad negativa, el m茅todo deber铆a generar una excepci贸n. Tampoco deber铆amos poder agregar m谩s elementos si estamos en nuestro l铆mite, eso tambi茅n deber铆a generar una excepci贸n.

    Los clientes comprar谩n estos art铆culos poco despu茅s de la entrada, por lo que tambi茅n necesitaremos un remove_stock()m茅todo. Esta funci贸n necesitar铆a la eliminaci贸n namedel stock y de los quantityart铆culos. Si la cantidad que se retira es negativa o si la cantidad total de las existencias es inferior a 0, entonces el m茅todo deber铆a generar una excepci贸n. Adem谩s, si lo nameproporcionado no se encuentra en nuestro inventario, el m茅todo deber铆a generar una excepci贸n.

    Primeras pruebas

    Prepararnos para hacer nuestras pruebas primero nos ha ayudado a dise帽ar nuestro sistema. Comencemos creando nuestra primera prueba de integraci贸n:

    def test_buy_and_sell_nikes_adidas():
        # Create inventory object
        inventory = Inventory()
        assert inventory.limit == 100
        assert inventory.total_items == 0
    
        # Add the new Nike sneakers
        inventory.add_new_stock('Nike Sneakers', 50.00, 10)
        assert inventory.total_items == 10
    
        # Add the new Adidas sweatpants
        inventory.add_new_stock('Adidas Sweatpants', 70.00, 5)
        assert inventory.total_items == 15
    
        # Remove 2 sneakers to sell to the first customer
        inventory.remove_stock('Nike Sneakers', 2)
        assert inventory.total_items == 13
    
        # Remove 1 sweatpants to sell to the next customer
        inventory.remove_stock('Adidas Sweatpants', 1)
        assert inventory.total_items == 12
    

    En cada acci贸n hacemos una afirmaci贸n sobre el estado del inventario. Es mejor afirmar despu茅s de realizar una acci贸n, de modo que cuando est茅 depurando sabr谩 el 煤ltimo paso que se tom贸.

    Ejecutar pytesty deber铆a fallar con una clase NameErrorya que no Inventoryest谩 definida.

    Creemos nuestra Inventoryclase, con un par谩metro de l铆mite que por defecto es 100, comenzando con las pruebas unitarias:

    def test_default_inventory():
        """Test that the default limit is 100"""
        inventory = Inventory()
        assert inventory.limit == 100
        assert inventory.total_items == 0
    

    Y ahora, la clase en s铆:

    class Inventory:
        def __init__(self, limit=100):
            self.limit = limit
            self.total_items = 0
    

    Antes de pasar a los m茅todos, queremos estar seguros de que nuestro objeto se puede inicializar con un l铆mite personalizado y debe configurarse correctamente:

    def test_custom_inventory_limit():
        """Test that we can set a custom limit"""
        inventory = Inventory(limit=25)
        assert inventory.limit == 25
        assert inventory.total_items == 0
    

    La integraci贸n contin煤a fallando pero esta prueba pasa.

    Accesorios

    Nuestras dos primeras pruebas nos obligaron a crear una instancia de un Inventoryobjeto antes de poder comenzar. Lo m谩s probable es que tengamos que hacer lo mismo para todas las pruebas futuras. Esto es un poco repetitivo.

    Podemos usar accesorios para ayudar a resolver este problema. Un accesorio es un estado conocido y fijo contra el que se ejecutan las pruebas para garantizar que los resultados sean repetibles.

    Es una buena pr谩ctica que las pruebas se ejecuten de forma aislada. Los resultados de un caso de prueba no deber铆an afectar los resultados de otro caso de prueba.

    Creemos nuestro primer accesorio, un Inventoryobjeto sin stock.

    test_inventory.py:

    import pytest
    
    @pytest.fixture
    def no_stock_inventory():
        """Returns an empty inventory that can store 10 items"""
        return Inventory(10)
    

    Tenga en cuenta el uso del pytest.fixturedecorador. Para fines de prueba, podemos reducir el l铆mite de inventario a 10.

    Usemos este accesorio para agregar una prueba para el add_new_stock()m茅todo:

    def test_add_new_stock_success(no_stock_inventory):
        no_stock_inventory.add_new_stock('Test Jacket', 10.00, 5)
        assert no_stock_inventory.total_items == 5
        assert no_stock_inventory.stocks['Test Jacket']['price'] == 10.00
        assert no_stock_inventory.stocks['Test Jacket']['quantity'] == 5
    

    Observe que el nombre de la funci贸n es el argumento de la prueba, deben ser el mismo nombre para el dispositivo que se aplicar谩. De lo contrario, lo usar铆a como un objeto normal.

    Para asegurarnos de que se agreg贸 el stock, tenemos que probar un poco m谩s que el total de art铆culos almacenados hasta ahora. Escribir esta prueba nos ha obligado a considerar c贸mo mostramos el precio de una acci贸n y la cantidad restante.

    Corre pytestpara observar que ahora hay 2 fallos y 2 pases. Ahora agregaremos el add_new_stock()m茅todo:

    class Inventory:
        def __init__(self, limit=100):
            self.limit = limit
            self.total_items = 0
            self.stocks = {}
    
        def add_new_stock(self, name, price, quantity):
            self.stocks[name] = {
                'price': price,
                'quantity': quantity
            }
            self.total_items += quantity
    

    Notar谩 que un objeto de existencias se inicializ贸 en la __init__funci贸n. Nuevamente, ejecute pytestpara confirmar que la prueba pas贸.

    Pruebas de parametrizaci贸n

    Mencionamos anteriormente que el add_new_stock()m茅todo realiza una validaci贸n de entrada: generamos una excepci贸n si la cantidad es cero o negativa, o si nos lleva por encima del l铆mite de nuestro inventario.

    Podemos agregar f谩cilmente m谩s casos de prueba, usando try / except para detectar cada excepci贸n. Esto tambi茅n se siente repetitivo.

    Pytest proporciona funciones parametrizadas que nos permiten probar m煤ltiples escenarios usando una funci贸n. Escribamos una funci贸n de prueba parametrizada para asegurarnos de que nuestra validaci贸n de entrada funcione:

    @pytest.mark.parametrize('name,price,quantity,exception', [
        ('Test Jacket', 10.00, 0, InvalidQuantityException(
            'Cannot add a quantity of 0. All new stocks must have at least 1 item'))
    ])
    def test_add_new_stock_bad_input(name, price, quantity, exception):
        inventory = Inventory(10)
        try:
            inventory.add_new_stock(name, price, quantity)
        except InvalidQuantityException as inst:
            # First ensure the exception is of the right type
            assert isinstance(inst, type(exception))
            # Ensure that exceptions have the same message
            assert inst.args == exception.args
        else:
            pytest.fail("Expected error but found none")
    

    Esta prueba intenta agregar una acci贸n, obtiene la excepci贸n y luego verifica que sea la excepci贸n correcta. Si no obtenemos una excepci贸n, falla la prueba. La elsecl谩usula es muy importante en este escenario. Sin 茅l, una excepci贸n que no se haya lanzado contar铆a como un pase. Por tanto, nuestra prueba tendr铆a un falso positivo.

    Usamos pytestdecoradores para agregar un par谩metro a la funci贸n. El primer argumento contiene una cadena de todos los nombres de los par谩metros. El segundo argumento es una lista de tuplas donde cada tupla es un caso de prueba.

    Ejecutar pytestpara ver que nuestra prueba falla ya InvalidQuantityExceptionque no est谩 definida. De vuelta inventory.py, creemos una nueva excepci贸n sobre la Inventoryclase:

    class InvalidQuantityException(Exception):
        pass
    

    Y cambia el add_new_stock()m茅todo:

    def add_new_stock(self, name, price, quantity):
            if quantity <= 0:
                raise InvalidQuantityException(
                    'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
            self.stocks[name] = {
                'price': price,
                'quantity': quantity
            }
            self.total_items += quantity
    

    Ejecute pytestpara ver que nuestra prueba m谩s reciente ahora pasa. Ahora agreguemos el segundo caso de prueba de error, se genera una excepci贸n si nuestro inventario no puede almacenarlo. Cambie la prueba de la siguiente manera:

    @pytest.mark.parametrize('name,price,quantity,exception', [
        ('Test Jacket', 10.00, 0, InvalidQuantityException(
            'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
        ('Test Jacket', 10.00, 25, NoSpaceException(
            'Cannot add these 25 items. Only 10 more items can be stored'))
    ])
    def test_add_new_stock_bad_input(name, price, quantity, exception):
        inventory = Inventory(10)
        try:
            inventory.add_new_stock(name, price, quantity)
        except (InvalidQuantityException, NoSpaceException) as inst:
            # First ensure the exception is of the right type
            assert isinstance(inst, type(exception))
            # Ensure that exceptions have the same message
            assert inst.args == exception.args
        else:
            pytest.fail("Expected error but found none")
    

    En lugar de crear una funci贸n completamente nueva, modificamos esta ligeramente para recoger nuestra nueva excepci贸n y agregar otra tupla al decorador. Ahora se ejecutan dos pruebas en una sola funci贸n.

    Las funciones parametrizadas reducen el tiempo necesario para agregar nuevos casos de prueba.

    En inventory.py, primero agregaremos nuestra nueva excepci贸n a continuaci贸n InvalidQuantityException:

    class NoSpaceException(Exception):
        pass
    

    Y cambia el add_new_stock()m茅todo:

    def add_new_stock(self, name, price, quantity):
        if quantity <= 0:
            raise InvalidQuantityException(
                'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
        if self.total_items + quantity > self.limit:
            remaining_space = self.limit - self.total_items
            raise NoSpaceException(
                'Cannot add these {} items. Only {} more items can be stored'.format(quantity, remaining_space))
        self.stocks[name] = {
            'price': price,
            'quantity': quantity
        }
        self.total_items += quantity
    

    Ejecute pytestpara ver que su nuevo caso de prueba tambi茅n se apruebe.

    Podemos usar dispositivos con nuestra funci贸n parametrizada. Refactoricemos nuestra prueba para usar el accesorio de inventario vac铆o:

    def test_add_new_stock_bad_input(no_stock_inventory, name, price, quantity, exception):
        try:
            no_stock_inventory.add_new_stock(name, price, quantity)
        except (InvalidQuantityException, NoSpaceException) as inst:
            # First ensure the exception is of the right type
            assert isinstance(inst, type(exception))
            # Ensure that exceptions have the same message
            assert inst.args == exception.args
        else:
            pytest.fail("Expected error but found none")
    

    Como antes, es solo otro argumento que usa el nombre de una funci贸n. La clave es excluirlo en el decorador parametrizar.

    Mirando el c贸digo un poco m谩s, no hay ninguna raz贸n por la que deba haber dos m茅todos para agregar nuevas existencias. Podemos probar los errores y el 茅xito en una funci贸n.

    Elimina test_add_new_stock_bad_input()y test_add_new_stock_success()agreguemos una nueva funci贸n:

    @pytest.mark.parametrize('name,price,quantity,exception', [
        ('Test Jacket', 10.00, 0, InvalidQuantityException(
            'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
        ('Test Jacket', 10.00, 25, NoSpaceException(
            'Cannot add these 25 items. Only 10 more items can be stored')),
        ('Test Jacket', 10.00, 5, None)
    ])
    def test_add_new_stock(no_stock_inventory, name, price, quantity, exception):
        try:
            no_stock_inventory.add_new_stock(name, price, quantity)
        except (InvalidQuantityException, NoSpaceException) as inst:
            # First ensure the exception is of the right type
            assert isinstance(inst, type(exception))
            # Ensure that exceptions have the same message
            assert inst.args == exception.args
        else:
            assert no_stock_inventory.total_items == quantity
            assert no_stock_inventory.stocks[name]['price'] == price
            assert no_stock_inventory.stocks[name]['quantity'] == quantity
    

    Esta funci贸n de prueba primero verifica las excepciones conocidas, si no se encuentra ninguna, nos aseguramos de que la adici贸n coincida con nuestras expectativas. La test_add_new_stock_success()funci贸n separada ahora se ejecuta mediante un par谩metro tuplado. Como no esperamos que se lance una excepci贸n en el caso exitoso, lo especificamos Nonecomo nuestra excepci贸n.

    Finalizando nuestro Gestor de Inventario

    Con nuestro pytestuso m谩s avanzado , podemos desarrollar r谩pidamente la remove_stockfunci贸n con TDD. En inventory_test.py:

    # The import statement needs one more exception
    from inventory import Inventory, InvalidQuantityException, NoSpaceException, ItemNotFoundException
    
    # ...
    # Add a new fixture that contains stocks by default
    # This makes writing tests easier for our remove function
    @pytest.fixture
    def ten_stock_inventory():
        """Returns an inventory with some test stock items"""
        inventory = Inventory(20)
        inventory.add_new_stock('Puma Test', 100.00, 8)
        inventory.add_new_stock('Reebok Test', 25.50, 2)
        return inventory
    
    # ...
    # Note the extra parameters, we need to set our expectation of
    # what totals should be after our remove action
    @pytest.mark.parametrize('name,quantity,exception,new_quantity,new_total', [
        ('Puma Test', 0,
         InvalidQuantityException(
             'Cannot remove a quantity of 0. Must remove at least 1 item'),
            0, 0),
        ('Not Here', 5,
         ItemNotFoundException(
             'Could not find Not Here in our stocks. Cannot remove non-existing stock'),
            0, 0),
        ('Puma Test', 25,
         InvalidQuantityException(
             'Cannot remove these 25 items. Only 8 items are in stock'),
         0, 0),
        ('Puma Test', 5, None, 3, 5)
    ])
    def test_remove_stock(ten_stock_inventory, name, quantity, exception,
                          new_quantity, new_total):
        try:
            ten_stock_inventory.remove_stock(name, quantity)
        except (InvalidQuantityException, NoSpaceException, ItemNotFoundException) as inst:
            assert isinstance(inst, type(exception))
            assert inst.args == exception.args
        else:
            assert ten_stock_inventory.stocks[name]['quantity'] == new_quantity
            assert ten_stock_inventory.total_items == new_total
    

    Y en nuestro inventory.pyarchivo primero creamos la nueva excepci贸n para cuando los usuarios intentan modificar un stock que no existe:

    class ItemNotFoundException(Exception):
        pass
    

    Y luego agregamos este m茅todo a nuestra Inventoryclase:

    def remove_stock(self, name, quantity):
        if quantity <= 0:
            raise InvalidQuantityException(
                'Cannot remove a quantity of {}. Must remove at least 1 item'.format(quantity))
        if name not in self.stocks:
            raise ItemNotFoundException(
                'Could not find {} in our stocks. Cannot remove non-existing stock'.format(name))
        if self.stocks[name]['quantity'] - quantity <= 0:
            raise InvalidQuantityException(
                'Cannot remove these {} items. Only {} items are in stock'.format(
                    quantity, self.stocks[name]['quantity']))
        self.stocks[name]['quantity'] -= quantity
        self.total_items -= quantity
    

    Cuando ejecute pytest, deber铆a ver que la prueba de integraci贸n y todas las dem谩s pasan.

    Conclusi贸n

    El desarrollo basado en pruebas es un proceso de desarrollo de software en el que se utilizan pruebas para guiar el dise帽o de un sistema. TDD exige que para cada caracter铆stica que tengamos que implementar, escribamos una prueba que falle, agreguemos la menor cantidad de c贸digo para que la prueba pase y finalmente refactoricemos ese c贸digo para que sea m谩s limpio.

    Para que este proceso sea posible y eficiente, utilizamos pytestuna herramienta de prueba automatizada. Con pytestpodemos realizar pruebas de script, lo que nos ahorra tiempo de tener que probar manualmente nuestro c贸digo en cada cambio.

    Las pruebas unitarias se utilizan para garantizar que un m贸dulo individual se comporte como se espera, mientras que las pruebas de integraci贸n aseguran que una colecci贸n de m贸dulos interopere como esperamos. Tanto la pytestherramienta como la metodolog铆a TDD permiten el uso de ambos tipos de prueba, y se anima a los desarrolladores a utilizar ambos.

    Con TDD, nos vemos obligados a pensar en las entradas y salidas de nuestro sistema y, por lo tanto, en su dise帽o general. Las pruebas de redacci贸n proporcionan beneficios adicionales, como una mayor confianza en la funcionalidad de nuestro programa despu茅s de los cambios. TDD exige un proceso altamente iterativo que puede ser eficiente al aprovechar un conjunto de pruebas automatizado como pytest. Con caracter铆sticas como accesorios y funciones parametrizadas, podemos escribir r谩pidamente casos de prueba seg煤n lo necesiten nuestros requisitos.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *