▷ Pytest desde cero — Tu primer test en Python en 10 minutos 2026

Pytest desde cero — Tu primer test en Python en 10 minutos

Si llevas un tiempo programando, sabes que escribir tests debería ser parte normal del trabajo. Pero también sabes que casi nunca lo es. La excusa habitual: “no tengo tiempo”, “es código de juguete”, “ya lo testeo a mano cuando ejecuto”.

Con pytest, escribir tests es literalmente más rápido que probar a mano. Defines un test una vez, lo lanzas con un comando, y cada vez que cambias el código sabes en segundos si algo se rompió. Una vez te acostumbras, no entiendes cómo trabajabas antes sin esto.

En esta entrada te enseño a escribir tu primer test, las funcionalidades clave (fixtures, parametrize, mocks), qué testear y qué no, y los errores típicos cuando empiezas. En 10 minutos te montas tu primer test y entiendes el patrón para todo lo demás.

Instalación

pip install pytest

💡 ¿Aún no usas entornos virtuales? venv y pip sin liarte. Cinco comandos que te ahorran problemas.

Listo. pytest viene con todo lo básico que necesitas.

Tu primer test

Estructura mínima de un proyecto:

mi_proyecto/
├── src/
│   └── calculadora.py
└── tests/
    └── test_calculadora.py
# src/calculadora.py
def sumar(a, b):
    return a + b

def restar(a, b):
    return a - b
# tests/test_calculadora.py
from src.calculadora import sumar, restar

def test_sumar_dos_positivos():
    assert sumar(2, 3) == 5

def test_sumar_con_cero():
    assert sumar(5, 0) == 5

def test_restar():
    assert restar(10, 4) == 6

Ejecutar:

$ pytest
============= test session starts =============
collected 3 items

tests/test_calculadora.py ...                [100%]

============= 3 passed in 0.02s =============

Tres puntos = tres tests pasaron. Una F sería un fallo. Eso es todo lo que pytest necesita: funciones cuyo nombre empiece por test_ y assert para verificar.

Convenciones que pytest descubre solo

pytest busca tests automáticamente:

  • Ficheros que empiezan por test_ o terminan en _test.py.
  • Funciones dentro que empiezan por test_.
  • Clases que empiezan por Test (sin __init__).

No hace falta configurar nada. Lanzas pytest desde la raíz del proyecto y descubre todo.

📥 Llévate el cheatsheet de Python (gratis)

PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.

Aserciones con assert plano

Una de las grandes ventajas de pytest sobre unittest: usas el assert normal de Python. Y cuando falla, te muestra exactamente qué falló:

def test_resta():
    assert restar(10, 4) == 7   # ← está mal, debería ser 6

# Output:
# >       assert restar(10, 4) == 7
# E       assert 6 == 7
# E        +  where 6 = restar(10, 4)

Te muestra el valor real (6) y el esperado (7). Sin necesidad de assertEqual, assertTrue, assertGreater… del unittest tradicional.

Otros asserts útiles:

def test_listas():
    assert sumar(1, 2) > 0
    assert "ana" in ["ana", "luis"]
    assert isinstance(sumar(1, 2), int)
    assert len([1, 2, 3]) == 3

Esperar excepciones

Cuando quieres comprobar que un código lanza una excepción concreta:

import pytest

def dividir(a, b):
    return a / b

def test_dividir_por_cero():
    with pytest.raises(ZeroDivisionError):
        dividir(10, 0)

pytest.raises(Exception) es un context manager que verifica que el bloque levanta esa excepción. Si no la levanta, el test falla.

Para verificar el mensaje:

def test_dividir_por_cero_mensaje():
    with pytest.raises(ZeroDivisionError, match="division by zero"):
        dividir(10, 0)

parametrize — un test, muchos casos

Aquí es donde pytest brilla. Para correr el mismo test con datos distintos:

import pytest

@pytest.mark.parametrize("a, b, esperado", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (10, -5, 5),
])
def test_sumar(a, b, esperado):
    assert sumar(a, b) == esperado

