Testar código é uma prática essencial no desenvolvimento de software, pois garante que o comportamento esperado seja mantido ao longo do tempo e que mudanças no código não introduzam erros inesperados. Nesta aula, vamos explorar os fundamentos de testes em Python, começando pela motivação, passando pelas principais ferramentas (unittest e pytest), e culminando com a criação do seu primeiro teste e a estrutura adequada para organizá-los.

Você aprenderá não apenas a escrever testes, mas também a pensar criticamente sobre o que testar e como estruturar seus projetos para facilitar a manutenção e a expansão dos testes. Ao final, você terá uma base sólida para incorporar testes no seu fluxo de desenvolvimento.

Por que testar

Testar é uma forma de verificar se o código faz o que deveria fazer. Sem testes, você depende de testes manuais ou da confiança de que as mudanças não quebram nada, o que é arriscado em projetos de qualquer tamanho. Testes automatizados permitem que você valide rapidamente o comportamento do código após cada alteração, aumentando a confiança e reduzindo a ocorrência de bugs.

Além disso, testes bem escritos funcionam como documentação executável: eles mostram como o código deve ser usado e quais resultados são esperados. Isso é valioso para novos desenvolvedores que entram no projeto e para você mesmo, meses depois, quando precisar lembrar o que determinada função faz.

Benefícios adicionais incluem:

  • Detecção precoce de erros: testes rodam automaticamente e identificam problemas antes que cheguem ao usuário final.
  • Refatoração segura: você pode modificar o código com confiança, sabendo que os testes vão pegar qualquer regressão.
  • Melhor design: escrever testes força você a pensar em interfaces claras e responsabilidades bem definidas.
  • Integração contínua: testes são a base para pipelines de CI/CD, que automatizam a validação do código.

unittest vs pytest

Python vem com um framework de testes embutido chamado unittest, que é baseado no padrão xUnit. Ele oferece classes de teste, métodos de asserção e um runner de testes. Por outro lado, pytest é uma biblioteca de terceiros que se tornou muito popular por sua simplicidade e recursos avançados, como fixtures, parametrização e plugins.

unittest tem a vantagem de não exigir instalação adicional e ser suficiente para projetos simples. No entanto, sua sintaxe é mais verbosa, exigindo que você crie classes que herdam de unittest.TestCase e use métodos de asserção específicos. Exemplo:

import unittest

def soma(a, b):
    return a + b

class TestSoma(unittest.TestCase):
    def test_soma_positivos(self):
        self.assertEqual(soma(2, 3), 5)

if __name__ == '__main__':
    unittest.main()

pytest, por sua vez, permite escrever testes como funções simples, sem a necessidade de classes. Ele usa a instrução assert do Python, o que torna o código mais conciso e legível. Além disso, pytest fornece muitas funcionalidades úteis, como captura de saída, fixtures e relatórios detalhados. Exemplo:

def soma(a, b):
    return a + b

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

Para instalar pytest, use pip install pytest. Em termos de escolha, pytest é frequentemente recomendado para novos projetos devido à sua facilidade de uso e ecossistema rico. No entanto, unittest ainda é uma opção válida, especialmente se você prefere não adicionar dependências externas ou precisa de compatibilidade com ferramentas que dependem dele.

Primeiro teste

Vamos escrever nosso primeiro teste usando pytest, pois é mais direto. Suponha que temos um módulo chamado calculadora.py com uma função soma:

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

Agora, criamos um arquivo de teste chamado test_calculadora.py (por convenção, os arquivos de teste começam com test_ ou terminam com _test.py):

# test_calculadora.py
from calculadora import soma

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

def test_soma_negativos():
    assert soma(-1, -1) == -2

Para rodar os testes, execute pytest no terminal a partir do diretório onde estão os arquivos. O pytest irá descobrir automaticamente os arquivos de teste e executar as funções que começam com test_. A saída mostrará se os testes passaram ou falharam.

Você também pode rodar com mais detalhes usando pytest -v para ver cada teste individualmente. Em caso de falha, o pytest exibirá o valor dos asserts, facilitando a depuração.

Estrutura

Uma estrutura de testes bem organizada é crucial para a manutenibilidade. A convenção comum em projetos Python é colocar os testes em um diretório separado, geralmente chamado tests. Dentro dele, você pode espelhar a estrutura do pacote principal, facilitando a localização dos testes correspondentes a cada módulo.

Exemplo de estrutura de projeto:

