Unidade 8 · a prova

A prova do pão

A receita da unidade 4 precisa aguentar o turno. assert, AssertionError, pytest — sem esperar o caixa apertar o programa por você.

Nesta unidade Rodar e digitar 2 e 3 no input não é método. Um arquivo test_*.py afirma o que a função deve devolver. pytest escreve o try, o except e o relatório. Função testável tem entrada e return claros — não só print.

Não só na mão, não só o outro

Até agora o teste era: rodar, digitar, olhar a tela, de novo. Ou esperar alguém (um curso, um colega) quebrar o seu arquivo. Na vida — projeto próprio ou trabalho — o hábito é outro: além do código que resolve o problema, um pouco de código que afirma que a solução está certa. Automático. Repetível. Sem você na frente do teclado a cada vez.

Não dá para testar infinitos números: o programa nunca pararia. Dá para escolher representantes — o óbvio, o zero, o negativo — e deixar o computador repetir isso sem cansaço.

A mesa quadrada, de novo

Unidade 4: área da mesa quadrada. Lado vezes lado. Arquivo mesa.py. Um main pergunta o lado, chama area, imprime. A função é duas linhas.

def main():
    lado = int(input("lado? "))
    print("área:", area(lado))

def area(n):
    return n * n

if __name__ == "__main__":
    main()

Na mão: lado 2 → 4. Lado 3 → 9. Parece certo. Ainda não testou 0, negativo, nem o resto do infinito. E o if __name__ já está ali de propósito: na unidade 7, importar o arquivo não deve disparar o main. Vamos importar area noutro arquivo.

Um programa só para testar

Convenção: o arquivo se chama test_ + o que você testa. test_mesa.py. Função test_area — testar area. from mesa import area (o prefixo mesa.area cansa; unidade 7).

Primeira versão, sem palavra nova: se o resultado não for o que a matemática manda, imprima o erro. Nada se estiver certo. Silêncio = passou.

from mesa import area

def test_area():
    if area(2) != 4:
        print("2² não deu 4")
    if area(3) != 9:
        print("3² não deu 9")

def main():
    test_area()

if __name__ == "__main__":
    main()

Definir test_area não a executa. Por isso o main, como sempre. python test_mesa.py: tela vazia. Bom sinal — os dois if não dispararam.

Um bug que um teste não vê

Quebre area de propósito: return n + n em vez de n * n. Rode o teste de novo. Só uma mensagem: 3² não deu 9. O caso 2 passou. Por quê?

8.A · sorte

area está com + no lugar de *. O teste do 2 passa e o do 3 falha. Por quê?

Faça a conta com mais e com vezes, para 2 e para 3.
Um caso óbvio mascarou o bug.
Solução: 2+2 acontece de ser 4. Por isso vários representantes.

Dois testes já valem mais que um. Ainda assim: a função tem duas linhas; o arquivo de teste já tem mais. Se forem dez casos, ninguém vai querer escrever isso.

Afirmar, em uma palavra

assert é palavra da linguagem: afirme que isto é verdadeiro. Se for, silêncio. Se a expressão booleana for falsa, AssertionError — traceback, arquivo, número da linha. Menos amigável que o seu print; metade do código.

def test_area():
    assert area(2) == 4
    assert area(3) == 9

Com o bug do +, o traceback aponta assert area(3) == 9. Cabe a você inferir o porquê. Unidade 6: try / except pega erro. Dá para pegar AssertionError e imprimir “3² não deu 9”. Um passo à frente (mensagem clara) e um atrás (mais linhas de novo).

try:
    assert area(2) == 4
except AssertionError:
    print("2² não deu 4")

Copiar isso para 3, −2, −3 e 0: trinta linhas para duas de area. Positivos costumam se comportar igual entre si; negativos, igual entre si; zero pode ser estranho. Não é ciência exata — é instinto do que a função promete. Com o +, 2 e 0 passam (sorte outra vez); 3 e os negativos denunciam. Vários testes pegaram o erro. Ninguém escreve trinta linhas de try para sempre.

8.B · assert

Uma linha assert (área, conta, o que for verdadeiro).

assert expressão_booleana
assert area(2) == 4
Exemplo: assert area(2) == 4

Deixe a ferramenta escrever o resto

Teste de unidade: testar um pedaço — em geral, uma função. Python traz um framework na caixa; pytest é de terceiro, instala com pip install pytest (unidade 7), e costuma ser mais curto. Convenção: ele acha test_*.py e def test_..., chama sozinho, pega o AssertionError, formata o relatório. Você não escreve main, nem if __name__, nem try, nem print no arquivo de teste.

from mesa import area

def test_area():
    assert area(2) == 4
    assert area(3) == 9
    assert area(-2) == 4
    assert area(-3) == 9
    assert area(0) == 0