Esto crea 4 tests separados automáticamente. Si uno falla, ves exactamente cuál — los otros 3 siguen ejecutándose.

tests/test_calculadora.py::test_sumar[2-3-5] PASSED
tests/test_calculadora.py::test_sumar[0-0-0] PASSED
tests/test_calculadora.py::test_sumar[-1-1-0] PASSED
tests/test_calculadora.py::test_sumar[10--5-5] PASSED

💡 Patrón pro: parametrize con todos los casos edge (negativo, cero, valores grandes, vacío…). Una sola función de test cubre 10 escenarios.

Fixtures — preparar contexto reutilizable

Una fixture es código que prepara datos o estado para un test. Se decora con @pytest.fixture:

import pytest

@pytest.fixture
def usuario_admin():
    return {"id": 1, "nombre": "Ana", "es_admin": True}

def test_usuario_es_admin(usuario_admin):
    assert usuario_admin["es_admin"]

def test_usuario_tiene_id(usuario_admin):
    assert "id" in usuario_admin

pytest ve que test_usuario_es_admin recibe un argumento usuario_admin y busca una fixture con ese nombre. Llama a la función decorada, le pasa el resultado al test.

Tip-friki: las fixtures pueden depender de otras fixtures. Cadenas de fixtures que preparan BBDD, sesiones, mocks… son lo que mantiene los tests limpios en proyectos grandes.

💡 Las fixtures son decoradores. ¿Quieres entender decoradores a fondo? Decoradores en Python explicados.

Fixture con setup y teardown

Si tu fixture tiene que limpiar al final, usa yield:

@pytest.fixture
def db_temporal():
    conn = abrir_db_temp()
    yield conn               # ← lo que recibe el test
    conn.close()             # ← se ejecuta después del test

Mismo patrón que context managers con yield.

Mockear con monkeypatch y unittest.mock

A veces tienes que simular dependencias externas (APIs, BBDD, fechas):

def saludo_de_dia():
    from datetime import datetime
    if datetime.now().hour < 12:
        return "Buenos días"
    return "Buenas tardes"


def test_buenos_dias(monkeypatch):
    from datetime import datetime

    class MockDateTime:
        @classmethod
        def now(cls):
            class FakeNow: hour = 9
            return FakeNow()

    monkeypatch.setattr("datetime.datetime", MockDateTime)
    assert saludo_de_dia() == "Buenos días"

Más común con unittest.mock.patch (también funciona en pytest):

from unittest.mock import patch

def test_descargar_url():
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"data": [1, 2, 3]}
        resultado = mi_funcion_que_llama_api()
    assert resultado == [1, 2, 3]

patch sustituye requests.get por un mock durante el bloque. Cuando sale del with, restaura.

Qué testear (y qué no)

Testea:

  • Lógica de negocio. Cualquier función con cálculos, decisiones, transformaciones.
  • Edge cases. Valores en los límites, vacíos, negativos, máximos.
  • Bugs encontrados. Cuando arregles uno, escribe el test que lo capture. No volverá.
  • Funciones puras. Las que no dependen de I/O y devuelven el mismo resultado para los mismos inputs.

No testees obsesivamente:

  • Frameworks ajenos. No testees que requests.get funciona — Django, Flask, requests ya tienen sus tests.
  • Trivialidades. def get_nombre(self): return self.nombre no necesita test.
  • UI/CSS. Los tests E2E (con Playwright/Selenium) son otra historia y son caros.

💡 La regla práctica: 80/20. El 20% de tu código (la lógica de negocio) tiene el 80% de los bugs. Testea eso bien y deja el resto a tests integration cuando los necesites.

Casos reales típicos

Test parametrizado de validación

import pytest

def es_email_valido(email):
    return "@" in email and "." in email.split("@")[1]


