Menos coverage, más herramientas

Ú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

Figura 1: 100% test coverage obtenido de https://medium.com/front-end-weekly/making-testable-javascript-code-2a71afba5120

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

from pydantic import ValidationError
from src.user import User
from typing import Union

MINIMUM_AGE = 18


class CreateUser:
    @staticmethod
    def run(name: str,
            last_name: str,
            age: int,
            extra_info: dict = None) -> Union[User, None]:
        user = None
        try:
            if age>MINIMUM_AGE:
                user = User(
                    name=name,
                    last_name=last_name,
                    age=age,
                    extra_info=extra_info)

        except ValidationError as error:
            print(str(error))

        return user
create_user.py
from src.user import User

users: list[User] = [
    User(name='Jairo', last_name='castaneda', age=25),
    User(name='Andres', last_name='Pacheco', age=25, extra_info={'City': 'Cucuta'})
]


class GetUsers:
    @staticmethod
    def run() -> list[User]:
        #get info database
        if len(users)==0:
            raise Exception("Users not found")
        return users

get_users.py
from pydantic import BaseModel
from typing import Optional


class User(BaseModel):
    name: str
    last_name: str
    age: int
    extra_info: Optional[dict]
user.py

y vamos a escribir los siguientes test para probar el código

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)
test_create_user.py
from src.get_users import GetUsers


class TestGetUsers:
    def test_run_success_response(self):
        users = GetUsers.run()
        assert isinstance(users,list)
test_get_users.py

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

Figura 2: Informe de coverage por consola

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

Figura 3: Informe por consola mejorado
Figura 4: Informe por WEB mejorado

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

Figura 5: Informe por WEB, revisión de código

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
Figura 6: Informe por WEB mejorado, revisión de código sin advertencias de 'partial'

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

¿Te gusto el post y quisieras poder aplicar lo aprendido?

Únete al equipo de Simetrik AQUI