Test Driven Development con pytest

Introducción

Buen software es software probado. Probar nuestro código puede ayudarnos a detectar errores o comportamientos no deseados.
El desarrollo dirigido por pruebas (TDD, por sus siglas en inglés ) es una práctica de desarrollo de software que requiere que escribamos pruebas de forma incremental para las características que deseamos agregar. Aprovecha las suites de prueba automatizadas, como pytest , un marco de prueba para programas Python.

Pruebas automatizadas

Los desarrolladores usualmente escriben el código, lo compilan si es necesario y luego ejecutan 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 exhaustivo con sus pruebas, deberá recordar cómo probar los distintos resultados de cada función.
¿Qué pasaría si un nuevo desarrollador comenzara a agregar características al proyecto, tendría que aprender sus características para probarlo también? Las nuevas funciones a veces afectan a las antiguas, ¿va a verificar manualmente que todas las funciones anteriores aún funcionan cuando agregó una nueva?
Las pruebas manuales nos pueden dar 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 cambian la carga de probar el código nosotros mismos y realizar un seguimiento de los resultados, al mantenimiento de 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 modulo 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 de elección en este artículo.
La mayoría de los desarrolladores encuentran pytestmás fácil de usar que unittestUna 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 ser 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 dirigido por pruebas?

Test-Driven Development es una práctica de desarrollo de software simple que le indica a usted o a un equipo de codificadores que sigan estos pasos de árbol para crear software:
  1. Escribe una prueba para una característica que falla
  2. Escribir código para hacer pasar la prueba.
  3. Refactorice el código según sea necesario
Este proceso se conoce comúnmente como el ciclo Red-Green-Refactor :
  • Usted escribe una prueba automatizada de cómo debe comportarse el nuevo código y lo ve fallar - Rojo
  • Escriba el código en la aplicación hasta que pase su prueba - Verde
  • Refactorice el código para que sea legible y eficiente. No hay necesidad de preocuparse de que su refactorización rompa la nueva función, simplemente debe volver a ejecutar la prueba y asegurarse de que pase.
Una característica está completa cuando ya no necesitamos escribir código para que pasen las pruebas.

¿Por qué usar 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 requerido por usted para mantenerlas disminuye. Además, TDD proporciona los siguientes beneficios, que puede encontrar validos por el tiempo de compensación:
  • 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 el código base: 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 nuevos cambios rompieron lo que existía antes.
  • TDD no elimina todos los errores, pero la probabilidad de encontrarlos es menor: cuando se trata de corregir un error, puede escribir una prueba para asegurarse de que se solucione cuando finalice la codificación.
  • Las pruebas se pueden utilizar como documentación adicional. A medida que escribimos las entradas y salidas de una característica, un desarrollador puede ver la prueba y ver cómo se debe usar la interfaz del código.

Cobertura de código

La cobertura del código es una métrica que mide la cantidad de código fuente que cubre su plan de prueba.
La cobertura del código al 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 maneras diferentes, aquí hay algunas métricas populares:
  • Líneas de código probadas
  • Cuántas funciones definidas son probadas
  • Cuántas ramas ( ifdeclaraciones, por ejemplo) se prueban
Es importante que sepa qué métricas utiliza su herramienta de cobertura de código.
Mientras hacemos un uso intensivo del mismo pytest, usaremos el popular complemento pytest-covpara obtener cobertura de código.
La cobertura de código alto no significa que su aplicación no tendrá errores. Es más que probable que el código no haya sido probado para todos los escenarios posibles .

Prueba unitaria vs 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 interactúe como lo esperamos también.
A medida que desarrollamos aplicaciones más grandes, tendremos que desarrollar muchos componentes. Si bien estos componentes individuales pueden tener sus correspondientes pruebas unitarias , también queremos una manera de asegurarnos de que estos componentes múltiples, cuando se usan juntos, cumplan con nuestras expectativas.
TDD requiere que comencemos escribiendo una sola prueba que falle con el código base actual, y luego trabajemos para completarla. No especifica que haya sido una prueba de unidad, su primera prueba puede ser una prueba de integración si lo desea.
Cuando se escribe su primera prueba de integración que falla, podemos comenzar a desarrollar cada componente individual.
La prueba de integración fallará hasta que cada componente se construya y pase sus pruebas. Cuando se pasa la prueba de integración, si se creara correctamente, habríamos cumplido con los requisitos de un 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 devuelve 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 agrega los números primos de una secuencia de números dada.
Cree un directorio llamado primesen un área de trabajo de su elección. Ahora agregue dos archivos: primes.pytest_primes.pyEl 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 llamado a 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 debe tomar un número y devolverlo Truesi es primo y de Falseotra manera.
En nuestro test_primes.py, vamos a agregar 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 idiomas) que inmediatamente arroja un error si una condición falla. Esta palabra clave es útil al escribir pruebas porque apunta exactamente a qué condición falló.
Si ingresamos 1o un número menor que 1, entonces no puede ser primo.
Ahora vamos a ejecutar nuestra prueba. Ingrese lo siguiente en su línea de comando:
$ pytest
Para salida detallada puede ejecutar pytest -vAsegúrese de que su entorno virtual aún esté activo (debería ver (env)al principio de la línea en su terminal).
Debes notar una salida como esta:
    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 una NameError, todavía no hemos creado nuestra función. Este es el aspecto "rojo" del ciclo rojo-verde-refactor.
pytestIncluso los registros fallaron las pruebas en color rojo si su shell está configurado para mostrar colores. Ahora agreguemos el código en nuestro primes.pyarchivo para hacer que esta prueba pase:
def is_prime(num):  
    if num == 1:
        return False
Nota : generalmente 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 las preocupaciones a medida que crece su base de código, 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 otro desarrollador lo usaría.
Ahora vamos a correr pytestuna vez más. Ahora deberíamos ver una salida como esta:
=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0  
rootdir: /Users/marcus/stackabuse/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.
Debemos 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 corremos pytest , nuestras pruebas todavía pasarían.
Ahora agreguemos un caso de prueba para un número primo, test_primes.pyagregue 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/stackabuse/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 ya que no calculamos realmente si el número es primo o no. La is_prime()función regresa Nonecomo otras funciones lo hacen por defecto para cualquier número mayor que 1.
La salida sigue fallando, o vemos rojo de 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 bucle 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 los números entre 2 y la raíz cuadrada del número.
Si no hay ningún resto de la división, entonces tiene un divisor que no es ni 1 ni sí mismo, y por lo tanto no es primo. Si no encuentra un divisor en el bucle, entonces debe ser primo.
Vamos a actualizar 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/stackabuse/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.
En test_primes.pyañadir el siguiente caso de prueba a continuación:
def test_prime_composite_number():  
    assert is_prime(15) == False
Si corremos 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/stackabuse/test-driven-development-with-pytest/primes  
plugins: cov-2.6.1  
collected 3 items

test_primes.py ...                                                                                                                   [100%]

========================================================= 3 passed in 0.04 seconds =========================================================

Prueba de suma de números primos ()

Al igual que con is_prime(), pensemos en los resultados de esta función. Si a la función se le da 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, desearemos probar que solo agrega números primos en una lista de números.
Escribamos nuestra primera prueba que falla, agregue el siguiente código al final de test_primes.py:
def test_sum_of_primes_empty_list():  
    assert sum_of_primes([]) == 0
Si ejecutamos pytestobtendremos el NameErrorerror de prueba familiar , 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)
Ahora en ejecución pytestdemostraría que todas las pruebas pasan. Nuestra próxima prueba debe garantizar que solo se agreguen los números primos.
Mezclaremos números primos y compuestos y esperaremos que la función solo agregue 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 en la lista que estamos probando son 11 y 17, que suman 28.
Se ejecuta pytestpara validar que la nueva prueba falla. Ahora modifiquemos nuestro sum_of_primes()para que solo se agreguen los 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 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%! De no ser así, podemos pasar algún tiempo agregando algunas pruebas más a nuestro código para asegurarnos de que nuestro plan de pruebas sea completo.
Por ejemplo, si a nuestra is_prime()función se le asigna un valor flotante, ¿arrojaría un error? Nuestrois_prime() método no impone la regla de que un número primo debe ser un número natural, solo verifica que sea mayor que 1.
Aunque tengamos cobertura total de código, la función que se está implementando puede no funcionar correctamente en todas las situaciones.

Ejemplo avanzado: escribiendo un gestor de inventario

Ahora que comprendimos los conceptos básicos de TDD, profundicemos en algunas características útiles pytestque nos permiten ser más eficientes en las pruebas de escritura.
Al igual que antes en nuestro ejemplo básico inventory.py, y un archivo de prueba test_inventory.py, serán nuestros dos archivos principales.

Características y planificación de pruebas

Una tienda de ropa y calzado desea transferir la administración de sus artículos del papel a una computadora nueva que compró el propietario. Si bien al propietario le gustaría tener muchas características, está contenta con un software que podría realizar las siguientes tareas de inmediato.
  • Graba las 10 nuevas zapatillas Nike que compró recientemente. Cada uno vale $ 50.00.
  • Agrega 5 pantalones de chándal Adidas más que cuestan $ 70.00 cada uno.
  • Ella espera que un cliente compre 2 de las zapatillas Nike.
  • Ella está esperando que otro cliente compre uno de los pantalones deportivos.