@pytest.mark.parametrize("email, esperado", [
    ("ana@gmail.com", True),
    ("info@elpythonista.com", True),
    ("sin-arroba", False),
    ("sin@dominio", False),
    ("", False),
])
def test_validar_email(email, esperado):
    assert es_email_valido(email) == esperado

Fixture que crea fichero temporal y lo limpia

import pytest
from pathlib import Path

@pytest.fixture
def fichero_temporal(tmp_path):
    ruta = tmp_path / "datos.txt"
    ruta.write_text("contenido inicial", encoding="utf-8")
    return ruta

def test_leer_fichero(fichero_temporal):
    contenido = fichero_temporal.read_text(encoding="utf-8")
    assert contenido == "contenido inicial"

tmp_path es una fixture que pytest da gratis: una Path a un directorio temporal único por test, que pytest borra después.

Marcadores para tests lentos

import pytest

@pytest.mark.slow
def test_consulta_costosa():
    ...

# Lanzar solo los rápidos:
# pytest -m "not slow"

# Solo los lentos:
# pytest -m slow

Útil cuando algunos tests tardan minutos: no los corres en cada pytest, solo en CI o cuando lanzas pytest -m slow.

Configuración mínima en pyproject.toml

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --tb=short"

markers = [
    "slow: tests que tardan más de 1 segundo",
    "integration: tests que requieren BBDD o red",
]

-v muestra cada test individual. --tb=short corta tracebacks largos. Con esto pytest queda limpio.

Errores típicos al empezar

# 1. Olvidar que los nombres deben empezar por test_
def comprueba_suma():    # ❌ pytest no lo descubre
    assert sumar(2, 3) == 5

def test_suma():         # ✓
    assert sumar(2, 3) == 5

# 2. Tests que dependen del orden
contador = 0

def test_uno():
    global contador
    contador += 1
    assert contador == 1

def test_dos():
    assert contador == 1   # ❌ depende de que test_uno corriera antes
# Cada test debe ser independiente.

# 3. Tests que usan datos reales (BBDD productiva, APIs externas)
def test_login():
    requests.post("https://api.real.com/login", ...)   # ❌ rompe la API real
# Mockea con `patch` o usa una BBDD de test.

# 4. Olvidar limpieza al usar fixtures
@pytest.fixture
def fichero():
    f = open("temporal.txt", "w")
    return f
# ❌ no cierra. Usa yield + close, o `tmp_path`.

# 5. assert con mensaje custom raro
assert sumar(2, 3) == 5, "esto debería ser 5"
# pytest ya muestra el valor real. El mensaje custom suele estorbar.

# 6. Tests demasiado grandes (10 asserts en uno)
def test_todo():
    assert sumar(2, 3) == 5
    assert restar(5, 3) == 2
    assert multiplicar(2, 4) == 8
    ...
# ❌ si falla el primero, no ves los demás. Divídelo en 3 tests.

Resumen

ConceptoSintaxis
Instalaciónpip install pytest
Test mínimodef test_xxx(): assert ...
Excepción esperadawith pytest.raises(ValueError): ...
Mismos test, varios datos@pytest.mark.parametrize("a, b", [...])
Setup reutilizable@pytest.fixture
Setup + teardownFixture con yield
Fichero temporal gratisFixture tmp_path
Mockearunittest.mock.patch o monkeypatch
Lanzarpytest (busca solo)
Verbosepytest -v
Por marcadorpytest -m slow
Configurar[tool.pytest.ini_options] en pyproject.toml

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.

Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.

Tu siguiente paso

Si has llegado hasta aquí, ya tienes lo necesario para escribir tests útiles desde la próxima función que escribas. Empezar es fácil: el primer test te llevará 5 minutos. Mantenerlos te lleva minutos al día. Y la primera vez que un test te avise de un bug que ibas a meter en producción, ya no te imaginas trabajando sin ellos.

Si quieres aprender Python desde la base hasta proyectos reales con tests, deploy y CI, en El Pythonista lo enseño paso a paso.

Un abrazo,
Oscar

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario