Curso de Python / Escrever testes com unittest e verificar limites e falhas
Testes automatizados conferem repetidamente resultados ou exceções esperados para entradas escolhidas. Defina o comportamento exigido pelo usuário, incluindo limites e falhas, para detectar regressões quando a implementação mudar.
Definir o contrato antes dos testes
A função desta prática recebe uma lista de inteiros não negativos e devolve sua soma. Uma lista vazia resulta em zero. Valores booleanos, strings e números negativos devem ser rejeitados com ValueError. A regra é pequena e não depende do relógio, de uma conexão de rede ou de arquivos pessoais, o que facilita sua reprodução.
O módulo unittest pertence à biblioteca padrão. Os testes usam uma classe derivada de unittest.TestCase, e os nomes dos métodos começam com test_. O arquivo também começa com esse prefixo para ser encontrado pela descoberta padrão. Consulte o framework unittest.
Escolha as entradas a partir das regras, em vez de escrever um teste para cada linha interna da função. A mesma regra deve continuar válida se você melhorar a implementação sem mudar seu comportamento. O contrato aqui é uma lista reutilizável; a função não foi projetada para um gerador consumido durante a validação.
Preparar a função e o arquivo de testes
Crie uma pasta de prática e salve os dois arquivos a seguir, em UTF-8, lado a lado. O arquivo expense_math.py contém a função:
def total(amounts):
if any(type(x) is not int or x < 0 for x in amounts):
raise ValueError("invalid amount")
return sum(amounts)
O arquivo test_expense_math.py contém a suíte de testes:
import unittest
from expense_math import total
class TotalTests(unittest.TestCase):
def test_regular(self):
self.assertEqual(total([3500, 8000, 2500]), 14000)
def test_empty(self):
self.assertEqual(total([]), 0)
def test_invalid(self):
for values in [[-1], [True], ["8000"]]:
with self.subTest(values=values):
with self.assertRaises(ValueError):
total(values)
if __name__ == "__main__":
unittest.main()
No terminal dessa pasta, selecione o ambiente virtual do projeto e execute:
python -m unittest -v
São esperados três métodos de teste e uma mensagem final OK, com código de saída zero. A duração e o formato exato das linhas podem variar. No macOS ou Ubuntu, fora de um ambiente virtual, use python3 se esse for o comando do interpretador. Nenhum pacote adicional precisa ser instalado.
assertEqual compara o resultado com o valor esperado; assertRaises exige que a chamada gere a exceção indicada. subTest distingue as três entradas inválidas dentro do mesmo método, sem transformá-las em três métodos separados. Assim, a informação “três testes” não significa que existam somente três entradas verificadas.
Se o executor disser que rodou zero testes, isso não comprova a função. Confira o diretório atual, os prefixos test_ e a presença dos dois arquivos. Se o módulo não for encontrado, verifique seus nomes e mantenha-os juntos na pasta informada.
Conferir se os testes detectam uma alteração incorreta
- Em
expense_math.py, troque temporariamente o retorno porreturn sum(amounts) + 1. Rode novamente o mesmo comando. O teste normal e o teste da lista vazia devem falhar, pois os resultados ficam uma unidade acima do contrato. - Restaure
return sum(amounts)e confirme que os três testes voltam a terminar comOK. - Em outra comparação temporária, remova o
ife seuraise, deixando a função retornar a soma sem validar. O método de entradas inválidas deve detectar o problema. Alguns casos falham por não gerar a exceção esperada; a string pode gerarTypeError, que também é um resultado incorreto para o contrato definido. - Restaure toda a implementação e confirme novamente o resultado correto. Não deixe a alteração defeituosa no arquivo final.
Essa alteração intencional verifica se a suíte protege as regras pretendidas. Uma suíte que sempre termina com sucesso, inclusive com uma implementação sabidamente incorreta, precisa ser investigada. Ainda assim, capturar essas alterações não prova todos os comportamentos possíveis; amplie os casos quando novas regras ou defeitos reais justificarem isso.
Separar testes unitários e de integração
Os testes anteriores verificam a função isolada. Eles não demonstram que o cabeçalho de um CSV, o diretório de trabalho ou uma conexão de banco esteja correto. Um teste de integração combina componentes e verifica seus efeitos observáveis, como arquivos produzidos e código de saída, usando dados independentes dos registros de produção.
O programa abaixo depende do arquivo completo csv_report.py do projeto de validação e relatórios CSV. Salve esse arquivo ao lado dos arquivos desta prática, sem executá-lo manualmente nesse diretório. Ele lê expenses-input.csv, produz report.csv e rejected.csv e devolve código 1 quando rejeita registros.
Salve o código Python completo abaixo como check_csv.py e execute python check_csv.py, sem a opção de otimização -O. O script usa instruções assert, que podem ser removidas na execução otimizada.
import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("csv_report.py").resolve()
with tempfile.TemporaryDirectory() as folder:
work = Path(folder)
source = work / "expenses-input.csv"
source.write_text("id,category,amount\ne1,food,8000\ne1,food,8000\n", encoding="utf-8")
result = subprocess.run([sys.executable, str(program)], cwd=work, capture_output=True, text=True)
assert result.returncode == 1
assert "food,8000" in (work / "report.csv").read_text(encoding="utf-8")
assert "duplicate id" in (work / "rejected.csv").read_text(encoding="utf-8")
print("CSV integration checks passed")
Resultado esperado:
CSV integration checks passed
A entrada contém o mesmo identificador duas vezes. O primeiro registro válido é aceito, o segundo é rejeitado; por isso, o total food é 8000 e o código de saída esperado é 1, mesmo havendo um relatório. O teste confere esses três sinais juntos, em vez de interpretar a existência de um arquivo como sucesso completo.
Path("csv_report.py").resolve() identifica o programa antes de trocar o diretório do subprocesso. O parâmetro cwd=work faz com que os arquivos relativos sejam lidos e gravados na pasta temporária. O with de TemporaryDirectory remove essa pasta ao sair normalmente ou por exceção, sem sobrescrever os relatórios do diretório da prática.
Se csv_report.py estiver ausente ou incompatível, investigue a dependência antes de mudar as condições esperadas. Para ampliar a suíte, inclua entradas controladas para cabeçalhos incorretos e saídas já existentes. Registre a versão do interpretador, os comandos e quais casos realmente foram executados; os resultados desses testes não abrangem interfaces de instalação ou ambientes que você não utilizou.




