Clase 06 · 30 minGratis

TDD con hash SHA-256 y cobertura

Clase 6 del curso gratis de buenas prácticas en Python: implementamos un hash SHA-256 con TDD de principio a fin, dejamos que GitHub Actions atrape los errores de estilo y medimos la cobertura de las pruebas con pytest-cov.

Toca cualquier marca ▸ 0:00 de los apuntes y el video salta a ese momento. Abrir en YouTube

Hash no es encriptación

Un hash también transforma un mensaje en algo ilegible, pero no tiene vuelta atrás: desde el hash no se puede recuperar el mensaje original. Por eso se usa para guardar contraseñas o verificar que un archivo no cambió. Y una propiedad curiosa : no importa qué tan largo sea el mensaje, el hash siempre mide lo mismo.

MD5 contra SHA-256

  • MD5 es más rápido, pero se ha demostrado que se pueden fabricar colisiones: otro archivo cualquiera que produce el mismo hash. Si tu contraseña se guardara así, un atacante no necesitaría saberla.
  • SHA-256 es más lento y produce un hash más largo, pero hasta hoy no se conoce una forma práctica de romperlo.

Nos quedamos con SHA-256.

TDD de principio a fin

Esta vez se sigue el orden estricto . Primero, solo el esqueleto de la función en app/funciones/hashing.py:

python
def text_hash(mensaje: str) -> str:
    return ""

Después, la prueba con siete ejemplos cuyo hash sacamos de una calculadora de SHA-256 en internet. Fíjate en que "Hola" y "hola" dan hashes completamente distintos :

python
"""Este modulo comprueba que se haga un buen hashing de los textos"""
import pytest
from app.funciones.hashing import text_hash


@pytest.mark.parametrize(
    "mensaje,hash_esperado",
    [
        ("Hola", "e633f4fc79badea1dc5db970cf397c8248bac47cc3acf9915ba60b5d76b0e88f"),
        ("A", "559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd"),
        ("a", "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"),
        ("1", "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"),
        ("A1b2", "2f8c6f88e15bba2d53d753346784a3f2fcba4472a078c08788072c68ecf3307d"),
        (
            "Este es un mensaje aleatorio",
            "5634339f53a8a1a83f86fc1a9d5d013515c4c7cb761752b31a6287038d62d59e",
        ),
        (
            "Mi clave es: 123@",
            "ac8d836ccfa8f549bced5792c73e8255d6ea74358e916e7e0fd6c2c7c582777c",
        ),
    ],
)
def test_text_hash(mensaje: str, hash_esperado: str):
    """Funcion para probar el hashing de texto

    Args:
        mensaje (str): Mensaje a ser hasheado
        hash_esperado (str): el hash correspondiente al mensaje
    """
    salida = text_hash(mensaje=mensaje)
    assert salida == hash_esperado

pytest responde: las 17 pruebas antiguas pasan y las 7 nuevas fallan . Ahora sí, a escribir la función hasta ponerlas en verde.

Formatea, no acomodes a mano. Clic derecho → Dar formato al documento en VS Code (con Black o autopep8) corrige espacios y saltos de línea . Pylint revisa; el formateador arregla.

La implementación

python
"""
Aqui se crea un modulo para obtener el hash de un string
"""
import hashlib


def text_hash(mensaje: str) -> str:
    """
    Esta función devuelve el hash con el agoritmo sha256 a 32bytes

    Args:
        mensaje (str): mensaje a ser hasheado

    Returns:
        str: cadena de texto con el hash del mensaje
    """
    entorno_hash = hashlib.sha256()
    entorno_hash.update(mensaje.encode("utf-8"))
    salida = entorno_hash.hexdigest()
    return salida
  • encode("utf-8") convierte el texto en bytes, con tildes y eñes incluidas.
  • hexdigest() devuelve el resultado en hexadecimal (0–9 y a–f); sin él verías bytes crudos como \x0b\x1f… .

Las siete pruebas pasan. Y para comprobar que las pruebas de verdad prueban algo, se daña un ejemplo a propósito ("11" en vez de "1") y falla .

Actions atrapa lo que se te escapó

En el computador todo pasaba, pero el pipeline sale en rojo en las tres versiones de Python: falta la línea vacía al final de hashing.py. Se corrige, y vuelve a fallar: ahora es Pylint sobre tests/, que pide el docstring de la prueba y protesta porque el argumento se llamaba hash, el nombre de una función propia de Python (W0622, redefined-builtin). Por eso el parámetro pasó a llamarse hash_esperado .

Ese es el valor del pipeline: nadie tuvo que acordarse de correr Pylint.

Cobertura: ¿cuánto de tu código prueban tus pruebas?

Así como Pylint te pone una nota de estilo, muchos equipos exigen un mínimo de cobertura: el porcentaje de líneas del código que se ejecutan durante las pruebas.

bash
pip install pytest-cov
pytest --cov=app tests/

El resultado : cifrado_cesar.py, encriptado.py y hashing.py al 100 %, main.py al 0 % (nada lo prueba todavía) y la carpeta app en total al 84 %. El comando deja un archivo oculto .coverage que cambia en cada corrida; va al .gitignore:

text
venv/*
**/__pycache__
.vscode/
.coverage

El reto de esta clase

Agrega dos casos a la prueba parametrizada: "hola" en minúsculas y el texto vacío "". Saca sus hashes con hashlib en una consola de Python, no de internet.

Ver una solución posible
python
import hashlib

print(hashlib.sha256("hola".encode("utf-8")).hexdigest())
print(hashlib.sha256("".encode("utf-8")).hexdigest())

# y en la lista de @pytest.mark.parametrize:
("hola", "b221d9dbb083a7f33428d7c2a3c3198ae925614d70210e28716ccaa7cd4ddb79"),
("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),

Sigue con la clase 7

Tu API con FastAPI y sus pruebas

Seguir el canal de WhatsApp