Nesta aula, vamos aprofundar dois conceitos essenciais para a qualidade de software em Python: cobertura de testes e testes de integração. A cobertura mede quanto do seu código é exercitado pelos testes, ajudando a identificar lacunas. Já os testes de integração verificam a interação entre diferentes módulos ou sistemas, garantindo que as partes funcionem juntas. Vamos explorar como usar a ferramenta coverage, como escrever testes de integração eficazes, como isolar esses testes para que sejam confiáveis e como incorporar tudo isso em um pipeline de CI (Integração Contínua).

Coverage

A cobertura de código (coverage) é uma métrica que indica a porcentagem de linhas, branches ou funções do seu código que são executadas durante os testes. Ela é fundamental para identificar partes não testadas, mas é importante lembrar que alta cobertura não garante a ausência de bugs: apenas mostra que o código foi executado, não que foi verificado corretamente.

No Python, a ferramenta mais comum é o coverage.py. Ela se integra com o pytest através do plugin pytest-cov. Para instalar, use:

pip install coverage pytest-cov

Depois, você pode executar os testes com:

pytest --cov=meu_modulo tests/

Isso exibirá um relatório no terminal. Para gerar um relatório HTML mais detalhado:

pytest --cov=meu_modulo --cov-report=html tests/

O relatório HTML fica na pasta htmlcov/ e mostra linha a linha o que foi coberto. Você também pode configurar um limite mínimo de cobertura: --cov-fail-under=80 faz o pytest falhar se a cobertura for menor que 80%.

Um exemplo de código com testes:

# calculadora.py
def soma(a, b):
    return a + b

def subtrai(a, b):
    return a - b
# test_calculadora.py
from calculadora import soma, subtrai

def test_soma():
    assert soma(2, 3) == 5

def test_subtrai():
    assert subtrai(5, 2) == 3

Ao rodar pytest --cov=calculadora, a cobertura será 100% porque todas as funções foram chamadas. Mas se você remover o teste de subtrai, a cobertura cairá para 50%.

Testes de integração

Testes de integração verificam se os componentes do sistema funcionam corretamente quando combinados. Eles podem envolver banco de dados, APIs, serviços externos, ou simplesmente a interação entre módulos internos. O objetivo é capturar problemas que não aparecem em testes unitários, como erros de comunicação, incompatibilidade de interfaces ou problemas de configuração.

Por exemplo, suponha que você tenha um módulo que busca dados de um banco e outro que processa esses dados. Um teste de integração pode verificar se o fluxo completo funciona, usando um banco de teste real ou um banco em memória (como SQLite).

Um exemplo simples:

# banco.py
import sqlite3

def criar_tabela(conn):
    conn.execute('''CREATE TABLE IF NOT EXISTS usuarios (
        id INTEGER PRIMARY KEY,
        nome TEXT NOT NULL
    )''')

def inserir_usuario(conn, nome):
    conn.execute('INSERT INTO usuarios (nome) VALUES (?)', (nome,))
    conn.commit()

def listar_usuarios(conn):
    return conn.execute('SELECT * FROM usuarios').fetchall()
# test_integracao.py
import sqlite3
from banco import criar_tabela, inserir_usuario, listar_usuarios

def test_fluxo_completo():
    conn = sqlite3.connect(':memory:')
    criar_tabela(conn)
    inserir_usuario(conn, 'Alice')
    inserir_usuario(conn, 'Bob')
    usuarios = listar_usuarios(conn)
    assert len(usuarios) == 2
    assert usuarios[0][1] == 'Alice'
    conn.close()

Esse teste verifica a interação entre as funções de banco, garantindo que o fluxo de criação, inserção e consulta funciona.

Isolamento

Testes de integração devem ser isolados do ambiente real para serem determinísticos e não dependerem de serviços externos instáveis. Isso é feito usando mocks, fakes, ou bancos de dados em memória. O isolamento garante que os testes sejam rápidos, confiáveis e possam rodar em qualquer ambiente, inclusive no CI.

No exemplo anterior, usamos um banco SQLite em memória, o que isola o teste de um banco real. Para serviços externos, como APIs HTTP, podemos usar a biblioteca responses ou unittest.mock para simular as respostas.

Exemplo com mock:

# servico.py
import requests

def obter_dados():
    r = requests.get('https://api.exemplo.com/dados')
    r.raise_for_status()
    return r.json()
# test_servico.py
from unittest.mock import patch
import servico

def test_obter_dados():
    with patch('servico.requests.get') as mock_get:
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'chave': 'valor'}
        resultado = servico.obter_dados()
        assert resultado == {'chave': 'valor'}
        mock_get.assert_called_once_with('https://api.exemplo.com/dados')

Isso permite testar o código sem depender da API real, isolando o teste de rede e de mudanças externas.

CI

Integração Contínua (CI) é a prática de automatizar a execução de testes e outras verificações sempre que há uma alteração no código. Ferramentas como GitHub Actions, GitLab CI, Travis CI, entre outras, permitem configurar pipelines que rodam seus testes automaticamente. Isso ajuda a detectar problemas cedo e garante que o código esteja sempre em um estado saudável.

