Testando APIs do GitHub com Pytest - Prática

Introdução

No post anterior, cobrimos os fundamentos teóricos do Pytest.

Agora vamos aplicar tudo na prática usando um exemplo real que consome a API pública do GitHub:

Este guia é intencionalmente muito detalhado. Cada linha importante será explicada para que até iniciantes possam seguir.


Estrutura do Projeto

TEXT
github_api_tests/
├── app.py                     # Aplicação Flask
├── services/
│   └── github_service.py      # Camada de serviço da API GitHub
├── tests/
│   ├── conftest.py            # Fixtures compartilhadas
│   ├── test_users_unit.py     # Testes unitários com mocks
│   ├── test_users_integration.py  # Testes de integração (API real)
│   ├── test_parametrize.py    # Testes parametrizados
│   ├── test_skip.py           # Exemplos de marcador skip
│   ├── test_fail.py           # Exemplos de marcador xfail
│   ├── test_errors.py         # Testes de tratamento de erros
│   ├── test_functional.py     # Testes funcionais (rotas Flask)
│   ├── test_performace.py     # Benchmarks de performance
│   └── test_regression.py     # Testes de regressão
├── pytest.ini                 # Configuração do pytest
└── requirements.txt           # Dependências do projeto
Clique para expandir e ver mais

Organizamos o projeto seguindo as melhores práticas:

Esta arquitetura proporciona:


Configuração do Projeto

Antes de executar os testes, precisamos preparar nosso ambiente de desenvolvimento local adequadamente.


Criando um Ambiente Virtual

Um ambiente virtual nos permite isolar as dependências do projeto da instalação global do Python.

Isso evita conflitos de versão entre diferentes projetos e garante reproduibilidade.

BASH
python -m venv .venv
Clique para expandir e ver mais

O que este comando faz:

python → Executa o interpretador Python.

-m venv → Executa o módulo embutido venv.

.venv → Cria uma nova pasta de ambiente virtual chamada .venv.

Após executar este comando, um diretório chamado .venv/ será criado contendo:

Isso garante que qualquer pacote instalado dentro deste ambiente não afetará outros projetos.


Ativando o Ambiente Virtual

Uma vez criado, o ambiente virtual deve ser ativado:

BASH
source .venv/bin/activate
Clique para expandir e ver mais

O que isto faz:

source → Executa o script no shell atual.

.venv/bin/activate → Ativa o ambiente virtual.

Após ativação:

Seu terminal geralmente muda (ex., (.venv) aparece).

python e pip agora apontam para as versões do ambiente virtual.

Todos os pacotes instalados serão isolados dentro do .venv.


Dependências

Todas as dependências do projeto estão listadas em um arquivo requirements.txt:

PLAINTEXT
flask
requests
pytest
pytest-mock
pytest-cov
pytest-benchmark
Clique para expandir e ver mais

O que cada dependência faz:


Instalando Dependências

Para instalar todos os pacotes necessários:

BASH
pip install -r requirements.txt
Clique para expandir e ver mais

O que este comando faz:


Configuração do Pytest

O projeto inclui um arquivo pytest.ini para configurar o comportamento do pytest globalmente.

pytest.ini

INI
[pytest]
addopts = -ra -q --cov=app --cov-report=term-missing
markers =
    unit: marca testes como testes unitários
    integration: marca testes como testes de integração
    slow: marca testes como testes lentos
    regression: marca testes como testes de regressão
Clique para expandir e ver mais

Explicação linha por linha

[pytest]

Declara que este arquivo contém configuração para o pytest.

addopts = -ra -q --cov=app --cov-report=term-missing

Define opções de linha de comando padrão que sempre serão aplicadas ao executar pytest.

Quebrando em partes:

Isso garante que toda execução de teste inclua automaticamente análise de cobertura.

markers =

Registra marcadores de teste personalizados para evitar avisos como:

MAKEFILE
PytestUnknownMarkWarning: Unknown pytest.mark.integration
Clique para expandir e ver mais

Cada marcador deve ser declarado explicitamente.


unit

INI
unit: marca testes como testes unitários
Clique para expandir e ver mais

Marca testes como testes unitários.

Estes testes:

Você pode executá-los usando:

BASH
pytest -m unit
Clique para expandir e ver mais

integration

INI
integration: marca testes como testes de integração
Clique para expandir e ver mais

Marca testes de integração.

Estes testes:

Execute apenas testes de integração:

BASH
pytest -m integration
Clique para expandir e ver mais

slow

