Python Testing

作者 affaan-mef648e01899b无许可证275K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3天前更新

Estrategias de pruebas Python usando pytest, metodología TDD, fixtures, mocking, parametrización y requisitos de cobertura.

AI 生成的概览

指导使用 pytest 进行 Python 测试:TDD、fixture、参数化、mock、标记与覆盖率。

功能
该技能为使用 pytest 编写和组织 Python 测试提供参考指导。内容涵盖 TDD 红-绿-重构循环、断言、fixture 及其作用域、conftest.py 共享、参数化、自定义标记、mock 与 patch、异步测试、异常测试、tmp_path 用法以及测试目录结构。它还给出 pytest.ini 配置示例、覆盖率目标和命令行调用方式。
适用场景
适用于以测试先行方式编写新的 Python 代码、为 Python 项目设计测试套件、审查测试覆盖率,或搭建 pytest 基础设施与配置时。
运行要求
不附带脚本,仅为说明性内容。按示例操作需要安装 Python 与 pytest,覆盖率与异步测试可选安装 pytest-cov、pytest-asyncio 等插件。

Patrones de Pruebas Python

Estrategias completas de pruebas para aplicaciones Python usando pytest, metodología TDD y buenas prácticas.

Cuándo Activar

  • Escribir código Python nuevo (seguir TDD: rojo, verde, refactorizar)
  • Diseñar suites de pruebas para proyectos Python
  • Revisar la cobertura de pruebas Python
  • Configurar infraestructura de pruebas

Filosofía Central de Pruebas

Desarrollo Guiado por Pruebas (TDD)

Siempre seguir el ciclo TDD:

  1. ROJO: Escribir una prueba que falle para el comportamiento deseado
  2. VERDE: Escribir el código mínimo para que la prueba pase
  3. REFACTORIZAR: Mejorar el código manteniendo las pruebas en verde
python
# Paso 1: Escribir prueba fallida (ROJO)def test_add_numbers():    result = add(2, 3)    assert result == 5
# Paso 2: Escribir implementación mínima (VERDE)def add(a, b):    return a + b
# Paso 3: Refactorizar si es necesario (REFACTORIZAR)

Requisitos de Cobertura

  • Objetivo: 80%+ de cobertura de código
  • Rutas críticas: 100% de cobertura requerida
  • Usar pytest --cov para medir la cobertura
bash
pytest --cov=mypackage --cov-report=term-missing --cov-report=html

Fundamentos de pytest

Estructura Básica de Pruebas

python
import pytest
def test_addition():    """Prueba la suma básica."""    assert 2 + 2 == 4
def test_string_uppercase():    """Prueba la conversión a mayúsculas."""    text = "hello"    assert text.upper() == "HELLO"
def test_list_append():    """Prueba el append de lista."""    items = [1, 2, 3]    items.append(4)    assert 4 in items    assert len(items) == 4

Aserciones

python
# Igualdadassert result == expected
# Desigualdadassert result != unexpected
# Veracidadassert result  # Truthyassert not result  # Falsyassert result is True  # Exactamente Trueassert result is False  # Exactamente Falseassert result is None  # Exactamente None
# Membresíaassert item in collectionassert item not in collection
# Comparacionesassert result > 0assert 0 <= result <= 100
# Verificación de tipoassert isinstance(result, str)
# Prueba de excepción (enfoque preferido)with pytest.raises(ValueError):    raise ValueError("mensaje de error")
# Verificar mensaje de excepciónwith pytest.raises(ValueError, match="entrada inválida"):    raise ValueError("entrada inválida proporcionada")

Fixtures

Uso Básico de Fixtures

python
import pytest
@pytest.fixturedef sample_data():    """Fixture que proporciona datos de ejemplo."""    return {"name": "Alice", "age": 30}
def test_sample_data(sample_data):    """Prueba usando el fixture."""    assert sample_data["name"] == "Alice"    assert sample_data["age"] == 30

Fixture con Setup/Teardown

python
@pytest.fixturedef database():    """Fixture con setup y teardown."""    # Setup    db = Database(":memory:")    db.create_tables()    db.insert_test_data()
    yield db  # Proporcionar a la prueba
    # Teardown    db.close()
def test_database_query(database):    """Prueba operaciones de base de datos."""    result = database.query("SELECT * FROM users")    assert len(result) > 0

Alcances de Fixtures