Podemos utilizar estos requisitos para crear nuestra primera prueba de integración. Antes de escribirlo, vamos a desarrollar 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á un nombre, precio y cantidad. Podremos agregar nuevos artículos, agregar acciones a artículos existentes y, por supuesto, eliminar acciones.
Cuando creamos una instancia de un Inventoryobjeto, queremos que el usuario proporcione un archivo limitEl 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 superemos nuestro límite, tendremos que hacer un seguimiento del total_itemscontador. Cuando se inicializa, este debe ser 0.
Tendremos que agregar 10 zapatillas Nike y los 5 pantalones deportivos Adidas al sistema. Podemos crear un add_new_stock()método que acepta una namepricequantity.
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 artículos 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 el namestock y la quantityeliminación de los elementos. Si la cantidad que se está eliminando es negativa o si hace que la cantidad total para el stock sea inferior a 0, el método debería generar una excepción. Además, si el contenido nameno se encuentra en nuestro inventario, el método debería generar una excepción.

Primeras pruebas

Prepararse para hacer nuestras pruebas primero nos ha ayudado a diseñar nuestro sistema. Empecemos por crear 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 que se realiza una acción, de modo que cuando realice la depuración, sabrá el último paso que se tomó.
Ejecutar pytesty debería fallar con una clase NameErrorno Inventorydefinida.
Vamos a crear nuestra Inventoryclase, con un parámetro límite que por defecto es 100, comenzando con las pruebas de unidad:
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 propia clase:
class Inventory:  
    def __init__(self, limit=100):
        self.limit = limit
        self.total_items = 0
Antes de pasar a los métodos, queremos asegurarnos de que nuestro objeto se puede inicializar con un límite personalizado, y se debe configurar 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 sigue 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 con el que se ejecutan las pruebas para garantizar que los resultados sean repetibles.
Es una buena práctica que las pruebas se ejecuten aisladas unas de otras. Los resultados de un caso de prueba no deben afectar los resultados de otro caso de prueba.
Vamos a crear 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.fixture decorador . Para propósitos 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 que se aplique el accesorio. De lo contrario, lo utilizarías 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 el momento. 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 añadiremos 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ás que un objeto de stocks se inicializó en la __init__función. Una vez más, ejecute pytestpara confirmar que la prueba pasó.

Pruebas de parametrización

Anteriormente mencionamos que el add_new_stock()método hace validación de entrada: generamos una excepción si la cantidad es cero o negativa, o si nos lleva más allá 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 utilizando una función. Escribamos una función de prueba parametrizada para asegurar 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 un stock, 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 fue lanzada contaría como un pase. Nuestra prueba por lo tanto 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 parámetros. El segundo argumento es una lista de tuplas donde cada tupla es un caso de prueba.
Ejecutar pytestpara ver nuestra prueba fallar como InvalidQuantityExceptionno está definido. De vuelta en inventory.pyvamos a crear una nueva excepción por encima de 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
Corre 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 ésta 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 que lleva 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
Corre pytestpara ver que su nuevo caso de prueba pasa también.
Podemos utilizar accesorios con nuestra función parametrizada. Vamos a refactorizar 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 parametrizado.
Mirando el código un poco más, no hay razón para que haya dos métodos para agregar nuevos stocks. Podemos probar errores y éxito en una función.
Eliminar test_add_new_stock_bad_input()test_add_new_stock_success()y vamos a añadir 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 a través de un parámetro agrupado. Como no esperamos que se lance una excepción en el caso exitoso, especificamos Nonecomo nuestra excepción.

Envolviendo nuestro administrador 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 intenten 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 corras pytestdebes ver que la prueba de integración y todas las demás pasen!

Conclusión

El desarrollo dirigido por pruebas es un proceso de desarrollo de software en el que las pruebas se utilizan para guiar el diseño de un sistema. TDD exige que para cada función que tengamos que implementar escribamos una prueba que falla, agregamos la menor cantidad de código para hacer que la prueba pase, y finalmente refactorizamos ese código para que sea más limpio.
Para hacer este proceso posible y eficiente, aprovechamos pytestuna herramienta de prueba automatizada. Con pytestnosotros podemos 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 interactúe como nosotros también esperamos. Tanto la pytestherramienta como la metodología TDD permiten el uso de ambos tipos de pruebas, y se alienta a los desarrolladores a usar ambos.
Con TDD, nos vemos obligados a pensar en las entradas y salidas de nuestro sistema y, por lo tanto, su diseño general. Las pruebas de escritura 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 pytestCon características como accesorios y funciones parametrizadas, podemos escribir rápidamente casos de prueba según lo necesiten nuestros requisitos.

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.