INI
slow: marca testes como testes lentos
Clique para expandir e ver mais

Marca testes mais lentos.

Você pode excluí-los:

BASH
pytest -m "not slow"
Clique para expandir e ver mais

Código de Produção

Aplicação Flask (app.py)

PYTHON
# app.py

from flask import Flask, jsonify
from services.github_service import fetch_users

app = Flask(__name__)

@app.route('/users')
def github_users():
    try:
        users = fetch_users()
        return jsonify(users)
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)
Clique para expandir e ver mais

Camada de Serviço (services/github_service.py)

PYTHON
# services/github_service.py

import requests

GITHUB_API_URL = 'https://api.github.com/users'

def fetch_users(per_page=10):
    """
    Busca usuários da API pública do GitHub.
    """
    response = requests.get(
        GITHUB_API_URL, params={'per_page': per_page}, timeout=5
    )
    response.raise_for_status()
    return response.json()
Clique para expandir e ver mais

Código de Produção (Explicação Linha por Linha)

Aplicação Flask (app.py)

PYTHON
# app.py

from flask import Flask, jsonify
from services.github_service import fetch_users
Clique para expandir e ver mais

Nós importamos:


PYTHON
app = Flask(__name__)
Clique para expandir e ver mais

Cria a instância da aplicação Flask.


PYTHON
@app.route('/users')
def github_users():
    try:
        users = fetch_users()
        return jsonify(users)
    except Exception as e:
        return jsonify({"error": str(e)}), 500
Clique para expandir e ver mais

Esta é uma rota Flask com tratamento de erros:


PYTHON
if __name__ == '__main__':
    app.run(debug=True)
Clique para expandir e ver mais

Permite executar o aplicativo diretamente com python app.py.

Camada de Serviço (services/github_service.py)

PYTHON
# services/github_service.py

import requests

GITHUB_API_URL = 'https://api.github.com/users'
Clique para expandir e ver mais

PYTHON
def fetch_users(per_page=10):
Clique para expandir e ver mais

Parâmetro da função:


PYTHON
response = requests.get(
    GITHUB_API_URL, params={'per_page': per_page}, timeout=5
)
Clique para expandir e ver mais

Detalhes da requisição HTTP:


PYTHON
response.raise_for_status()
return response.json()
Clique para expandir e ver mais

Esta arquitetura segue o Padrão de Camada de Serviço, separando preocupações web da lógica de negócios.


Entendendo o Padrão AAA

AAA significa:

Exemplo:

PYTHON
def test_simple_math():
    # Arrange
    number = 5

    # Act
    result = number + 2

    # Assert
    assert result == 7
Clique para expandir e ver mais

Esta estrutura melhora a legibilidade e profissionalismo.


Fixtures

PYTHON
# tests/conftest.py

import pytest
from app import app

@pytest.fixture
def client():
    """
    Fixture que cria um cliente HTTP de teste para o Flask.
    """
    with app.test_client() as client:
        yield client

@pytest.fixture
def sample_username():
    return 'octocat'
Clique para expandir e ver mais

Esta fixture fornece dados de teste reutilizáveis.

Fixtures (Explicação)

PYTHON
# tests/conftest.py

import pytest
from app import app
Clique para expandir e ver mais

Nós importamos pytest e o aplicativo Flask.


Fixture de Cliente de Teste Flask

PYTHON
@pytest.fixture
def client():
    """
    Fixture que cria um cliente HTTP de teste para o Flask.
    """
    with app.test_client() as client:
        yield client
Clique para expandir e ver mais

Análise linha por linha:

Esta fixture permite testar rotas Flask sem iniciar um servidor real.


Fixture de Dados Amostra

PYTHON
@pytest.fixture
def sample_username():
    return 'octocat'
Clique para expandir e ver mais

Análise linha por linha:

Quando os testes incluem estes parâmetros:

PYTHON
def test_example(client, sample_username):
Clique para expandir e ver mais

O Pytest injeta automaticamente ambas as fixtures. Isso é injeção de dependência.


Teste Unitário com Mock

PYTHON
# tests/test_users_unit.py

from services.github_service import fetch_users

def test_fetch_users_with_pytest_mock(mocker):
    """
    Mesmo teste unitário, usando pytest-mock.
    """
    # Arrange
    fake_users = [{'login': 'pytest-mock'}]

    mocker.patch(
        'services.github_service.requests.get',
        return_value=mocker.Mock(
            json=lambda: fake_users, raise_for_status=lambda: None
        ),
    )

    # Act
    users = fetch_users()

    # Assert
    assert users == fake_users
Clique para expandir e ver mais

Teste Unitário (Explicação Linha por Linha)

PYTHON
# tests/test_users_unit.py

from services.github_service import fetch_users
Clique para expandir e ver mais

Nós importamos a função de serviço para testá-la em isolamento.


PYTHON
def test_fetch_users_with_pytest_mock(mocker):
Clique para expandir e ver mais

Seção Arrange

PYTHON
fake_users = [{'login': 'pytest-mock'}]
Clique para expandir e ver mais

Cria dados de teste esperados.


PYTHON
mocker.patch(
    'services.github_service.requests.get',
    return_value=mocker.Mock(
        json=lambda: fake_users, 
        raise_for_status=lambda: None
    ),
)
Clique para expandir e ver mais

Esta é a configuração de mocking:

Ponto chave: Nenhuma chamada HTTP real é feita!


Seção Act

PYTHON
users = fetch_users()
Clique para expandir e ver mais

Executa a função de serviço, que usa o requests.get mockado.


Seção Assert

PYTHON
assert users == fake_users
Clique para expandir e ver mais

Verifica que o serviço retorna exatamente o que nosso mock forneceu.


Teste de Integração (Chamada Real de API)

PYTHON
# tests/test_users_integration.py

import pytest
from services.github_service import fetch_users

@pytest.mark.integration
@pytest.mark.skip(reason="GitHub API rate limit exceeded - skip for now")
def test_fetch_users_integration():
    """
    Chama API REAL do GitHub
    """
    # Act
    users = fetch_users(5)

    # Assert
    assert isinstance(users, list)
    assert len(users) > 0
    assert 'login' in users[0]
Clique para expandir e ver mais

Este teste faz uma chamada HTTP real para o GitHub.

Teste de Integração (Explicação Linha por Linha)

PYTHON
@pytest.mark.integration
def test_fetch_users_integration():
Clique para expandir e ver mais

PYTHON
users = fetch_users(5)
Clique para expandir e ver mais

Isto realiza uma requisição real para a API GitHub, pedindo por 5 usuários.


PYTHON
assert isinstance(users, list)
assert len(users) > 0
assert 'login' in users[0]
Clique para expandir e ver mais

Múltiplas asserções validam:

Importante: Este teste requer conexão com internet e pode ser lento.


Parametrizar

PYTHON
# tests/test_parametrize.py

import pytest
from services.github_service import fetch_users

@pytest.mark.parametrize('qty', [1, 3, 5])
def test_fetch_users_parametrize(qty, mocker):
    """
    Mesmo teste com múltiplos valores.
    Usando requests mockados para evitar rate limiting.
    """
    # Arrange - mockar resposta para evitar rate limiting
    fake_users = [{'login': f'user{i}'} for i in range(qty)]
    
    mocker.patch(
        'services.github_service.requests.get',
        return_value=mocker.Mock(
            json=lambda: fake_users, 
            raise_for_status=lambda: None
        ),
    )
    
    # Act
    users = fetch_users(qty)
    
    # Assert
    assert len(users) <= qty
    assert len(users) == qty  # Deve corresponder exatamente com nosso mock
Clique para expandir e ver mais

O Pytest executa este teste três vezes com diferentes quantidades.

Parametrizar (Detalhado)

PYTHON
@pytest.mark.parametrize('qty', [1, 3, 5])
Clique para expandir e ver mais

O Pytest irá:


PYTHON
def test_fetch_users_parametrize(qty, mocker):
    fake_users = [{'login': f'user{i}'} for i in range(qty)]
    mocker.patch('services.github_service.requests.get', ...)
    users = fetch_users(qty)
    assert len(users) == qty
Clique para expandir e ver mais

Os parâmetros são automaticamente injetados na função de teste.

Esta abordagem:

Dica profissional: Você também pode combinar múltiplos parâmetros:

PYTHON
@pytest.mark.parametrize('qty,expected', [(1, 1), (5, 5), (100, 100)])
Clique para expandir e ver mais

Testando Erros

PYTHON
# tests/test_errors.py

import pytest
import requests
from services.github_service import fetch_users

def test_timeout_error(mocker):
    """
    Testa como o serviço lida com timeouts.
    """
    # Arrange
    mocker.patch(
        'services.github_service.requests.get',
        side_effect=requests.Timeout("Request timed out")
    )

    # Act & Assert
    with pytest.raises(requests.Timeout):
        fetch_users()

def test_http_error_handling(mocker):
    """
    Testa tratamento de erros HTTP.
    """
    # Arrange
    mocker.patch(
        'services.github_service.requests.get',
        side_effect=requests.HTTPError("404 Not Found")
    )

    # Act & Assert
    with pytest.raises(requests.HTTPError):
        fetch_users()
Clique para expandir e ver mais

Isso garante que erros são tratados corretamente.

Testando Erros (Explicação Linha por Linha)

Teste de Erro de Timeout

PYTHON
mocker.patch(
    'services.github_service.requests.get',
    side_effect=requests.Timeout("Request timed out")
)
Clique para expandir e ver mais

PYTHON
with pytest.raises(requests.Timeout):
    fetch_users()
Clique para expandir e ver mais

Teste de Erro HTTP

PYTHON
side_effect=requests.HTTPError("404 Not Found")
Clique para expandir e ver mais

Simula erros HTTP como 404, 500, etc.

Benefícios chave:


Skip vs Xfail

PYTHON
# tests/test_skip.py

import pytest

@pytest.mark.skip(reason='Feature em desenvolvimento')
def test_future_feature():
    assert True
Clique para expandir e ver mais
PYTHON
# tests/test_fail.py

import pytest
from services.github_service import fetch_users

@pytest.mark.xfail(reason='Bug conhecido')
def test_known_bug():
    assert False

@pytest.mark.xfail(reason='Bug conhecido quando per_page > 100')
def test_expected_failure():
    fetch_users(200)
Clique para expandir e ver mais

Skip vs Xfail (Detalhado)

Marcador Skip

PYTHON
@pytest.mark.skip(reason='Feature em desenvolvimento')
Clique para expandir e ver mais

Marcador Xfail

PYTHON
@pytest.mark.xfail(reason='Bug conhecido')
Clique para expandir e ver mais

Quando usar cada:

Pulando condicional:

PYTHON
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requer Python 3.8+")
def test_python_38_feature():
    pass
Clique para expandir e ver mais

Testes Funcionais (Rotas Flask)

PYTHON
# tests/test_functional.py

from http import HTTPStatus

def test_users_page(client):
    """
    Teste funcional:
    - Simula acesso à rota Flask
    """
    # Act
    response = client.get('/users')

    # Assert - verificar por resposta bem-sucedida ou erro de rate limit
    assert response.status_code in [HTTPStatus.OK, HTTPStatus.INTERNAL_SERVER_ERROR]
    
    # Verificar se resposta é JSON válido
    try:
        data = json.loads(response.data)
        if response.status_code == HTTPStatus.OK:
            assert isinstance(data, list)
        else:
            # Deve ser uma resposta de erro
            assert isinstance(data, dict)
            assert 'error' in data
    except json.JSONDecodeError:
        assert False, "Resposta não é JSON válido"
Clique para expandir e ver mais

Testes Funcionais (Explicação Linha por Linha)

PYTHON
def test_users_page(client):
Clique para expandir e ver mais

PYTHON
response = client.get('/users')
Clique para expandir e ver mais

PYTHON
assert response.status_code in [HTTPStatus.OK, HTTPStatus.INTERNAL_SERVER_ERROR]
Clique para expandir e ver mais

PYTHON
data = json.loads(response.data)
if response.status_code == HTTPStatus.OK:
    assert isinstance(data, list)
else:
    assert isinstance(data, dict)
    assert 'error' in data
Clique para expandir e ver mais

Testes de Performance

PYTHON
# tests/test_performace.py

def test_users_endpoint_performance(benchmark, client):
    """
    Mede tempo de resposta da rota /users.
    """
    benchmark(lambda: client.get('/users'))
Clique para expandir e ver mais

Teste de Performance (Explicação Linha por Linha)

PYTHON
def test_users_endpoint_performance(benchmark, client):
Clique para expandir e ver mais

PYTHON
benchmark(lambda: client.get('/users'))
Clique para expandir e ver mais

Resultados de benchmark atuais do projeto:

PLAINTEXT
------------------------------------------------------- benchmark: 1 tests ------------------------------------------------------
Name (time in ms)                        Min       Max      Mean  StdDev    Median      IQR  Outliers     OPS  Rounds  Iterations
---------------------------------------------------------------------------------------------------------------------------------
test_users_endpoint_performance     246.0832  264.0037  254.0807  8.1997  252.4519  15.2874       1;0  3.9358       5           1
---------------------------------------------------------------------------------------------------------------------------------

Legend:
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
Clique para expandir e ver mais

Métricas de benchmark explicadas:

Execute com: pytest --benchmark-only

Testes de Regressão

PYTHON
# tests/test_regression.py

from http import HTTPStatus
import pytest

@pytest.mark.regression
def test_users_endpoint_handles_gracefully(client):
    """
    Testa que a rota lida com erros elegantemente.
    Mesmo se a API externa falhar, a rota não deve travar.
    """
    # Act
    response = client.get('/users')

    # Deve retornar resposta JSON adequada mesmo quando a API GitHub falhar
    assert response.status_code in [HTTPStatus.OK, HTTPStatus.INTERNAL_SERVER_ERROR]
    
    # Resposta deve ser sempre JSON válido
    import json
    try:
        data = json.loads(response.data)
        assert isinstance(data, (list, dict))
    except json.JSONDecodeError:
        assert False, "Resposta não é JSON válido"
Clique para expandir e ver mais

Teste de Regressão (Explicação Linha por Linha)

PYTHON
@pytest.mark.regression
Clique para expandir e ver mais

PYTHON
assert response.status_code in [HTTPStatus.OK, HTTPStatus.INTERNAL_SERVER_ERROR]
Clique para expandir e ver mais

Execute testes de regressão apenas: pytest -m regression

Cobertura

BASH
pytest --cov=app --cov-report=term-missing
Clique para expandir e ver mais

Isto:

Relatório de cobertura atual do projeto:

PLAINTEXT
==================== tests coverage ====================
Name     Stmts   Miss  Cover   Missing
--------------------------------------
app.py      12      2    83%   10, 15
--------------------------------------
TOTAL       12      2    83%
Clique para expandir e ver mais

Métricas de cobertura explicadas:

⚠️ Importante: Alta cobertura não significa testes de alta qualidade. Foque em testar comportamento, não apenas linhas.


Executando Diferentes Tipos de Teste

BASH
# Executar apenas testes unitários
pytest -m unit

# Executar apenas testes de integração
pytest -m integration

# Executar testes de regressão
pytest -m regression

# Pular testes lentos
pytest -m "not slow"

# Executar benchmarks de performance
pytest --benchmark-only

# Executar com cobertura (configurado em pytest.ini)
pytest

# Executar com output detalhado
pytest -v

# Executar arquivo de teste específico
pytest tests/test_users_unit.py
Clique para expandir e ver mais

Resumo das Melhores Práticas

  1. Organização de Testes

    • Separar código de produção de testes
    • Usar nomes de testes descritivos
    • Agrupar testes relacionados em arquivos
  2. Padrão AAA

    • Arrange: Preparar dados de teste e mocks
    • Act: Executar a função sob teste
    • Assert: Verificar resultado
  3. Estratégia de Mocking

    • Mockar dependências externas em testes unitários
    • Usar chamadas reais em testes de integração
    • Mockar apenas o que você precisa (métodos específicos)
  4. Uso de Fixtures

    • Reutilizar configuração de teste comum
    • Manter fixtures focadas e simples
    • Usar yield para operações de limpeza
  5. Categorias de Teste

    • Unit: Rápidos, isolados, lógica de negócios
    • Integration: Chamadas externas reais
    • Functional: Cenários completos do usuário
    • Performance: Benchmark de caminhos críticos
    • Regression: Prevenir recorrência de bugs

Dicas Avançadas

Marcadores Personalizados

PYTHON
# pytest.ini
markers =
    unit: marca testes como testes unitários
    integration: marca testes como testes de integração
    regression: marca testes como testes de regressão
    slow: marca testes como testes lentos
    network: marca testes requerendo internet
Clique para expandir e ver mais

Configuração de Teste

PYTHON
# conftest.py
@pytest.fixture(scope="session")
def api_client():
    """Cliente compartilhado entre todos os testes"""
    return SomeApiClient()

@pytest.fixture(autouse=True)
def setup_test_environment():
    """Fixture auto-usada para todos os testes"""
    # Código de configuração aqui
    yield
    # Código de limpeza aqui
Clique para expandir e ver mais

Conclusão

Você agora entende:

Isto é arquitetura de teste de nível profissional que escalará com seu projeto.

Próximos Passos

  1. Adicionar mais testes de casos extremos
  2. Implementar fábricas de dados de teste
  3. Configurar CI/CD com testes automatizados
  4. Explorar teste baseado em propriedades com hypothesis
  5. Considerar teste de contrato para APIs

Projeto completo no Github

Iniciar busca

Digite palavras-chave para buscar

↑↓
ESC
⌘K Atalho