Um exemplo de pipeline no GitHub Actions (arquivo .github/workflows/python.yml):

name: Python CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.12'
    - name: Install dependencies
      run: pip install pytest pytest-cov
    - name: Run tests with coverage
      run: pytest --cov=meu_modulo --cov-report=term-missing --cov-fail-under=80

Esse pipeline instala as dependências e roda os testes com cobertura, falhando se a cobertura for menor que 80%. A integração com o CI garante que a cobertura seja monitorada continuamente e que os testes de integração sejam executados em um ambiente limpo.

Boas práticas

Aqui estão algumas boas práticas para testes de integração e cobertura:

  • Mire em uma cobertura alta (acima de 80%) para código crítico, mas não persiga 100% cegamente; foque nas partes mais importantes.
  • Escreva testes de integração para os fluxos principais do sistema, como login, criação de pedidos, etc.
  • Use bancos de dados em memória (SQLite) ou contêineres Docker para isolar testes de integração.
  • Configure o CI para rodar os testes em múltiplas versões do Python e em diferentes sistemas operacionais, se aplicável.
  • Evite testar código de terceiros; concentre-se no seu código.

Referências

Exercícios

  1. Escreva um teste de integração para uma classe que gerencia um carrinho de compras, onde os itens são adicionados e o total é calculado. Use um mock para simular o banco de dados de produtos.

    ✓ Resposta:
    from unittest.mock import Mock
    
    class ProdutoDB:
        def get_preco(self, produto_id):
            raise NotImplementedError
    
    class Carrinho:
        def __init__(self, db):
            self.db = db
            self.itens = []
    
        def adicionar(self, produto_id, quantidade):
            preco = self.db.get_preco(produto_id)
            self.itens.append({'produto_id': produto_id, 'quantidade': quantidade, 'preco': preco})
    
        def total(self):
            return sum(item['preco'] * item['quantidade'] for item in self.itens)
    
    def test_carrinho_integracao():
        db = Mock()
        db.get_preco.side_effect = lambda pid: 10 if pid == 1 else 20
        carrinho = Carrinho(db)
        carrinho.adicionar(1, 2)
        carrinho.adicionar(2, 1)
        assert carrinho.total() == 40
        db.get_preco.assert_any_call(1)
        db.get_preco.assert_any_call(2)
  2. Explique como você configuraria o coverage para falhar se a cobertura for menor que 90% em um projeto pytest.

    ✓ Resposta:

    Utilizando o plugin pytest-cov, basta adicionar a opção --cov-fail-under=90 na linha de comando, ou configurar no arquivo pytest.ini ou pyproject.toml. Por exemplo, no pytest.ini:

    [pytest]
    addopts = --cov=meu_modulo --cov-report=term-missing --cov-fail-under=90
  3. Qual é a diferença entre testes unitários e testes de integração? Dê um exemplo de cada.

    ✓ Resposta:

    Testes unitários verificam uma única unidade de código (função, classe) de forma isolada, usando mocks para dependências. Exemplo: testar a função soma sem depender de outros módulos. Testes de integração verificam a interação entre várias unidades ou sistemas. Exemplo: testar o fluxo completo de uma API que consulta um banco de dados.

  4. Escreva um teste de integração para uma função que lê um arquivo CSV e retorna uma lista de dicionários. Use um arquivo temporário para o teste.

    ✓ Resposta:
    import csv
    import tempfile
    import os
    
    def ler_csv(caminho):
        with open(caminho, newline='') as f:
            return list(csv.DictReader(f))
    
    def test_ler_csv():
        conteudo = "nome,idade\nAlice,30\nBob,25\n"
        with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as f:
            f.write(conteudo)
            caminho = f.name
        try:
            dados = ler_csv(caminho)
            assert dados == [{'nome': 'Alice', 'idade': '30'}, {'nome': 'Bob', 'idade': '25'}]
        finally:
            os.unlink(caminho)
  5. Descreva como você configuraria um pipeline de CI no GitHub Actions para rodar testes de integração que usam um banco de dados PostgreSQL. Que passos seriam necessários?

    ✓ Resposta:

    O pipeline deve incluir um serviço do PostgreSQL usando o services do GitHub Actions, configurar as variáveis de ambiente (como DATABASE_URL), instalar as dependências (incluindo drivers como psycopg2), executar migrações e rodar os testes. Exemplo:

    jobs:
      test:
        runs-on: ubuntu-latest
        services:
          postgres:
            image: postgres:16
            env:
              POSTGRES_PASSWORD: postgres
            ports:
              - 5432:5432
            options: >-
              --health-cmd pg_isready
              --health-interval 10s
              --health-timeout 5s
              --health-retries 5
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-python@v4
            with:
              python-version: '3.12'
          - run: pip install -r requirements.txt
          - run: pytest

Com esses conceitos, você está pronto para aplicar cobertura e testes de integração em seus projetos, garantindo maior qualidade e confiabilidade. Lembre-se de sempre revisar a cobertura e manter os testes isolados para obter resultados consistentes.