Oito linhas. Rode pytest test_mesa.py — não python. Com o bug: um F vermelho (fail). No bloco FAILURES, a primeira afirmação do 2 não tem E embaixo; a do 3 tem. O relatório mostra algo como 6 == 9: area(3) devolveu 6. Chapéu de detetive: na linha do return, mais em vez de vezes. Corrija para *, rode de novo: um ponto verde. Cem por cento destes testes — não prova que o código é perfeito; prova que passou o que você pediu. Falta o 1000, a string, o que o chefe vai digitar. A obrigação de escolher os casos é sua.

8.C · nome

O pytest encontra testes pelo quê?

Há um contrato de nome, não de comentário.
test_mesa.py e def test_area.
Solução: prefixo test_ no arquivo e na função.

O que é testável

E se no input alguém digitar bolo em vez de um inteiro? int("bolo") é ValueError (unidade 6). Isso está no main. Estes testes não exercitam o main — e isso é de propósito. area tem parâmetro e return: você controla o que entra. Por isso fatie o programa em funções pequenas. Tudo no main, ou tudo solto no arquivo, é difícil de cravar num assert.

Dá para afirmar que area("bolo") levanta TypeError (não dá para fazer "bolo" * "bolo"). Não é assert area("bolo") == …. É outra função da biblioteca pytest.

import pytest
from mesa import area

def test_str():
    with pytest.raises(TypeError):
        area("bolo")
8.D · raises

Como o teste diz “eu espero um TypeError se passarem texto para area”?

assert compara valores. Exceção é outro contrato.
A biblioteca pytest tem raises.
Solução: with pytest.raises(TypeError).

Um assert que para os outros

Cinco asserts na mesma test_area: o primeiro que falha (o 3, com o bug) aborta a função. Os negativos nem rodam. Uma falha já prova que está quebrado — mas várias pistas de uma vez aceleram o diagnóstico. Separe: test_positivo, test_negativo, test_zero. pytest chama as três mesmo se uma falhar. Relatório: dois F e um ponto; ou F.F na linha curta. Zero passou (0+0), positivo falhou no 3, negativo falhou no −2. A conta aponta para o +.

Pode usar for e lista dentro do teste. Continua sendo um teste para o pytest. O ideal é teste pequeno o bastante para um humano olhar e dizer: isso está certo. Senão você vai precisar de teste do teste.

8.E · várias funções

Por que partir os asserts em test_positivo, test_negativo e test_zero?

AssertionError encerra aquela def.
Mais funções = mais pistas no relatório.
Solução: o pytest tenta cada test_ mesmo se outra falhou.
8.F · casos

Pelo menos um assert … == … (pode cobrir 2 e 0, ou 3 e −3).

assert algo == valor
Um segundo assert no zero ou no negativo.
Exemplo:
assert 2 * 2 == 4
assert 0 * 0 == 0

O recado que não devolve

Unidade 7: recados.py, função ola. Se ela só faz print(f"olá, {nome}"), não tem return. assert ola("Ana") == "olá, Ana" compara com None — o print é efeito na tela, como na unidade 1. Por isso, quando o programa cresce, o melhor é a função devolver a frase e o main imprimir.

def ola(para="fila"):
    return f"olá, {para}"

def main():
    print(ola(input("nome? ").strip()))

if __name__ == "__main__":
    main()
# test_recados.py
from recados import ola

def test_padrao():
    assert ola() == "olá, fila"

def test_argumento():
    assert ola("Ana") == "olá, Ana"

Duas funções: padrão e argumento. Se uma quebrar, o relatório diz qual. Loop com nomes da fila (Ana, Bia, Leo) cabe dentro de um teste — desde que continue óbvio.

8.G · return

Qual função é mais fácil de testar com assert … == …?

assert compara valores, não o que apareceu no terminal.
return entrega um objeto; print é efeito.
Solução: a que devolve com return.

Uma pasta de testes

Poucos testes: um arquivo basta. Muitos: uma pasta, por exemplo testes/, com vários test_*.py. pytest aceita pytest testes e procura dentro. Para o Python tratar a pasta como pacote (módulo numa pasta, unidade 7), crie lá um __init__.py mesmo vazio. É o sinal. Aí o import from mesa import area continua fazendo sentido conforme a árvore de pastas do projeto.

O ganho real, em equipe: alguém muda mesa.py; os testes que você deixou gritam se a área da mesa deixou de ser lado vezes lado. Não apague o teste para “ficar verde”.

8.H · pacote

Para o pytest rodar uma pasta de testes, o que aquele __init__.py vazio faz?

Unidade 7: pacote é módulo numa pasta.
O arquivo pode estar vazio.
Solução: marca a pasta como pacote.

Oficina · test_area

Problema da unidade Uma função test_... com assert sobre a área da mesa (lado * lado) ou sobre uma conta equivalente. No computador: pip install pytest se faltar, e pytest test_mesa.py. Aqui, cole o teste.
Oficina 8

Cole def test_... com assert.

O nome da função começa com test_
assert 2 * 2 == 4
Modelo:
def test_area():
    assert 2 * 2 == 4
    assert 3 * 3 == 9