🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Teste Python com pytest 2026: luminárias, simulação e integração de CI

⏱️5 min read  ·  939 words

pytest é o padrão ouro para testes Python em 2026. Usado por Django, FastAPI, NumPy e milhares de projetos de código aberto, pytest torna a escrita e a execução de testes intuitiva e poderosa. Este guia completo leva você desde o primeiro teste até equipamentos avançados, parametrização e integração de CI.

Por que pytest em vez de unittest?

  • Sintaxe mais simples– simplesassertdeclarações com mensagens úteis de falha
  • Acessórios poderosos— injeção de dependência com desmontagem automática
  • Parametrizar— execute um teste com muitas entradas
  • Rico ecossistema de plugins— cobertura, xdist, mock, assíncio e muito mais
  • Melhor descoberta— encontra testes automaticamente sem clichê

Instalação e primeiro teste

# Install pytest
pip install pytest pytest-cov pytest-asyncio

# Create a test
# test_math.py
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

# Run tests
pytest                    # run all tests
pytest test_math.py       # specific file
pytest -v                 # verbose output
pytest -k "add"           # run tests matching name
pytest --tb=short         # shorter tracebacks

Organização de teste

# Recommended structure
# tests/
#   conftest.py          — shared fixtures
#   test_users.py
#   test_orders.py
#   integration/
#     test_api.py
#   unit/
#     test_services.py

# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# testpaths = ["tests"]
# addopts = "-v --tb=short"

# Test class grouping
class TestUserService:
    def test_create_user(self):
        user = create_user("alice@example.com")
        assert user.email == "alice@example.com"

    def test_create_user_invalid_email(self):
        with pytest.raises(ValueError, match="Invalid email"):
            create_user("not-an-email")

    def test_duplicate_email_raises(self, db):
        create_user("alice@example.com")
        with pytest.raises(DuplicateError):
            create_user("alice@example.com")

Luminárias – superpotência do pytest

import pytest
from app.database import Database
from app.models import User

# Simple fixture
@pytest.fixture
def sample_user():
    return User(id=1, name="Alice", email="alice@example.com")

# Fixture with setup and teardown
@pytest.fixture
def db():
    database = Database(url="sqlite:///:memory:")
    database.create_tables()
    yield database           # test runs here
    database.drop_tables()   # teardown

# Session-scoped fixture — runs once per test session
@pytest.fixture(scope="session")
def app_client():
    from app import create_app
    app = create_app(testing=True)
    return app.test_client()

# Fixture using other fixtures
@pytest.fixture
def admin_user(db):
    user = db.create_user(email="admin@test.com", role="admin")
    return user

# conftest.py — shared fixtures auto-discovered
# pytest finds conftest.py automatically

Parametrizar – Testar Vários Casos

import pytest

def is_valid_email(email: str) -> bool:
    import re
    return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))

@pytest.mark.parametrize("email,expected", [
    ("alice@example.com", True),
    ("user+tag@domain.co.uk", True),
    ("invalid", False),
    ("no-at-sign", False),
    ("@nodomain.com", False),
    ("user@.com", False),
    ("", False),
])
def test_email_validation(email, expected):
    assert is_valid_email(email) == expected

# Multiple parametrize decorators — creates cartesian product
@pytest.mark.parametrize("base", [2, 10, 16])
@pytest.mark.parametrize("exp", [1, 2, 3])
def test_powers(base, exp):
    result = base ** exp
    assert result == pow(base, exp)

Zombando com pytest-mock

pip install pytest-mock

import pytest
from app.services import UserService
from app.email import EmailSender

def test_registration_sends_welcome_email(mocker):
    # Mock the email sender
    mock_send = mocker.patch("app.services.EmailSender.send")

    service = UserService()
    service.register("alice@example.com", "securepass")

    # Assert email was sent
    mock_send.assert_called_once_with(
        to="alice@example.com",
        subject="Welcome to TechPulse!"
    )

def test_external_api_timeout(mocker):
    # Mock requests.get to simulate timeout
    mocker.patch(
        "requests.get",
        side_effect=requests.exceptions.Timeout
    )

    with pytest.raises(ServiceUnavailableError):
        fetch_weather("London")

# Spy on real method (calls real code but tracks calls)
def test_cache_used_on_second_call(mocker):
    spy = mocker.spy(cache_service, "get")
    fetch_user(1)
    fetch_user(1)  # should hit cache
    assert spy.call_count == 2

Testando código assíncrono

pip install pytest-asyncio

import pytest
import pytest_asyncio

# Mark entire file as async
# pytest.ini: asyncio_mode = auto

@pytest.mark.asyncio
async def test_async_fetch():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/api/users")
    assert response.status_code == 200

# Async fixture
@pytest_asyncio.fixture
async def async_db():
    async with AsyncDatabase() as db:
        await db.create_tables()
        yield db
        await db.drop_tables()

Relatório de cobertura

# Run with coverage
pytest --cov=app --cov-report=html --cov-report=term-missing

# Output:
# Name              Stmts   Miss  Cover
# app/models.py        45      3    93%
# app/services.py      87     12    86%
# TOTAL               132     15    89%

# Fail if coverage below threshold
pytest --cov=app --cov-fail-under=80

# .coveragerc or pyproject.toml
# [tool.coverage.run]
# omit = ["*/tests/*", "*/migrations/*"]

Marcas pytest – categorizar testes

import pytest

# Define marks in pyproject.toml
# [tool.pytest.ini_options]
# markers = [
#   "slow: marks tests as slow",
#   "integration: marks integration tests",
#   "unit: marks unit tests",
# ]

@pytest.mark.slow
@pytest.mark.integration
def test_full_user_registration_flow(client, db):
    # End-to-end registration test
    response = client.post("/register", json={
        "email": "test@example.com",
        "password": "SecurePass123!"
    })
    assert response.status_code == 201
    assert db.users.count() == 1

# Run only unit tests
# pytest -m unit

# Skip slow tests in CI
# pytest -m "not slow" 

Integração de CI com ações do GitHub

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          pip install uv
          uv pip install -e ".[dev]" --system

      - name: Run tests
        run: pytest --cov=app --cov-fail-under=80

      - name: Upload coverage
        uses: codecov/codecov-action@v4

o domínio do pytest requer prática, mas paga dividendos. Escreva testes junto com recursos (TDD), use fixtures para configuração/desmontagem, parametrize para casos extremos e simule dependências externas. Tenha como objetivo uma cobertura de mais de 80% na lógica de negócios e 100% nos caminhos críticos.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা