Estructura pruebas robustas con pytest: inyección de fixtures, teardown basado en yield, parametrización en forma de tabla y las trampas de los scopes amplios.
Abrir esta lección en Kodokonpytest se apoya en dos ideas potentes: el descubrimiento por convención (archivos test_*.py, funciones test_*) y el simple assert, cuya introspección muestra los valores reales de ambos lados cuando falla - se acabó memorizar assertEqual y sus treinta variantes. Dos herramientas que conviene conocer desde ya: pytest.raises para comprobar que efectivamente se lanza una excepción, y pytest.approx para comparar números de coma flotante sin pelearse con el redondeo binario.
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_nominal() -> None:
assert divide(8, 4) == 2.0
def test_zero_division() -> None:
with pytest.raises(ZeroDivisionError):
divide(1, 0)
def test_floats() -> None:
assert 0.1 + 0.2 == pytest.approx(0.3)Las fixtures son la inyección de dependencias de pytest: declara un parámetro que lleve el nombre de una fixture, y pytest la construye y la inyecta. El yield separa el setup del teardown - el código que va después se ejecuta incluso si la prueba falla. El parámetro scope (function por defecto, module, session) controla el tiempo de vida del objeto. Coloca las fixtures compartidas en un conftest.py: quedan disponibles en toda la carpeta, sin un solo import.
import sqlite3
import pytest
@pytest.fixture
def db():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
yield conn
conn.close()
def test_insert(db) -> None:
db.execute("INSERT INTO users VALUES ('Ada')")
query = "SELECT name FROM users"
rows = db.execute(query).fetchall()
assert rows == [("Ada",)]@pytest.mark.parametrize convierte una prueba en una tabla de casos: cada tupla se convierte en una prueba independiente, con nombre propio y filtrable de forma individual. Apila varios decoradores parametrize para obtener el producto cartesiano de los parámetros. Dale a tus casos id legibles mediante pytest.param(..., id='...') - un informe de fallo debería entenderse sin abrir el archivo fuente.
import pytest
def slugify(text: str) -> str:
return "-".join(text.lower().split())
@pytest.mark.parametrize(
("raw", "expected"),
[
("Hello World", "hello-world"),
(" Python rocks ", "python-rocks"),
pytest.param("", "", id="empty"),
],
)
def test_slugify(raw: str, expected: str) -> None:
assert slugify(raw) == expectedpytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5yield?pytest --lf?