TechLead
Lección 18 de 25
5 min de lectura
Python

Testing en Python con pytest

Domina el testing en Python con pytest, fixtures, mocking y practicas de desarrollo guiado por tests

¿Por que pytest?

pytest es el framework de testing mas popular en el ecosistema Python. Ofrece una sintaxis simple, fixtures poderosas, mensajes de error informativos y un rico ecosistema de plugins. Puede ejecutar tests escritos para unittest y nose tambien, facilitando la migracion.

Escribiendo tus primeros tests

# Install pytest
# pip install pytest

# test_calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# Tests - just use assert!
def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

def test_add_floats():
    result = add(0.1, 0.2)
    assert result == pytest.approx(0.3)  # Handle float precision

def test_divide():
    assert divide(10, 2) == 5.0
    assert divide(7, 2) == 3.5

def test_divide_by_zero():
    import pytest
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

# Run tests:
# pytest                    # Run all tests
# pytest test_calculator.py # Run specific file
# pytest -v                 # Verbose output
# pytest -x                 # Stop on first failure
# pytest -k "test_add"      # Run tests matching pattern

Fixtures

Las fixtures proporcionan una forma de configurar datos y recursos de prueba. Son mas poderosas que los metodos setUp/tearDown porque son modulares, componibles y soportan inyeccion de dependencias.

import pytest

# Basic fixture
@pytest.fixture
def sample_users():
    return [
        {"name": "Alice", "age": 30, "role": "admin"},
        {"name": "Bob", "age": 25, "role": "user"},
        {"name": "Charlie", "age": 35, "role": "user"},
    ]

def test_user_count(sample_users):
    assert len(sample_users) == 3

def test_admin_exists(sample_users):
    admins = [u for u in sample_users if u["role"] == "admin"]
    assert len(admins) == 1

# Fixture with setup and teardown
@pytest.fixture
def temp_file(tmp_path):
    file_path = tmp_path / "test.txt"
    file_path.write_text("test data")
    yield file_path  # test runs here
    # Cleanup runs after test (automatic with tmp_path)

def test_read_file(temp_file):
    assert temp_file.read_text() == "test data"

# Fixture scopes
@pytest.fixture(scope="module")  # Created once per module
def db_connection():
    conn = create_connection()
    yield conn
    conn.close()

@pytest.fixture(scope="session")  # Created once per test session
def app_config():
    return {"debug": True, "db": "sqlite:///:memory:"}

# Parametrized fixtures
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_type(request):
    return request.param

def test_supported_database(database_type):
    assert database_type in ["sqlite", "postgresql", "mysql"]

Tests parametrizados

import pytest

# Run the same test with different inputs
@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("Python", "PYTHON"),
    ("", ""),
])
def test_uppercase(input, expected):
    assert input.upper() == expected

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
    (100, 200, 300),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

# Multiple parametrize decorators create combinations
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
    assert x * y > 0  # Tests: (1,10), (1,20), (2,10), (2,20)

Mocking

from unittest.mock import Mock, patch, MagicMock
import pytest

# Simple mock
def test_mock_basic():
    mock_db = Mock()
    mock_db.get_user.return_value = {"id": 1, "name": "Alice"}

    user = mock_db.get_user(1)
    assert user["name"] == "Alice"
    mock_db.get_user.assert_called_once_with(1)

# Patching external dependencies
# service.py
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/{city}")
    return response.json()

# test_service.py
@patch("service.requests.get")
def test_get_weather(mock_get):
    mock_response = Mock()
    mock_response.json.return_value = {"temp": 72, "condition": "sunny"}
    mock_get.return_value = mock_response

    result = get_weather("NYC")
    assert result["temp"] == 72
    mock_get.assert_called_once_with("https://api.weather.com/NYC")

# Context manager style
def test_get_weather_context():
    with patch("service.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"temp": 72}
        result = get_weather("NYC")
        assert result["temp"] == 72

# pytest-mock fixture (cleaner)
# pip install pytest-mock
def test_get_weather_mocker(mocker):
    mock_get = mocker.patch("service.requests.get")
    mock_get.return_value.json.return_value = {"temp": 72}
    result = get_weather("NYC")
    assert result["temp"] == 72

Puntos clave

  • Usa pytest: Sintaxis mas simple y mejor salida que unittest
  • Fixtures para setup: Modulares, componibles y soportan inyeccion de dependencias
  • Parametriza tests: Prueba muchas entradas con una sola funcion de test
  • Mockea dependencias externas: Aisla tus tests de redes, bases de datos y archivos

Continuar Aprendiendo