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:
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 :
"""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_esperadopytest 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
"""
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 salidaencode("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.
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:
venv/*
**/__pycache__
.vscode/
.coverageEl 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 posibleOcultar la solución
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"),