Test unitarios y cobertura de código en Python

En la presente entrada, Test unitarios y cobertura de código en Python, realizaré la descripción de cómo se realizan test unitarios en Python con unittest y, además, cómo se realizan el análisis del código para generar el índice de cobertura de código con la herramienta coverage.

Los ejemplos estarán realizados con la versión 3.6 de Python.

logo-python

Test unitarios

Los test unitarios los definimos utilizando el framework unittest el cual está incorporado en la distribución de la versión del lenguaje.

Para realizar un test de un código, iniciaremos la definición de un código al cual se definirán el conjunto de test a definir. Este proceso inicial lo realizaré para comprender el proceso.

Definiré una clase de utilidad Util con un método statusToCode cuya funcionalidad consistirá en parsear un parámetro alfanumérico de entrada y, como salida, retornará un valor entero.

El snippet de la clase es la siguiente:

class Util:
  """Class utils."""
  @staticmethod
  def statusToCode(code="") -> int:
    """Return exit code"""
    assert len(code) > 0, "Argument not valid"
    result = {
      'UP': 0,
      'WARNING': 1,
      'CRITICAL': 2,
      'UNKOWN': 3,
    }.get(code, 3)
    return result

La clase Util está definida en el módulo util.py dentro de la carpeta lib. La función statusToCode tiene un decorador definido con nombre @staticmethod el cual permite definir el método en la clase con referencia estática para poderlo utilizar sin la necesidad de instanciar la clase.

Los test los definiremos en la carpeta lib_test la cual está definida al mismo nivel que la clase lib. Definiré las clases de test conforme a los módulos definidos. Así, tendremos la clase UtilTest en el módulo test_utils.py de la carpeta lib_test.

La clase UtilTest deberá de importar el módulo unittest y definir la clase heredando de la clase unittest.TestCase para poder realizar los test. Además, deberá de importar la clase con el código que se desea probar. Así, la clase queda definida de la siguiente forma:

import unittest
from lib.utils import Util

class UtilTests(unittest.TestCase):
  def setUp(self):
    pass

  def test_statusToCode_EMPTY(self):
    try:
      print(sys.executable)
      Util.statusToCode("")
    except AssertionError as exception:
      self.assertTrue(exception != None)

  def test_statusToCode_UP(self):
    self.assertEqual(Util.statusToCode("UP"), 0)

  def test_statusToCode_WARNING(self):
    self.assertEqual(Util.statusToCode("WARNING"), 1)

  def test_statusToCode_CRITICAL(self):
    self.assertEqual(Util.statusToCode("CRITICAL"), 2)

  def test_statusToCode_UNKNOWN(self):
    self.assertEqual(Util.statusToCode("UNKNOWN"), 3)

La clases UtilTests presenta seis métodos: el método setUp, el cual realiza la definición de las operaciones previas a la ejecución de los test, en nuestro caso no es necesario realizar ninguna; y, el resto de métodos, que definen los test al tener como prefijo la cadena «test_».

La verificación de los resultados se realiza empleando la referencia self la cual define las funciones de comprobación.

Cobertura

La cobertura de código la realizaremos con la herramienta coverage cuya referencia la pondremos en el fichero requirements.txt para que sea cargado en el entorno virtual del proyecto.

La herramienta coverage tiene la capacidad de realizar la generación de los informes por línea de comando, o bien, la generación de los informes en formato html; dichos informes, se generarán en la carpeta htmlcov del propio proyecto.

Los comandos que ejecutaremos son los siguientes:

  • coverage erase.- Eliminación de los datos previos de cobertura. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage erase
  • coverage run.- Arranque del programa Python que recolecta los datos. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage run –omit=’.tox/*,.venv/*’ -m unittest
  • coverage report.- Generación resultados. Un ejemplo de ejecución en la línea de comandos es el siguiente:  coverage report –omit=’.tox/*,venv/*’ -m
  • coverage html.- Generación de los informer en formato HTML. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage html –omit=’.tox/*,venv/*’

Un ejemplo de informe de cobertura tiene el siguiente aspecto:

python coverage html

Automatización del proceso de Cobertura

En el apartado anterior, he definido la forma de ejecutar los test y la Generación de los informes de cobertura y, en el presente apartado, realizaré la descripción de cómo lo podemos automatizar.

La automatización la realizamos empleando la herramienta tox(https://tox.readthedocs.io/en/latest/) Para poder utilizar tox, primeramente, es necerios definir en el fichero requirement.txt la herramienta tox; una vez instalado en el entorno virtual, deberemos definir el plan de ejecución de tox el cual se define en el fichero tox.ini ubicado en la carpeta raíz del proyecto.

El aspecto del fichero tox es el siguiente:

[tox]
envlist = py36, coverage-report
skipsdist = True

[testenv]
commands = python -m pytest {posargs}
deps =
-r{toxinidir}/requirements.txt
freezegun==0.3.9
pytest==3.5.0
passenv=*

[testenv:coverage-report]
skip_install = true
commands =
coverage erase
coverage run --omit='.tox/*,.venv/*' -m unittest
coverage report --omit='.tox/*,venv/*' -m
coverage html --omit='.tox/*,venv/*'

El fichero tox está compuesto de tres elementos de configuración los cuáles tienen la siguiente descripción:

  • Elemento tox: Definición del entorno virtual de ejecución, elemento a ejecutar y el flag de generación del artefacto para la distribución
  • Elemento testenv: Definición de la configuración necesaria por tox.
  • Elemento coverage-report: Definición de la secuencia de comandos de la herramienta coverage para el cálculo y generación de informes de cobertura.

Para ejecutar el proceso automático tecleamos en la línea de comando y posicionados en la carpeta de proyecto el comando tox.

Conclusión

El proceso de generación de test unitartios en Python es un proceso parecido a otros lenguajes como Java o Scala. Además, al estár el framework incorporado en la distribución no requiere de ninguna operación de carga, facilitando su uso.