python
# Alcance de función (por defecto) - se ejecuta por cada prueba@pytest.fixturedef temp_file():    with open("temp.txt", "w") as f:        yield f    os.remove("temp.txt")
# Alcance de módulo - se ejecuta una vez por módulo@pytest.fixture(scope="module")def module_db():    db = Database(":memory:")    db.create_tables()    yield db    db.close()
# Alcance de sesión - se ejecuta una vez por sesión de pruebas@pytest.fixture(scope="session")def shared_resource():    resource = ExpensiveResource()    yield resource    resource.cleanup()

Fixture con Parámetros

python
@pytest.fixture(params=[1, 2, 3])def number(request):    """Fixture parametrizado."""    return request.param
def test_numbers(number):    """La prueba se ejecuta 3 veces, una por cada parámetro."""    assert number > 0

Fixtures Autouse

python
@pytest.fixture(autouse=True)def reset_config():    """Se ejecuta automáticamente antes de cada prueba."""    Config.reset()    yield    Config.cleanup()
def test_without_fixture_call():    # reset_config se ejecuta automáticamente    assert Config.get_setting("debug") is False

Conftest.py para Fixtures Compartidos

python
# tests/conftest.pyimport pytest
@pytest.fixturedef client():    """Fixture compartido para todas las pruebas."""    app = create_app(testing=True)    with app.test_client() as client:        yield client
@pytest.fixturedef auth_headers(client):    """Genera cabeceras de autenticación para pruebas de API."""    response = client.post("/api/login", json={        "username": "test",        "password": "test"    })    token = response.json["token"]    return {"Authorization": f"Bearer {token}"}

Parametrización

Parametrización Básica

python
@pytest.mark.parametrize("input,expected", [    ("hello", "HELLO"),    ("world", "WORLD"),    ("PyThOn", "PYTHON"),])def test_uppercase(input, expected):    """La prueba se ejecuta 3 veces con diferentes entradas."""    assert input.upper() == expected

Múltiples Parámetros

python
@pytest.mark.parametrize("a,b,expected", [    (2, 3, 5),    (0, 0, 0),    (-1, 1, 0),    (100, 200, 300),])def test_add(a, b, expected):    """Prueba la suma con múltiples entradas."""    assert add(a, b) == expected

Parametrizar con IDs

python
@pytest.mark.parametrize("input,expected", [    ("[email protected]", True),    ("invalid", False),    ("@no-domain.com", False),], ids=["valid-email", "missing-at", "missing-domain"])def test_email_validation(input, expected):    """Prueba validación de email con IDs legibles."""    assert is_valid_email(input) is expected

Markers y Selección de Pruebas

Markers Personalizados

python
# Marcar pruebas lentas@pytest.mark.slowdef test_slow_operation():    time.sleep(5)
# Marcar pruebas de integración@pytest.mark.integrationdef test_api_integration():    response = requests.get("https://api.example.com")    assert response.status_code == 200
# Marcar pruebas unitarias@pytest.mark.unitdef test_unit_logic():    assert calculate(2, 3) == 5

Ejecutar Pruebas Específicas

bash
# Ejecutar solo pruebas rápidaspytest -m "not slow"
# Ejecutar solo pruebas de integraciónpytest -m integration
# Ejecutar pruebas de integración o lentaspytest -m "integration or slow"

Configurar Markers en pytest.ini

ini
[pytest]markers =    slow: marca pruebas como lentas    integration: marca pruebas como de integración    unit: marca pruebas como unitarias

Mocking y Patching

Mocking de Funciones

python
from unittest.mock import patch, Mock
@patch("mypackage.external_api_call")def test_with_mock(api_call_mock):    """Prueba con API externa mockeada."""    api_call_mock.return_value = {"status": "success"}
    result = my_function()
    api_call_mock.assert_called_once()    assert result["status"] == "success"

Mocking de Excepciones

python
@patch("mypackage.api_call")def test_api_error_handling(api_call_mock):    """Prueba manejo de errores con excepción mockeada."""    api_call_mock.side_effect = ConnectionError("Error de red")
    with pytest.raises(ConnectionError):        api_call()
    api_call_mock.assert_called_once()

Mocking de Context Managers

python
@patch("builtins.open", new_callable=mock_open)def test_file_reading(mock_file):    """Prueba lectura de archivo con open mockeado."""    mock_file.return_value.read.return_value = "contenido del archivo"
    result = read_file("test.txt")
    mock_file.assert_called_once_with("test.txt", "r")    assert result == "contenido del archivo"

Usar Autospec

python
@patch("mypackage.DBConnection", autospec=True)def test_autospec(db_mock):    """Prueba con autospec para detectar mal uso de API."""    db = db_mock.return_value    db.query("SELECT * FROM users")
    db_mock.assert_called_once()

Mock de Propiedades

