unittest es un marco de trabajo que permite realizar pruebas unitarias para un software en Python. Está inspirado en JUnit, por lo que guarda cierta similitud, y permite automatizar, compartir y configurar pruebas unitarias.Este marco permite preparar los datos para hacer las pruebas, definir casos de pruebas, agrupar casos de pruebas y ejecutar los casos para ver los resultados obtenidos.

Este documento presenta los conceptos básicos de unittest, parte de de la aplicación de TutorialCanciones (disponible en https://github.com/MISW-4101-Practicas/TutorialCancionesUnittest) sobre la que se van a ejecutar algunas pruebas, continúa con los métodos para verificar las pruebas y cierra con ejemplos de pruebas unitarias.

Para una guía completa, el lector puede visitar la siguiente dirección: https://docs.python.org/3/library/unittest.html

Objetivo

El propósito de este tutorial es desarrollar pruebas unitarias en Python utilizando unittest.

Requisitos

Para el desarrollo de este tutorial es importante cumplir con los siguientes requisitos:

  1. Tener Python y pip correctamente instalados y actualizados a una versión reciente. Si aún no los instala, puede descargar las versiones para los diferentes sistemas operativos en https://www.python.org/downloads/ y si es necesario revisar las instrucciones de instalación, se pueden seguir en sitios como https://realpython.com/installing-python/
  2. Instalar un IDE. Se recomienda utilizar PyCharm Community Edition (Disponible en: https://www.jetbrains.com/es-es/pycharm/download/) ó Visual Studio Code (Disponible en: https://code.visualstudio.com/download)
  3. Descargar el código del repositorio https://github.com/MISW-4101-Practicas/TutorialCancionesUnittest
  4. Iniciar un virtual environment (venv) para separar el uso de librerías de este proyecto de otros proyectos. Si no ha creado un virtual environment, puede seguir las instrucciones del sitio https://docs.python.org/3/library/venv.html
  5. Contar con SQLAlchemy instalado.

Con estos requisitos ya es posible comenzar con el tutorial.

A continuación se presenta el diagrama de clases que se tomará como base para este tutorial:

El diagrama muestra las clases del modelo que se presentaron en el tutorial de SQLAlchemy y presenta un nuevo paquete que se denomina lógica, el cual contiene la definición de la clase Colección. Esta clase se relaciona con las clases Álbum, Canción e Intérprete y esta contendrá los métodos para consultar y almacenar información sobre estas tres clases. Esta clase se crea dentro de un paquete llamado lógica y la definición de la clase con algunos métodos para gestionar información de álbumes es la siguiente:

from src.modelo.album import Album, Medio
from src.modelo.cancion import Cancion
from src.modelo.interprete import Interprete
from src.modelo.declarative_base import engine, Base, session


class Coleccion():

   def __init__(self):
       Base.metadata.create_all(engine)

   def agregar_album(self, titulo, anio, descripcion, medio):
       busqueda = session.query(Album).filter(Album.titulo == titulo).all()
       if len(busqueda) == 0:
           album = Album(titulo=titulo, ano=anio, descripcion=descripcion, medio=medio)
           session.add(album)
           session.commit()
           return True
       else:
           return False

   def dar_medios(self):
       return [medio.name for medio in Medio]

   def editar_album(self, album_id, titulo, anio, descripcion, medio):
       busqueda = session.query(Album).filter(Album.titulo == titulo, Album.id != album_id).all()
       if len(busqueda) == 0:
           album = session.query(Album).filter(Album.id == album_id).first()
           album.titulo = titulo
           album.ano = anio
           album.descripcion = descripcion
           album.medio = medio
           session.commit()
           return True
       else:
           return False

   def eliminar_album(self, album_id):
       try:
           album = session.query(Album).filter(Album.id == album_id).first()
           session.delete(album)
           session.commit()
           return True
       except:
           return False

   def dar_album_por_id(self, album_id):
       return session.query(Album).get(album_id).__dict__

Este marco de trabajo viene instalado para su uso en python. Para comprobar su funcionamiento se puede ejecutar l siguiente instrucción:

python -m unittest -h

Se mostrará en la salida la información de ayuda de unittest.

Las pruebas unitarias se pueden agrupar en clases las cuales deben crearse dentro del paquete tests en la raíz del proyecto. Cada clase debe llamarse <<clase>>TestCase, dónde <<clase>> hace referencia a la clase cuyos métodos de van a probar, y dicha clase debe heredar de unittest.TestCase para dirigir las pruebas y los métodos que unittest utiliza para verificar dichas pruebas. Para este tutorial se creará la clase AlbumTestCase, se almacenará en el archivo test_album.py dentro del paquete tests, y probará aglunos métodos de la clase Album.

Para crear la clase de pruebas en el archivo test_album.py, primero se deben importar las funcionalidades de unittest, así:

import unittest

Y a continuación se importan la clases Colección (Donde está la lógica de las operaciones), Album (Con el modelo que tiene la información a probar) y la sesión de nuestro declarative_base, así:

from src.logica.coleccion import Coleccion
from src.modelo.album import Album, Medio
from src.modelo.declarative_base import Session

Luego se crea la clase de prueba, en este caso, AlbumTestCase, así:

class AlbumTestCase(unittest.TestCase):

Y a continuación se crean los métodos de la clase que se utilizarán para hacer las pruebas, donde cada método se define como un método en python, con la excepción que que cada método debe comenzar con la palabra test.

Hasta el momento, el archivo test_album.py se ve de la siguiente manera:

import unittest

from src.logica.coleccion import Coleccion
from src.modelo.album import Album, Medio
from src.modelo.declarative_base import Session


class AlbumTestCase(unittest.TestCase):

Para probar que la clase con las pruebas ya permite crearlas, se puede crear el siguiente método:

def test_prueba(self):
   self.assertEqual(0, 0)

Las pruebas deben incluir un llamado a alguno de los métodos assert, ya que con estos llamados es que se verifica que la prueba fue o no exitosa. Luego, para ejecutarla, utiliza la siguiente instrucción desde la raíz del proyecto:

python -m unittest tests/test_album.py

Debe mostrar un resultado similar al siguiente:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

El cual muestra que las pruebas se ejecutaron satisfactoriamente sin fallos o errores. Si se modifica el método, y se escribe de la siguiente manera:

def test_prueba(self):
   self.assertEqual(0, 1)

La prueba debe fallar al ejecutarla, y debe mostrar un mensaje similar al siguiente:

F
======================================================================
FAIL: test_prueba (tests.test_album.AlbumTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\path\to\project\tests\test_album.py", line 7, in test_prueba
    self.assertEqual(0, 1)
AssertionError: 0 != 1
----------------------------------------------------------------------
Ran 1 test in 0.005s

FAILED (failures=1)

El resultado especifica en cuál archivo de las pruebas ocurre el error, la prueba que falló y la verificación que causó el fallo. En ambos casos se puede observar el resultado de los tests, cuales funcionaron adecuadamente y cuales fallaron (Con las causas de los fallos).

Dentro de las opciones para ejecutar las pruebas existen opciones como las siguientes:

python -m unittest -v tests/test_album.py
python -m unittest discover -s tests -v

Para este tutorial y el resto de pruebas se recomienda utilizar este comando.

python -m unittest -h

Hasta el momento, el archivo test_album.py se ve de la siguiente manera:

import unittest

from src.logica.coleccion import Coleccion
from src.modelo.album import Album, Medio
from src.modelo.declarative_base import Session


class AlbumTestCase(unittest.TestCase):
   def test_prueba(self):
      self.assertEqual(0, 1)

Las clases que implementan las pruebas unitarias heredan de la clase TestCase, la cual tiene tres tipos de métodos: Los utilizados para ejecutar las pruebas, los encargados de verificar las pruebas, y un tercer grupo que permiten consultar y recoger información sobre la prueba en sí. Para este tutorial, solamente se abordarán los dos primeros tipos de métodos

Los dos métodos principales para ejecutar las pruebas son:

Los métodos utilizados para verificar las pruebas también se conocen como los métodos assert, los cuales permiten, al momento de correr las pruebas, verificar las condiciones que se establecen en el llamado al método, y en caso de que estas condiciones no se cumplan, se encargan de desencadenar o reportar los fallos.

Los métodos assert más utilizados según el sitio https://docs.python.org/3/library/unittest.html son los siguientes:

Adicionalmente, estos métodos pueden incluir un parámetro adicional, el cual es una cadena de caracteres que representa un mensaje que le aparecerá al usuario cuando ejecute las pruebas y la verificación con el assert falle.

Las pruebas son métodos que al final verifican si una o varias situaciones se están presentando luego de la ejecución de código. Esas situaciones se verifican a través del uso de los métodos assert, los cuales verifican una condición, que al cumplirse, hacen que la prueba pase. Un método de prueba puede incluir varios métodos assert y la prueba que se ejecuta en ese método puede arrojar una combinación de condiciones acertadas y erradas.

Adicionalmente, cada método de pruebas, puede procesar información antes de ejecutar los assert, lo que permite preparar datos para el desarrollo de las pruebas que no se hacen de manera general en el método setUp().

A continuación se presenta un conjunto de pruebas sobre la clase Colección, donde se verificarán que los métodos funcionen adecuadamente. El primer paso es configurar los objetos que se utilizarán para cada caso de prueba prueba en el método setUp():

def setUp(self):
   '''Crea una colección para hacer las pruebas'''
   self.coleccion = Coleccion()

   '''Abre la sesión'''
   self.session = Session()

   '''Crea los objetos'''
   self.album1 = Album(titulo = 'Corazones', ano = 1990, descripcion = 'No tiene', medio = Medio.CD, canciones = [])
   self.album2 = Album(titulo = 'La voz de los 80s', ano = 1984, descripcion = 'No tiene', medio = Medio.CASETE, canciones = [])
   self.album3 = Album(titulo = 'Pateando piedras', ano = 1986, descripcion = 'No tiene', medio = Medio.DISCO, canciones = [])
   self.album4 = Album(titulo = 'La cultura de la basura', ano = 1987, descripcion = 'No tiene', medio = Medio.DISCO, canciones = [])

   '''Adiciona los objetos a la sesión'''
   self.session.add(self.album1)
   self.session.add(self.album2)
   self.session.add(self.album3)
   self.session.add(self.album4)

   '''Persiste los objetos y cierra la sesión'''
   self.session.commit()
   self.session.close()

Aquí se creó una colección, la cual tendrá los métodos que se quieren probar, y se crearon cuatro objetos que son instancias de la clase Álbum que se persisten con el propósito de verificar las operaciones.

También se utilizará el método tearDown para regresar al estado inicial antes de iniciar cada caso de pruebas. Esto eliminará todos los objetos creados, como se muestra a continuación:

def tearDown(self):
   '''Abre la sesión'''
   self.session = Session()

   '''Consulta todos los álbumes'''
   busqueda = self.session.query(Album).all()

   '''Borra todos los álbumes'''
   for album in busqueda:
       self.session.delete(album)

   self.session.commit()
   self.session.close()

En un primer caso, se verificará que el método agregar_album de la clase colección si adiciona un nuevo álbum. Para esto se invoca al método con parámetros válidos y se verifica que retorne True con assertEqual:

def test_agregar_album(self):
   '''Prueba la adición de un álbum'''
   resultado = self.coleccion.agregar_album(titulo = "Nada personal", anio = 1985, descripcion = "No tiene", medio = Medio.CASETE)
   self.assertEqual(resultado, True)

Las pruebas también sirven para verificar que ciertas operación no de un valor específico, como se muestra a continuación, que se prueba que, al repetir un álbum creado en el setup, el método agregar_album no nos debe retornar true:

def test_agregar_album_repetido(self):
   '''Prueba la adición de un álbum repetido en el setup'''
   resultado = self.coleccion.agregar_album(titulo = "Corazones", anio = 1985, descripcion = "No tiene", medio = Medio.CASETE)
   self.assertNotEqual(resultado, True)

Ya se probó el método agregar_album, sin embargo, se utilizó el método para verificar su funcionamiento que se emplea para verificar valores específicos. Existen los métodos assertFalse y assertTrue para verificar que un valor sea False o True respectivamente. Para ver su funcionamiento, se utilizará el método editar_album, el cual también retorna estos valores. En un primer caso se verificará que edite un valor de manera que un álbum no quede repetido, y en un segundo caso, que se intente editar un valor y dejarlo repetido. Se puede observar que en un mismo método se puede hacer más de una verificación si es necesario (Aunque no siempre es recomendable):

def test_editar_album(self):
   '''Prueba la edición de dos álbumes'''
   #Se cambia el título el primer álbum creado por uno que no existe
   resultado1 = self.coleccion.editar_album(album_id = 1, titulo = "Corazones Remastered", anio = 1985, descripcion = "No tiene", medio = Medio.CASETE)

   #Se cambia el título del segundo álbum creado por uno que ya existe
   resultado2 = self.coleccion.editar_album(album_id = 2, titulo = "Pateando piedras", anio = 1985, descripcion = "No tiene", medio = Medio.CASETE)

   self.assertTrue(resultado1)
   self.assertFalse(resultado2)

En las pruebas se puede verificar si dos objetos son el mismo, en este caso, revisar que album1 no es el mismo objeto que uno que se acaba de recuperar con el método dar_album_por_id así sus propiedades sean iguales, y que solamente se considerarán el mismo objeto si guardan la misma referencia. Para esto se duplicará la referencia a album1 y se verificará que no sea el mismo álbum así:

def test_albumes_iguales(self):
   '''Prueba si dos álbumes son la misma referencia a un objeto'''
   album_nuevo = self.album1
   album_recuperado = self.coleccion.dar_album_por_id(1)
   self.assertIs(album_nuevo, self.album1)
   self.assertIsNot(album_recuperado, self.album1)

En las pruebas se puede verificar si un objeto está contenido en un conjunto, en este caso, se creará un conjunto con los álbumes 1, 2 y 3, y se verificará si el album1 y el album4 están en ese conjunto, de la siguiente manera:

def test_elemento_en_conjunto(self):
   '''Prueba que un elemento se encuentre en un conjunto'''
   conjunto = [self.album1, self.album2, self.album3]
   self.assertIn(self.album1, conjunto)
   self.assertNotIn(self.album4, conjunto)

Las pruebas también pueden verificar si un objeto es de una clase, para esto se verificará que album1 sea de la clase Album, y coleccion no es de dicha clase, así:

def test_instancia_clase(self):
   '''Prueba que un elemento sea de una clase'''
   self.assertIsInstance(self.album1, Album)
   self.assertNotIsInstance(self.coleccion, Album)

En las pruebas se puede revisar si la información almacenada en la base de datos quedó guardada correctamente. Para esto la prueba almacena la información con el método agregar_album, y luego se recupera la información consultando y filtrando los datos almacenados. Con los assert se verifica que los datos almacenados son los correctos, como se ven en la siguiente prueba:

def test_verificar_almacenamiento_agregar_album(self):
   '''Verifica que al almacenar los datos queden guardados en la el almacenamiento'''
   self.coleccion.agregar_album(titulo="Signos", anio=1986, descripcion="No tiene", medio=Medio.DISCO)

   self.session = Session()
   album = self.session.query(Album).filter(Album.titulo == 'Signos' and Album.medio == Medio.DISCO).first()

   self.assertEqual(album.titulo, 'Signos')
   self.assertEqual(album.ano, 1986)

Al ejecutar los casos con la instrucción python -m unittest discover -s tests -v, se verá un mensaje como el siguiente:

test_agregar_album (test_album.AlbumTestCase)
Prueba la adición de un álbum ... ok
test_agregar_album_repetido (test_album.AlbumTestCase)
Prueba la adición de un álbum repetido en el setup ... ok
test_albumes_iguales (test_album.AlbumTestCase)
Prueba si dos álbumes son la misma referencia a un objeto al recuperar un album del almacenamiento ... ok
test_editar_album (test_album.AlbumTestCase)
Prueba la edición de dos álbumes ... ok
test_elemento_en_conjunto (test_album.AlbumTestCase)
Prueba que un elemento se encuentre en un conjunto ... ok
test_instancia_clase (test_album.AlbumTestCase)
Prueba que un elemento sea de una clase ... ok
test_verificar_almacenamiento_agregar_album (test_album.AlbumTestCase)
Verifica que al almacenar los datos queden guardados en la el almacenamiento ... ok

-------------------------------------------------------------------
Ran 7 tests in 3.201s

OK

Antes de realizar las pruebas, se recomienda enunciar los casos de prueba, es decir, una lista de pruebas a realizar, con las situaciones a verificar y los resultados esperados. Para los casos del apartado anterior, la lista de pruebas corresponde a cada ejemplo, posteriormente, se diseñaron las pruebas a partir de situaciones con los datos de objetos de la clase Album, y luego los resultados esperados con los assert. Cuando no se cuenta con estas lista y estos casos, las pruebas pueden tomar mucho tiempo, al no tener claridad sobre qué se quiere probar. Los criterios de aceptación son, en muchos casos, una fuente de casos de prueba.

Al momento de realizar las pruebas, una buena práctica consiste en utilizar generadores aleatorios de datos para garantizar la posibilidad de poner a prueba nuestros desarrollos con más casos y más valores.

En esta sección vamos a implementar las pruebas anteriores usando Faker, una librería de Python que genera valores aleatorios. Usaremos Faker para crear una versión idéntica de las pruebas anteriores que generen 10 usuarios distintos sin preocuparnos por los valores.

Para instalar Faker basta con usar pip:

pip install faker

Para utilizar Faker en las pruebas se debe importar al archivo de pruebas de la siguiente manera:

from faker import Faker

Adicionalmente se va a añadir la librería random para elegir de manera aleatoria los medios de la enumeración Medio:

import random

En este punto ya se puede utilizar faker. Lo primero es modificar el método setUp para instanciar a Faker y crear 10 álbumes con sus títulos, años y descripciones generados aleatoriamente, y un medio elegido aleatoriamente:

def setUp(self):
   '''Crea una colección para hacer las pruebas'''
   self.coleccion = Coleccion()

   '''Abre la sesión'''
   self.session = Session()

   '''Crea una isntancia de Faker'''
   self.data_factory = Faker()

   '''Se programa para que Faker cree los mismos datos cuando se ejecuta'''
   Faker.seed(1000)

   '''Genera 10 datos en data y creamos los álbumes'''
   self.data = []
   self.albumes = []
   self.medios = [Medio.CD, Medio.CASETE, Medio.DISCO]

   for i in range(0,10):
       self.data.append((
           self.data_factory.unique.name(),
           self.data_factory.random_int(1800,2021),
           self.data_factory.text(),
           random.choice(self.medios)))
       self.albumes.append(
           Album(
               titulo = self.data[-1][0],
               ano = self.data[-1][1],
               descripcion = self.data[-1][2],
               medio = self.data[-1][3],
               canciones = []
           ))
       self.session.add(self.albumes[-1])


   '''Persiste los objetos
       En este setUp no se cierra la sesión para usar los albumes en las pruebas'''
   self.session.commit()

Se puede notar que para los nombres se utilizó un generador de nombres de personas, que puede simular los nombres de los álbumes. La sentencia cuenta con la palabra unique, esto permitirá tener nombres únicos en el título. También se debe anotar que no se cerró la sesión en el setup, esto es con el propósito de acceder a los objetos de la sesión en las pruebas posteriores.

Con esto, se puede definir un test_constructor para que verifique los datos que los datos generados aleatoriamente son los que se usaron para cada crear cada álbum en la lista de álbumes:

def test_constructor(self):
   for album, dato in zip(self.albumes, self.data):
       self.assertEqual(album.titulo, dato[0])
       self.assertEqual(album.ano, dato[1])
       self.assertEqual(album.descripcion, dato[2])
       self.assertEqual(album.medio, dato[3])

La prueba test_agregar_album se puede modificar para adicionar un álbum con nuevos datos aleatorios que no estarán repetidos en la colección, conforme con las validaciones que tiene el método agregar_album. Estos datos aleatorios no están repetidos debido al uso del unique al momento de crearlos, y a que con el seed podemos tener siempre los mismos datos en el mismo orden. Para esta prueba se adiciona un nuevo álbum agregando un nuevo dato al conjunto de datos y se prueba el método, como se muestra a continuación:

def test_agregar_album(self):
   '''Prueba la adición de un álbum'''
   self.data.append((self.data_factory.unique.name(), self.data_factory.random_int(1800, 2021), self.data_factory.text(), random.choice(self.medios)))

   resultado = self.coleccion.agregar_album(
       titulo = self.data[-1][0],
       anio = self.data[-1][1],
       descripcion = self.data[-1][2],
       medio = self.data[-1][3])
   self.assertEqual(resultado, True)

Se puede probar también que con los datos ya agregados la adición de un álbum repetido falla, como en el siguiente caso:

def test_agregar_album_repetido(self):
   '''Prueba la adición de un álbum repetido en el setup'''
   resultado = self.coleccion.agregar_album(
       titulo = self.data[-1][0],
       anio = self.data[-1][1],
       descripcion = self.data[-1][2],
       medio = self.data[-1][3])
   self.assertNotEqual(resultado, True)

Se puede probar la edición de los álbumes también intentando editar un álbum con un dato nuevo y otro con un dato repetido, como se muestra a continuación:

def test_editar_album(self):
   '''Prueba la edición de dos álbumes'''
   self.data.append((self.data_factory.unique.name(), self.data_factory.random_int(1800, 2021), self.data_factory.text(), random.choice(self.medios)))

   #Se cambia el título el primer álbum creado por uno que no existe
   resultado1 = self.coleccion.editar_album(
       album_id = 1,
       titulo = self.data[-1][0],
       anio = self.data[-1][1],
       descripcion = self.data[-1][2],
       medio = self.data[-1][3])

   #Se cambia el título del segundo álbum creado por uno que ya existe
   resultado2 = self.coleccion.editar_album(
       album_id = 2,
       titulo = self.data[-3][0],
       anio = self.data[-3][1],
       descripcion = self.data[-3][2],
       medio = self.data[-3][3])

   self.assertTrue(resultado1)
   self.assertFalse(resultado2)

También se puede redefinir el test_albumes_iguales, no requiere mayor trabajo, e implica buscar elementos en la lista de álbumes. Esta prueba funcionará siempre que se tenga más de un álbum en la lista de álbumes.

def test_albumes_iguales(self):
   '''Prueba si dos álbumes son la misma referencia a un objeto al recuperar un album del almacenamiento'''
   album_nuevo = self.albumes[0]
   album_recuperado = self.coleccion.dar_album_por_id(1)
   self.assertIs(album_nuevo, self.albumes[0])
   self.assertIsNot(album_recuperado, self.albumes[0])

De igual manera se usa de nuevo la fábrica de datos para crear un elemento para test_elemento_en_conjunto y comprobar si se encuentra o no:

def test_elemento_en_conjunto(self):
   '''Prueba que un elemento se encuentre en un conjunto'''
   album_nuevo = Album(
               titulo = self.data_factory.unique.name(),
               ano = self.data_factory.random_int(1800, 2021),
               descripcion = self.data_factory.text(),
               medio = random.choice(self.medios),
               canciones = []
           )

   album_existente = self.albumes[2]

   self.assertIn(album_existente, self.albumes)
   self.assertNotIn(album_nuevo, self.albumes)

Así mismo, para verificar si un objeto es una instancia de la clase se puede usar la lista generada inicialmente:

def test_instancia_clase(self):
   '''Prueba que un elemento sea de una clase'''
   self.assertIsInstance(self.albumes[0], Album)
   self.assertNotIsInstance(self.coleccion, Album)

Y finalmente las pruebas sobre la base de datos usando SQLAlchemy:

def test_verificar_almacenamiento_agregar_album(self):
   '''Verifica que al almacenar los datos queden guardados en la el almacenamiento'''
   self.data.append((self.data_factory.unique.name(), self.data_factory.random_int(1800, 2021), self.data_factory.text(), random.choice(self.medios)))

   self.coleccion.agregar_album(
       titulo = self.data[-1][0],
       anio = self.data[-1][1],
       descripcion = self.data[-1][2],
       medio = self.data[-1][3])

   album = self.session.query(Album).filter(Album.titulo == self.data[-1][0] and Album.ano == self.data[-1][1]).first()

   self.assertEqual(album.titulo, self.data[-1][0])
   self.assertEqual(album.ano, self.data[-1][1])
   self.assertEqual(album.descripcion, self.data[-1][2])
   self.assertIn(album.medio, self.medios)

Al ejecutar los casos con la instrucción python -m unittest discover -s tests -v con la generación de datos aleatorios correctamente obtendremos entonces:

test_agregar_album (test_album.AlbumTestCase)
Prueba la adición de un álbum ... ok
test_agregar_album_repetido (test_album.AlbumTestCase)
Prueba la adición de un álbum repetido en el setup ... ok
test_albumes_iguales (test_album.AlbumTestCase)
Prueba si dos álbumes son la misma referencia a un objeto al recuperar un album del almacenamiento ... ok
test_constructor (test_album.AlbumTestCase) ... ok
test_editar_album (test_album.AlbumTestCase)
Prueba la edición de dos álbumes ... ok
test_elemento_en_conjunto (test_album.AlbumTestCase)
Prueba que un elemento se encuentre en un conjunto ... ok
test_instancia_clase (test_album.AlbumTestCase)
Prueba que un elemento sea de una clase ... ok
test_verificar_almacenamiento_agregar_album (test_album.AlbumTestCase)
Verifica que al almacenar los datos queden guardados en la el almacenamiento ... ok

--------------------------------------------------------------------
Ran 8 tests in 4.450s

OK

Coverage.py es una herramienta que monitorea el código y analiza cuáles partes han sido ejecutadas. Generalmente se utiliza en los procesos de pruebas unitarias para determinar el porcentaje de código que estas cubren.

Para instalar esta herramienta se debe ejecutar la siguiente instrucción:

pip install coverage

Antes de iniciar, se deben ajustar un archivo de configuración de cobertura que trae el proyecto, con el propósito de indicar en cual sitio se ubican las pruebas y qué partes del código se deben o no incluir en el reporte de cobertura. Este archivo se llava .coveragerc, se encuentra en la raíz del proyecto y tiene varias secciones que permiten cambiar las propiedades acerca de la cobertura.

Para el caso de este tutorial, solo se modificará la primera sección denominada [run], en la que se definen las propiedades de ejecución de la cobertura. En esta sección se debe modificar, primero, la fuente donde se hará la cobertura del código. Esta fuente se identifica por la variable source y debe quedar asignada a la carpeta src de la siguiente manera:

source = src

Adicionalmente, en la misma sección, en la variable omit se deben especificar en líneas cuáles serán los archivos o carpetas que no se tendrán en la cobertura del código. En este caso se omite el archivo __init__.py que indica que logica es un paquete, se omite también todo lo que está en el paquete vista, ya que esto no está sujeto a pruebas, y se omite también el paquete modelo, debido a que se le quitaron los métodos para simplificar el tutorial de pruebas unitarias y para no probar todo el paquete de SQLAlchemy. En ese sentido, solamente se ejecutará la cobertura de código sobre el paquete logica, y se omite el resto con las siguientes definiciones:

omit =
   src\logica\__init__.py
   src\modelo\*
   src\vista\*

Para ejecutar coverage, se cambia python por coverage run en la instrucción que permite ejecutar las pruebas. Por ejemplo, si las pruebas se ejecutan con la siguiente instrucción:

python -m unittest discover -s tests -v

Para verificar la cobertura de código se utiliza la siguiente instrucción:

coverage run -m unittest discover -s tests -v

En la salida, se verá el resultado de ejecución de las pruebas como si se hubiera ejecutado el comando python para correr las pruebas, pero, luego de ejecutar coverage, se puede generar un reporte por pantalla del porcentaje de código cubierto con la siguiente instrucción:

coverage report -m

Al ejecutarla, se mostrará un reporte por pantalla similar al siguiente:

Name                     Stmts   Miss Branch BrPart  Cover Missing
------------------------------------------------------------------
src\logica\coleccion.py    28      0      4      0   100%
------------------------------------------------------------------
TOTAL                      28      0      4      0   100%

Este reporte muestra por cada línea la siguiente información:

En este caso, las pruebas cubren todo el código, sin embargo, si se quiere ver qué aparece cuando el código no es totalmente cubierto, primero, se debe borrar la información de la cobertura de las pruebas con la siguiente instrucción:

coverage erase

Luego, se puede borrar el método de pruebas llamado test_editar_album del archivo test_album.py, se ejecutan nuevamente las pruebas con la instrucción coverage run -m unittest discover -s tests -v, y, finalmente, se genera de nuevo el reporte con coverage report -m, para obtener el siguiente reporte:

Name                    Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------------------
src\logica\coleccion.py    28     10      4      0    62%   23-33
-------------------------------------------------------------------
TOTAL                      28     10      4      0    62%

En el reporte se observa que en el archivo coleccion.py contiene 28 instrucciones, de las cuales 10 no se están ejecutando, lo que da un 62% de cobertura. Las líneas que no se ejecutarían con las pruebas son las líneas eliminadas al quitar el método de pruebas.

Sin embargo, si se quieren verificar las líneas no que no han sido ejecutadas en un formato que no sea por consola, se puede utilizar la siguiente instrucción:

coverage html

Esta instrucción genera una carpeta llamada htmlcov con un archivo index.html con el reporte de la cobertura total, similar a la salida de la consola, y otros en formato html por cada archivo de salida de la cobertura de código realizada por cada archivo revisado. En archivo index.html se ve de la siguiente manera:

Para ver el reporte por cada archivo de salida de cobertura se pueden encontrar diferentes archivos en formato html. Por ejemplo, el archivo llamado htmlcov/src_logica_coleccion_py.html corresponde a los resultados del reporte sobre logica\coleccion.py. Al abrir el archivo en formato html se puede observar la cobertura de la siguiente manera:

El archivo en formato html muestra a la izquierda los números de línea de la clase o módulo, al frente del número de línea muestra una barra verde o roja que identifica una línea de código ejecutable y a continuación de cada barra la línea de código. Cuando la barra es de color verde, indica que la línea de código fue ejecutada durante las pruebas, y cuando es roja, indica que esta no se ejecutó. El archivo html muestra también, en la parte superior, la información general del archivo y el resumen de la cantidad de líneas que tiene, las ejecutadas y no ejecutadas, así como las excluidas.