meu_projeto/
├── meu_pacote/
│   ├── __init__.py
│   ├── calculadora.py
│   └── utils.py
└── tests/
    ├── __init__.py
    ├── test_calculadora.py
    └── test_utils.py

Além disso, é importante usar nomes de testes descritivos, como test_soma_retorna_correto_para_positivos, para que falhas sejam facilmente identificáveis. No pytest, você também pode usar fixtures para configurar pré-condições e recursos compartilhados, e parametrização para testar múltiplos casos com um único teste.

Boas práticas adicionais incluem:

  • Isolamento: cada teste deve ser independente e não depender de outros testes.
  • Testes pequenos: foque em testar uma única funcionalidade por teste.
  • Use mocks quando necessário: para isolar testes de dependências externas, como bancos de dados ou APIs.
  • Rode os testes frequentemente: integre-os ao seu fluxo de trabalho, usando ferramentas como pre-commit ou CI.

Referências

Exercícios

  1. Escreva um teste para uma função subtrai(a, b) que retorna a subtração de dois números, usando pytest. Crie o arquivo de teste e execute-o.
  2. ✓ Resposta: Crie um arquivo test_calculadora.py com o seguinte conteúdo:
    from calculadora import subtrai
    
    def test_subtrai_positivos():
        assert subtrai(5, 3) == 2
    
    def test_subtrai_negativos():
        assert subtrai(-1, -2) == 1
    Execute pytest no terminal.
  3. Converta o teste do exercício 1 para usar unittest em vez de pytest. Escreva a classe de teste correspondente.
  4. ✓ Resposta: Usando unittest:
    import unittest
    from calculadora import subtrai
    
    class TestSubtrai(unittest.TestCase):
        def test_subtrai_positivos(self):
            self.assertEqual(subtrai(5, 3), 2)
        def test_subtrai_negativos(self):
            self.assertEqual(subtrai(-1, -2), 1)
    
    if __name__ == '__main__':
        unittest.main()
  5. Crie uma estrutura de diretórios para um projeto chamado meu_projeto com um pacote meu_pacote contendo um módulo calculadora.py e um diretório tests com um arquivo de teste para a função soma. Escreva os arquivos e execute os testes.
  6. ✓ Resposta: Estrutura:
    meu_projeto/
    ├── meu_pacote/
    │   ├── __init__.py
    │   └── calculadora.py
    └── tests/
        ├── __init__.py
        └── test_calculadora.py
    Conteúdo de calculadora.py:
    def soma(a, b):
        return a + b
    Conteúdo de test_calculadora.py:
    from meu_pacote.calculadora import soma
    
    def test_soma():
        assert soma(2, 3) == 5
    Execute pytest a partir do diretório meu_projeto.
  7. Escreva um teste parametrizado usando pytest para verificar a função multiplica(a, b) com vários pares de números, incluindo zero e negativos.
  8. ✓ Resposta: Use o decorador @pytest.mark.parametrize:
    import pytest
    from calculadora import multiplica
    
    @pytest.mark.parametrize("a,b,esperado", [
        (2, 3, 6),
        (0, 5, 0),
        (-2, 3, -6),
        (2, -3, -6),
    ])
    def test_multiplica(a, b, esperado):
        assert multiplica(a, b) == esperado
  9. Explique a diferença entre unittest e pytest em termos de sintaxe e flexibilidade, e dê um exemplo de uma situação em que você preferiria usar um em vez do outro.
  10. ✓ Resposta:

    unittest usa classes e métodos de asserção específicos (ex.: assertEqual), enquanto pytest usa funções simples e a instrução assert nativa. pytest é mais conciso e oferece recursos como fixtures, parametrização e plugins, enquanto unittest é embutido e não requer dependências externas. Prefira unittest em projetos que precisam de compatibilidade máxima sem instalações adicionais, ou quando já está integrado com outras ferramentas. Prefira pytest para novos projetos, especialmente se você valoriza produtividade e recursos avançados.

Boas práticas

Além do que já discutimos, lembre-se de manter seus testes rápidos e confiáveis. Evite dependências de rede ou de tempo real nos testes unitários, utilizando mocks ou stubs quando necessário. Também é útil rodar os testes com frequência, por exemplo, após cada alteração no código, para detectar problemas cedo. Considere adotar uma ferramenta de cobertura de código, como pytest-cov, para identificar áreas não testadas e melhorar a qualidade geral.