python
@pytest.fixturedef mock_config():    """Crea un mock con una propiedad."""    config = Mock()    type(config).debug = PropertyMock(return_value=True)    type(config).api_key = PropertyMock(return_value="test-key")    return config

Pruebas de Código Asíncrono

Pruebas Async con pytest-asyncio

python
import pytest
@pytest.mark.asyncioasync def test_async_function():    """Prueba función async."""    result = await async_add(2, 3)    assert result == 5

Fixture Async

python
@pytest.fixtureasync def async_client():    """Fixture async que proporciona cliente de prueba async."""    app = create_app()    async with app.test_client() as client:        yield client

Pruebas de Excepciones

Probar Excepciones Esperadas

python
def test_divide_by_zero():    """Prueba que dividir por cero lanza ZeroDivisionError."""    with pytest.raises(ZeroDivisionError):        divide(10, 0)
def test_custom_exception():    """Prueba excepción personalizada con mensaje."""    with pytest.raises(ValueError, match="entrada inválida"):        validate_input("invalid")

Pruebas con tmp_path

python
def test_with_tmp_path(tmp_path):    """Prueba usando el fixture de ruta temporal de pytest."""    test_file = tmp_path / "test.txt"    test_file.write_text("hello world")
    result = process_file(str(test_file))    assert result == "hello world"    # tmp_path se limpia automáticamente

Organización de Pruebas

Estructura de Directorio

tests/├── conftest.py                 # Fixtures compartidos├── __init__.py├── unit/                       # Pruebas unitarias│   ├── __init__.py│   ├── test_models.py│   ├── test_utils.py│   └── test_services.py├── integration/                # Pruebas de integración│   ├── __init__.py│   ├── test_api.py│   └── test_database.py└── e2e/                        # Pruebas end-to-end    ├── __init__.py    └── test_user_flow.py

Clases de Prueba

python
class TestUserService:    """Agrupa pruebas relacionadas en una clase."""
    @pytest.fixture(autouse=True)    def setup(self):        """Setup se ejecuta antes de cada prueba en esta clase."""        self.service = UserService()
    def test_create_user(self):        """Prueba creación de usuario."""        user = self.service.create_user("Alice")        assert user.name == "Alice"
    def test_delete_user(self):        """Prueba eliminación de usuario."""        user = User(id=1, name="Bob")        self.service.delete_user(user)        assert not self.service.user_exists(1)

Buenas Prácticas

HACER

  • Seguir TDD: Escribir pruebas antes que el código (rojo-verde-refactorizar)
  • Probar una sola cosa: Cada prueba debe verificar un único comportamiento
  • Usar nombres descriptivos: test_user_login_with_invalid_credentials_fails
  • Usar fixtures: Eliminar duplicación con fixtures
  • Mockear dependencias externas: No depender de servicios externos
  • Probar casos borde: Entradas vacías, valores None, condiciones de frontera
  • Apuntar a 80%+ de cobertura: Enfocarse en rutas críticas
  • Mantener pruebas rápidas: Usar markers para separar pruebas lentas

NO HACER

  • No probar implementación: Probar comportamiento, no internos
  • No usar condicionales complejos en pruebas: Mantener pruebas simples
  • No ignorar fallos de prueba: Todas las pruebas deben pasar
  • No probar código de terceros: Confiar en que las bibliotecas funcionan
  • No compartir estado entre pruebas: Las pruebas deben ser independientes

Configuración de pytest

pytest.ini

ini
[pytest]testpaths = testspython_files = test_*.pypython_classes = Test*python_functions = test_*addopts =    --strict-markers    --disable-warnings    --cov=mypackage    --cov-report=term-missing    --cov-report=htmlmarkers =    slow: marca pruebas como lentas    integration: marca pruebas como de integración    unit: marca pruebas como unitarias

Ejecutar Pruebas

bash
# Ejecutar todas las pruebaspytest
# Ejecutar archivo específicopytest tests/test_utils.py
# Ejecutar prueba específicapytest tests/test_utils.py::test_function
# Ejecutar con salida detalladapytest -v
# Ejecutar con coberturapytest --cov=mypackage --cov-report=html
# Ejecutar solo pruebas rápidaspytest -m "not slow"
# Ejecutar hasta el primer fallopytest -x
# Ejecutar últimas pruebas fallidaspytest --lf
# Ejecutar pruebas con patrónpytest -k "test_user"
# Ejecutar con depurador al fallarpytest --pdb

Recuerda: Las pruebas también son código. Mantenlas limpias, legibles y mantenibles. Las buenas pruebas detectan bugs; las excelentes pruebas los previenen.

来源与署名

来源:affaan-m/ecc位于docs/es/skills/python-testing提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架