Últimamente, he venido hablando bastante sobre test, hoy seguiré en esa misma línea, hablaremos un poco de coverage y porque un número de coberturas de pruebas altas no debería ser el objetivo principal por el cual se escriben test, de hecho incluso puede convertirse en un número que genere una falsa confianza, como escuche en un podcast debemos ver la cobertura como una herramienta y no como un objetivo, 100% de coverage y aun con bugs? Ver Figura 1
Coverage en pytest
en Python tenemos una herramienta llamada coverage, esta herramienta es un analizador de código enfocado a las pruebas, validando que cantidad de código ha sido testeado, sin embargo, cuando trabajamos con pytest tenemos una tool llamada pytes-cov que funciona como un wrapper esta coverage, agregando algunos beneficios adicionales, para instalar con poetry ejecutaremos el siguiente comando
poetry add pytest-cov
vamos a utilizar el siguiente código de contexto para la prueba, y ejecutar un test básico
y vamos a escribir los siguientes test para probar el código
ahora con este código de contexto, vamos a empezar a enmarcar algunas cosas relevantes, lo primero es ejecutar coverage para entender que cobertura tenemos, para ello usaremos poetry con el siguiente comando
poetry run pytest --cov-report term --cov=. tests/
y se obtiene el siguiente resultado
El resultado es un informe por consola donde la columna cover contiene un cálculo basado en los test ejecutados para entender que cantidad de líneas de código se está testeando, en promedio al final tenemos una cobertura del 93%, sin embargo, si revisamos el test solo tenemos un test de happy path y no se están probando diversas situaciones que pueden ocurrir, no hay en la literatura una cantidad de pruebas definidas, esa cantidad la define el programador que está trabajando en esa función porque es él, en ese momento quien más conoce que está haciendo y que otras situaciones podrían suceder. El paso seguir es agregar algunas configuraciones para garantizar que se cubren cierta cantidad de situaciones mínimas que pueden ocurrir en el código, pero como mencionaré más adelante, esto es solo una parte para garantizar esos casos
Poetry
gracias a poetry, podemos centralizar configuraciones para que los comandos se vuelvan algo más cortos, si no tuviéramos poetry tendríamos que tener un archivo .coverage, veamos un ejemplo
[tool.poetry]
name = "src"
version = "0.1.0"
description = ""
authors = ["Jairo Castañeda"]
[tool.poetry.dependencies]
python = "^3.8"
pydantic = "^1.9.0"
[tool.poetry.dev-dependencies]
pytest = "^7.1.1"
pytest-cov = "^3.0.0"
[tool.coverage.run]
branch = true
source = ['src']
omit = [
"tests/*",
""
]
[tool.coverage.report]
exclude_lines = [
'@(abc\.)?abstractmethod',
]
show_missing = true
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
hay dos secciones en las cuales no enfocaremos [tool.coverage.run] y [tool.coverage.report] describire los valores importantes, sin embargo, aquí pueden encontrar más opciones
- source hace referencia a los archivos y carpetas que queremos incluir en el coverage
- omit hace referencia a los archivos y folders que se van a omitir, la carpeta test/ por defecto se agrega a la cobertura, esta carpeta siempre va a tener un coverage de 100% lo cual afecta el promedio
- branch es un muy buen comando, permite agregar a la cobertura secciones de código que incluye if y else y que no se están probando por las diferentes condiciones
- show_missing permite mostrar que líneas de código no se han probado
- exclude_lines permite agregar líneas de código que no se tendrá en cuenta para la cobertura, permitiendo utilizar patrones con regex
vamos a ejecutar el comando con alguna modificación adicional para ver el reporte en un HTML
poetry run pytest --cov-report html --cov-report term --cov=. tests/
con este comando, además de generar el reporte por consola, ahora se genera un reporte en HTML, de la siguiente forma
Se puede ver que baja la cobertura, claramente al ser poco código, el bajón no es tan notable, sin embargo, vamos algo interesante, en el reporte en HTML, si yo le doy clic a cada Module, me lleva al siguiente pantalla
En la figura anterior tenemos cosas muy interesantes, sobre todo en color amarillo y rojo, el código marcado en color ojo indica que código no se está testeando, esto a razón de que solo se prueba el happy path, coverage indica que no se prueba la excepción, por lo tanto, hay un código sin testear, y de amarillo más interesante aún, es un código probado de forma parcial, es decir no se prueba del todo, esto porque existe un if y esa validación crea una rama de ejecución o un flujo y ese flujo no está siendo probado, veamos que pasa si cambio un poco el test de la siguiente manera
from src.create_user import CreateUser
from src.user import User
class TestCreateUser:
def test_run_success_response(self):
response = CreateUser.run(
name='Jairo',
last_name='Castaneda',
age=25,
extra_info={'City': 'Cucuta'}
)
assert isinstance(response, User)
def test_run_not_the_minimum_age(self):
response = CreateUser.run(
name='Jairo',
last_name='Castaneda',
age=18,
extra_info={'City': 'Cucuta'}
)
assert response is None
Se logra apreciar en la Figura 6, que al agregar un test con la edad que no es la mínima, se aumenta el porcentaje del test y se elimina la advertencia, por lo tanto, lo que se logra apreciar es que en un principio se tenía una gran cobertura, pero que solo cubría happy paths, después de las configuraciones sin tocar código, la cobertura baja, con esto se logra apreciar que el objetivo de la tool coverage es empoderar al programador para brindarle reportes sobre su código, estos reportes le debe permitir al programador entender más allá de solamente números en porcentajes, ya que estos números deben complementarse con un stack de test de integración, revisión de código, validaciones de smell code etc
Conclusiones
Coverage en Python es una herramienta que no es solo números, con las configuraciones adecuadas empodera al desarrollador/a para que logre realizar un proceso de casos de prueba unitarias exitosas, puesto que esto es un proceso creativo que requiere un gran entendimiento del problema, y al final del día el objetivo principal no debe estar enfocado a números de coberturas, sino un conjunto de validaciones compuestas de herramientas automáticas y revisiones de código realizadas por diferentes personas