Python Testes
A Ana escreveu uma função que calcula o preço com desconto. Para saber se ela funciona, rodou o programa, olhou o resultado e pensou "parece certo". Uma semana depois, mexeu na função para aceitar outra coisa e… será que o desconto antigo continua certo? Ela teria de conferir tudo de novo, de cabeça. Testes automáticos resolvem isso: pequenos programas que conferem os seus programas, sozinhos, em um segundo, quantas vezes você quiser.
- O que é um teste: uma pergunta cuja resposta você já sabe.
- A conferir com
assert, e o que oAssertionErrorquer dizer. - A organizar testes com o módulo
unittest:TestCase,assertEqual,assertRaisese companhia. - A rodar tudo com
python3 -m unitteste a ler o relatório: os pontos, oF, oE. - A escrever exemplos nas docstrings que viram testes, com
doctest. - Como é o
pytest, a ferramenta de fora mais usada. - Os enganos comuns: arquivo com o nome errado, método sem
test, semself, centavos que não fecham.
A ideia: provar a receita antes de servir
Na padaria do bairro, a confeiteira não serve um bolo novo sem provar. E ela prova sempre do mesmo jeito: um pedaço, "está doce o bastante? a massa está fofa?". Ela sabe de antemão como o bolo certo tem de ser, e compara.
Um teste é isso: você chama a sua função com valores que você já sabe a resposta (100 reais com 10% de desconto têm de dar 90) e compara o que a função devolveu com o que devia devolver. Se bater, o teste passa. Se não bater, o teste falha e avisa onde.
| Ferramenta | O que é | Vem com o Python? |
|---|---|---|
assert | Uma palavra-chave: confere uma condição numa linha | Sim (é da própria linguagem) |
unittest | Um módulo para organizar muitos testes e rodar todos de uma vez | Sim (biblioteca padrão) |
doctest | Um módulo que testa os exemplos escritos nas docstrings | Sim (biblioteca padrão) |
pytest | Uma ferramenta de terceiros, muito popular | Não: instala-se com o pip |
1. A conferência mais simples: assert
Primeiro, o jeito "olhômetro": rodar e olhar.
def preco_com_desconto(preco, porcentagem):
return preco - preco * porcentagem / 100
print(preco_com_desconto(100, 10))
print(preco_com_desconto(50, 20))
$ python3 desconto.py 90.0 40.0
Funciona, mas é você quem tem de olhar e decidir se 90.0 e 40.0 estão certos. O assert ("afirmar", "garantir") faz a comparação por você:
def preco_com_desconto(preco, porcentagem):
return preco - preco * porcentagem / 100
assert preco_com_desconto(100, 10) == 90
assert preco_com_desconto(50, 20) == 40
assert preco_com_desconto(80, 0) == 80
print("Todos os testes passaram.")
- assert
- "Garanto que o que vem a seguir é verdade". Se for, o programa segue em silêncio. Se não for, ele para com um erro.
- preco_com_desconto(100, 10) == 90
- A condição: a função, chamada com 100 e 10, tem de devolver 90. O
==compara (veja Comparações).90.0 == 90éTrue: umfloate umintcom o mesmo valor são iguais.
$ python3 com_assert.py Todos os testes passaram.
Os três assert passaram calados, e o print do fim rodou. Agora, o que acontece se a função tiver um erro? A Ana, mexendo no código, esqueceu de multiplicar pelo preço:
def preco_com_desconto(preco, porcentagem):
return preco - porcentagem / 100 # esqueci de multiplicar pelo preço
assert preco_com_desconto(100, 10) == 90
print("Todos os testes passaram.")
$ python3 assert_falha.py
Traceback (most recent call last):
File "/home/ana/assert_falha.py", line 4, in <module>
assert preco_com_desconto(100, 10) == 90
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
- File "/home/ana/assert_falha.py", line 4
- A linha do
assertque não se confirmou. - ^^^^^^^^^
- As setinhas marcam a condição que deu
False. - AssertionError
- "Erro de afirmação": o que você garantiu não era verdade. O programa parou ali; o
printnunca rodou.
O AssertionError sozinho não diz quanto a função devolveu. Dá para pôr uma mensagem depois de uma vírgula:
def preco_com_desconto(preco, porcentagem):
return preco - porcentagem / 100
resultado = preco_com_desconto(100, 10)
assert resultado == 90, f"esperava 90, veio {resultado}"
$ python3 assert_msg.py
Traceback (most recent call last):
File "/home/ana/assert_msg.py", line 5, in <module>
assert resultado == 90, f"esperava 90, veio {resultado}"
^^^^^^^^^^^^^^^
AssertionError: esperava 90, veio 99.9
Agora a pista está na mensagem: veio 99.9, ou seja, a função tirou só 0,10 do preço (10 / 100), e não 10%.
assert pode ser desligado
Quem roda o Python com a opção -O ("otimizar") faz todos os assert sumirem:
$ python3 assert_O.py
Traceback (most recent call last):
File "/home/ana/assert_O.py", line 1, in <module>
assert 1 == 2, "isso é falso"
^^^^^^
AssertionError: isso é falso
$ python3 -O assert_O.py
cheguei ao fim
Por isso, o assert serve para testes e para conferências do programador, nunca para validar o que o usuário digitou (para isso existem o if e o raise).
Missão: a função media() calcula a média das notas da turma, mas o primeiro assert falha. Leia o erro, descubra o engano na função e conserte (não mexa nos assert).
def media(notas):
return sum(notas) / 2
assert media([8, 6]) == 7
assert media([10, 7, 4]) == 7
assert media([5]) == 5
print("Todos os testes passaram.")
def media(notas):
return sum(notas) / len(notas)
assert media([8, 6]) == 7
assert media([10, 7, 4]) == 7
assert media([5]) == 5
print("Todos os testes passaram.")
(Repare: com duas notas, / 2 até acerta. Foi preciso um teste com três notas para pegar o erro. Bons testes experimentam casos diferentes.)
2. Organizando os testes: unittest
Com assert solto, o primeiro teste que falha para o programa, e você não fica sabendo dos outros. O módulo unittest ("teste de unidade": cada teste confere uma pequena parte, uma unidade) roda todos os testes, conta quantos passaram e mostra um relatório de cada falha.
O costume é separar em dois arquivos: o código de verdade e os testes dele. O código da lojinha:
"""Funções da lojinha da Ana."""
def preco_com_desconto(preco, porcentagem):
"""Devolve o preço depois de aplicar o desconto (em %)."""
if not 0 <= porcentagem <= 100:
raise ValueError("a porcentagem deve ficar entre 0 e 100")
return preco - preco * porcentagem / 100
def total_do_carrinho(itens):
"""Soma preço × quantidade de cada item do carrinho."""
return sum(preco * quantidade for preco, quantidade in itens)
def etiqueta(nome, preco):
"""Monta o texto da etiqueta de preço."""
return f"{nome.upper()} - R$ {preco:.2f}".replace(".", ",")
E os testes, num arquivo cujo nome começa com test:
import unittest
from loja import etiqueta, preco_com_desconto, total_do_carrinho
class TestDesconto(unittest.TestCase):
def test_dez_por_cento(self):
self.assertEqual(preco_com_desconto(100, 10), 90)
def test_sem_desconto(self):
self.assertEqual(preco_com_desconto(80, 0), 80)
def test_porcentagem_invalida(self):
with self.assertRaises(ValueError):
preco_com_desconto(100, 150)
class TestCarrinho(unittest.TestCase):
def test_carrinho_vazio(self):
self.assertEqual(total_do_carrinho([]), 0)
def test_dois_itens(self):
self.assertEqual(total_do_carrinho([(4.50, 2), (10.00, 1)]), 19.00)
class TestEtiqueta(unittest.TestCase):
def test_etiqueta(self):
self.assertEqual(etiqueta("café", 18.9), "CAFÉ - R$ 18,90")
def test_comeca_com_o_nome(self):
self.assertStartsWith(etiqueta("arroz", 27.9), "ARROZ")
if __name__ == "__main__":
unittest.main()
- from loja import …
- Os testes importam as funções do
loja.py, como qualquer módulo seu. Os dois arquivos ficam na mesma pasta. - class TestDesconto(unittest.TestCase):
- Uma classe que herda de
unittest.TestCase("caso de teste"). Ela é só uma caixa para juntar testes do mesmo assunto. Você não precisa saber criar classes para usá-la: copie a forma. - def test_dez_por_cento(self):
- Cada teste é um método cujo nome começa com
test. É assim que ounittesto encontra. Oselfé obrigatório. - self.assertEqual(a, b)
- "Confira que
aé igual ab". Se não for, o teste falha e o relatório mostra os dois valores. - with self.assertRaises(ValueError):
- "Confira que o código dentro do bloco lança um
ValueError". Aqui, o teste passa quando dá erro: uma porcentagem de 150% tem de ser recusada. - self.assertStartsWith(texto, início) 3.14
- "Confira que o texto começa com…". É novo no Python 3.14, junto com
assertEndsWith; num Python mais antigo, useself.assertTrue(texto.startswith("ARROZ")). - unittest.main()
- Roda os testes do arquivo quando você faz
python3 test_loja.py. Oif __name__ == "__main__":faz isso acontecer só quando o arquivo é rodado direto, e não quando é importado.
Rodando: python3 -m unittest
Na pasta dos dois arquivos, o comando python3 -m unittest procura sozinho todos os arquivos test*.py, todas as classes de teste e todos os métodos test…, e roda tudo:
$ python3 -m unittest ....... ---------------------------------------------------------------------- Ran 7 tests in 0.000s OK
- .......
- Um ponto para cada teste que passou. Sete testes, sete pontos.
- Ran 7 tests in 0.000s
- "Rodei 7 testes em 0,000 segundo." O tempo muda de uma vez para outra.
- OK
- Todos passaram. No terminal do Python 3.14, os pontos e o
OKaparecem em verde 3.14.
Com -v ("verboso", falante), aparece o nome de cada teste:
$ python3 -m unittest -v test_carrinho_vazio (test_loja.TestCarrinho.test_carrinho_vazio) ... ok test_dois_itens (test_loja.TestCarrinho.test_dois_itens) ... ok test_dez_por_cento (test_loja.TestDesconto.test_dez_por_cento) ... ok test_porcentagem_invalida (test_loja.TestDesconto.test_porcentagem_invalida) ... ok test_sem_desconto (test_loja.TestDesconto.test_sem_desconto) ... ok test_comeca_com_o_nome (test_loja.TestEtiqueta.test_comeca_com_o_nome) ... ok test_etiqueta (test_loja.TestEtiqueta.test_etiqueta) ... ok ---------------------------------------------------------------------- Ran 7 tests in 0.000s OK
Cada linha diz o teste, o arquivo, a classe e o resultado. (O unittest roda as classes e os testes em ordem alfabética, e não na ordem do arquivo; por isso um teste nunca deve depender de outro.)
Lendo o relatório quando algo falha
Agora a Ana "melhorou" o loja.py e, sem perceber, a soma do carrinho passou a ignorar a quantidade:
def total_do_carrinho(itens):
"""Soma preço × quantidade de cada item do carrinho."""
return sum(preco for preco, quantidade in itens) # esqueceu a quantidade
E, num teste, ela escreveu a porcentagem entre aspas: preco_com_desconto(80, "0"). Rodando os testes de novo:
$ python3 -m unittest
.F..E..
======================================================================
ERROR: test_sem_desconto (test_loja.TestDesconto.test_sem_desconto)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/ana/test_loja.py", line 12, in test_sem_desconto
self.assertEqual(preco_com_desconto(80, "0"), 80)
~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/home/ana/loja.py", line 6, in preco_com_desconto
if not 0 <= porcentagem <= 100:
^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '<=' not supported between instances of 'int' and 'str'
======================================================================
FAIL: test_dois_itens (test_loja.TestCarrinho.test_dois_itens)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/ana/test_loja.py", line 25, in test_dois_itens
self.assertEqual(total_do_carrinho([(4.50, 2), (10.00, 1)]), 19.00)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 14.5 != 19.0
----------------------------------------------------------------------
Ran 7 tests in 0.001s
FAILED (failures=1, errors=1)
.F..E..: um sinal por teste, na ordem em que rodaram..passou;F(failure, "falha") quer dizer que umassert…não se confirmou;E(error, "erro") quer dizer que o teste nem chegou ao fim, porque estourou uma exceção no meio.ERROR: test_sem_desconto: o traceback mostra o caminho, de cima para baixo: o teste (linha 12 dotest_loja.py) chamou a função, e dentro dela (linha 6 doloja.py) a comparação0 <= "0"deuTypeError: "<=não funciona entreintestr". O erro está no teste, que passou um texto.FAIL: test_dois_itens: aqui a função rodou até o fim, mas o resultado não bateu:14.5 != 19.0("14,5 é diferente de 19,0"). Do lado esquerdo, o que a função devolveu; do direito, o esperado. 4,50 + 10,00 = 14,50: a quantidade foi esquecida. O erro está no código.FAILED (failures=1, errors=1): o resumo final, "falhou: uma falha e um erro". Os outros cinco testes passaram e foram contados mesmo assim.
Um F diz "a resposta veio errada": olhe o valor que veio, e descubra quem errou a conta (o código ou o valor esperado no teste). Um E diz "o teste quebrou no meio": leia o traceback como qualquer outro erro do Python (veja Lendo uma Mensagem de Erro).
Outras formas de rodar
$ python3 test_loja.py TestDesconto
..E
======================================================================
ERROR: test_sem_desconto (__main__.TestDesconto.test_sem_desconto)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/ana/test_loja.py", line 12, in test_sem_desconto
self.assertEqual(preco_com_desconto(80, "0"), 80)
~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/home/ana/loja.py", line 6, in preco_com_desconto
if not 0 <= porcentagem <= 100:
^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '<=' not supported between instances of 'int' and 'str'
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (errors=1)
Rodando o arquivo direto (graças ao unittest.main()), e só a classe TestDesconto: 3 testes. O arquivo aparece como __main__, porque foi ele que você mandou rodar.
| Comando | O que roda |
|---|---|
python3 -m unittest | Todos os arquivos test*.py da pasta atual (e das subpastas que são pacotes) |
python3 -m unittest -v | O mesmo, com o nome de cada teste |
python3 -m unittest test_loja | Só o arquivo test_loja.py (sem o .py) |
python3 -m unittest test_loja.TestDesconto | Só uma classe |
python3 -m unittest test_loja.TestDesconto.test_dez_por_cento | Só um teste |
python3 test_loja.py | O arquivo, se ele tiver unittest.main() no fim |
O comando termina com o código de saída 0 quando tudo passa e 1 quando algo falha (o echo $? mostra o código; veja os e sys). É assim que outros programas, como os servidores que testam o código a cada mudança, sabem se deu certo.
Missão: a função parcelas() já tem um teste. Escreva mais dois: um que confira que parcelas(90, 3) dá 30.0, e outro, com assertRaises, que confira que parcelas(90, 0) lança ValueError. O relatório tem de dizer Ran 3 tests e OK.
import unittest
def parcelas(total, vezes):
if vezes < 1:
raise ValueError("o número de parcelas deve ser pelo menos 1")
return total / vezes
class TestParcelas(unittest.TestCase):
def test_duas_vezes(self):
self.assertEqual(parcelas(100, 2), 50)
# escreva aqui os outros dois testes
unittest.main()
import unittest
def parcelas(total, vezes):
if vezes < 1:
raise ValueError("o número de parcelas deve ser pelo menos 1")
return total / vezes
class TestParcelas(unittest.TestCase):
def test_duas_vezes(self):
self.assertEqual(parcelas(100, 2), 50)
def test_tres_vezes(self):
self.assertEqual(parcelas(90, 3), 30.0)
def test_zero_vezes(self):
with self.assertRaises(ValueError):
parcelas(90, 0)
unittest.main()
No editor, a função e os testes ficam no mesmo arquivo, e o unittest.main() no fim roda os testes. No seu computador, prefira separar em loja.py e test_loja.py.
As conferências mais usadas
| Método | Passa quando… |
|---|---|
assertEqual(a, b) / assertNotEqual(a, b) | a == b / a != b |
assertTrue(x) / assertFalse(x) | x é verdadeiro / falso |
assertIs(a, b) / assertIsNone(x) | a is b / x is None |
assertIn(a, b) / assertNotIn(a, b) | a in b / a not in b |
assertIsInstance(x, tipo) | x é do tipo dado |
assertAlmostEqual(a, b) | a e b são iguais até a 7ª casa decimal (para float) |
assertGreater(a, b) / assertLess(a, b) | a > b / a < b |
with assertRaises(Erro): | O bloco lança aquele erro |
assertStartsWith(t, s) / assertEndsWith(t, s) 3.14 | O texto t começa / termina com s |
assertHasAttr(obj, nome) 3.14 | O objeto tem o atributo com esse nome |
Por que não usar assert dentro do TestCase? Funciona, mas os métodos assert… dão mensagens melhores: o assertEqual mostra os dois valores (14.5 != 19.0), o assertIn mostra o item e a lista.
3. Testes nos exemplos da docstring: doctest
Lembra das docstrings, os textos de explicação no começo das funções? Muita gente põe nelas um exemplo de uso, copiado do modo interativo (>>>). O módulo doctest transforma esses exemplos em testes: ele roda cada linha com >>> e confere se a resposta é exatamente a que está escrita embaixo.
def preco_com_desconto(preco, porcentagem):
"""Devolve o preço depois de aplicar o desconto (em %).
>>> preco_com_desconto(100, 10)
90.0
>>> preco_com_desconto(50, 20)
40.0
>>> preco_com_desconto(100, 150)
Traceback (most recent call last):
...
ValueError: a porcentagem deve ficar entre 0 e 100
"""
if not 0 <= porcentagem <= 100:
raise ValueError("a porcentagem deve ficar entre 0 e 100")
return preco - preco * porcentagem / 100
def parcelas(total, vezes):
"""Divide o total em parcelas iguais, arredondadas em centavos.
>>> parcelas(100, 4)
[25.0, 25.0, 25.0, 25.0]
>>> parcelas(10, 3)
[3.33, 3.33, 3.33]
"""
return [round(total / vezes, 2)] * vezes
if __name__ == "__main__":
import doctest
doctest.testmod()
- >>> preco_com_desconto(100, 10)
- O que seria digitado no modo interativo. O
>>>e um espaço, depois o código. - 90.0
- A resposta esperada, como o Python a mostraria:
90.0, e não90. Odoctestcompara o texto. - Traceback (most recent call last): … ValueError: …
- Para esperar um erro, escreva a primeira linha do traceback,
...no lugar do meio, e a última linha. - doctest.testmod()
- "Teste o módulo": roda os exemplos de todas as docstrings deste arquivo.
$ python3 -m doctest precos.py $ echo $? 0 $ python3 precos.py
Silêncio quer dizer que tudo passou. Tanto python3 -m doctest precos.py quanto python3 precos.py (que chama o testmod()) não mostram nada. Para ver o que ele fez, use -v:
$ python3 -m doctest -v precos.py
Trying:
parcelas(100, 4)
Expecting:
[25.0, 25.0, 25.0, 25.0]
ok
Trying:
parcelas(10, 3)
Expecting:
[3.33, 3.33, 3.33]
ok
Trying:
preco_com_desconto(100, 10)
Expecting:
90.0
ok
Trying:
preco_com_desconto(50, 20)
Expecting:
40.0
ok
Trying:
preco_com_desconto(100, 150)
Expecting:
Traceback (most recent call last):
...
ValueError: a porcentagem deve ficar entre 0 e 100
ok
1 item had no tests:
precos
2 items passed all tests:
2 tests in precos.parcelas
3 tests in precos.preco_com_desconto
5 tests in 3 items.
5 passed.
Test passed.
"Tentando… Esperando… ok", para cada exemplo. No fim: "1 item não tinha testes" (o próprio módulo, que não tem docstring), "2 itens passaram em todos os testes", "5 testes, 5 passaram".
Quando um exemplo não bate
A Ana reparou que 3 parcelas de 3,33 dão 9,99, e não 10. Ela escreveu na docstring o que queria (a última parcela com o centavo que sobra) e, por descuido, escreveu 25 em vez de 25.0 no outro exemplo:
def parcelas(total, vezes):
"""Divide o total em parcelas iguais, arredondadas em centavos.
>>> parcelas(10, 3)
[3.33, 3.33, 3.34]
>>> parcelas(100, 4)
[25, 25, 25, 25]
"""
return [round(total / vezes, 2)] * vezes
$ python3 -m doctest precos_erro.py
**********************************************************************
File "/home/ana/precos_erro.py", line 4, in precos_erro.parcelas
Failed example:
parcelas(10, 3)
Expected:
[3.33, 3.33, 3.34]
Got:
[3.33, 3.33, 3.33]
**********************************************************************
File "/home/ana/precos_erro.py", line 6, in precos_erro.parcelas
Failed example:
parcelas(100, 4)
Expected:
[25, 25, 25, 25]
Got:
[25.0, 25.0, 25.0, 25.0]
**********************************************************************
1 item had failures:
2 of 2 in precos_erro.parcelas
***Test Failed*** 2 failures.
Cada falha diz a linha, o exemplo, o esperado (Expected) e o que veio (Got). A primeira é uma falha "de verdade": a função ainda não faz o que a Ana quer. A segunda é só de escrita: [25, 25, 25, 25] e [25.0, 25.0, 25.0, 25.0] são iguais para o ==, mas não como texto, e o doctest compara texto. É a grande fraqueza dele: um espaço a mais, a ordem dos itens de um conjunto, um endereço de memória (0x7f…), e o teste falha.
Missão: o exemplo da docstring de etiqueta() está com a resposta errada, e o doctest reclama. Rode, veja o que veio (Got) e corrija a resposta esperada na docstring.
def etiqueta(nome, preco):
"""Monta o texto da etiqueta de preço.
>>> etiqueta("café", 18.9)
'Café - R$ 18.90'
"""
return f"{nome.upper()} - R$ {preco:.2f}".replace(".", ",")
import doctest
doctest.testmod(verbose=True)
def etiqueta(nome, preco):
"""Monta o texto da etiqueta de preço.
>>> etiqueta("café", 18.9)
'CAFÉ - R$ 18,90'
"""
return f"{nome.upper()} - R$ {preco:.2f}".replace(".", ",")
import doctest
doctest.testmod(verbose=True)
4. Uma espiada no pytest
O pytest não vem com o Python, mas é a ferramenta de testes mais usada nos projetos de hoje. Nele, um teste é uma função comum com nome começando por test_, e a conferência é um assert simples, sem classe e sem self.assertEqual:
from loja import preco_com_desconto, total_do_carrinho
def test_dez_por_cento():
assert preco_com_desconto(100, 10) == 90
def test_dois_itens():
assert total_do_carrinho([(4.50, 2), (10.00, 1)]) == 19.00
def test_desconto_grande():
assert preco_com_desconto(200, 25) == 140
A Ana instalou o pytest num ambiente virtual (no Ubuntu, o pip fora de um ambiente virtual é bloqueado; veja pip) e rodou. O último teste está errado de propósito (25% de 200 é 50, e 200 − 50 = 150):
$ python3 -m venv .venv
$ .venv/bin/pip install -q pytest
$ .venv/bin/pytest --version
pytest 9.1.1
$ .venv/bin/pytest test_pytest.py
============================= test session starts ==============================
platform linux -- Python 3.14.4, pytest-9.1.1, pluggy-1.7.0
rootdir: /home/ana
collected 3 items
test_pytest.py ..F [100%]
=================================== FAILURES ===================================
_____________________________ test_desconto_grande _____________________________
def test_desconto_grande():
> assert preco_com_desconto(200, 25) == 140
E assert 150.0 == 140
E + where 150.0 = preco_com_desconto(200, 25)
test_pytest.py:13: AssertionError
=========================== short test summary info ============================
FAILED test_pytest.py::test_desconto_grande - assert 150.0 == 140
========================= 1 failed, 2 passed in 0.03s ==========================
Os mesmos pontos e o mesmo F. A diferença está no relatório: o > aponta a linha que falhou, e as linhas com E mostram de onde veio cada valor ("150.0, que é o que preco_com_desconto(200, 25) devolveu"), mesmo com um assert comum. O pytest também roda, sem mudar nada, os testes escritos com unittest (.venv/bin/pytest test_loja.py dá 7 passed). (A versão do pytest e o tempo mudam com o tempo; o que importa é o formato.) As missões deste guia usam o unittest, que vem com o Python.
Formas diferentes de testar
| Forma | Vantagem | Desvantagem | Quando usar |
|---|---|---|---|
assert solto | Uma linha, nada para aprender | Para no primeiro erro; some com -O | Conferências rápidas, exercícios |
unittest | Vem com o Python; roda tudo e mostra um relatório | Mais "cerimônia": classe, self, assertEqual | Os testes de um programa de verdade, sem instalar nada |
doctest | O exemplo da documentação nunca fica desatualizado | Compara texto: frágil com float, espaços, endereços | Exemplos curtos nas docstrings |
pytest | assert simples com relatório detalhado; muitos recursos | Precisa instalar | Projetos maiores; o mais usado no mercado |
Quando dá errado
A Ana guardou os testes num arquivo chamado verifica_loja.py e rodou:
$ python3 -m unittest ---------------------------------------------------------------------- Ran 0 tests in 0.000s NO TESTS RAN $ echo $? 5
"Rodei 0 testes. Nenhum teste rodou." E o código de saída é 5, o jeito do unittest avisar "não achei nada". A descoberta automática só olha arquivos cujo nome casa com test*.py. Como consertar: renomeie para test_loja.py (ou rode o arquivo pelo nome: python3 -m unittest verifica_loja). Curiosidade: testes_loja.py funciona, porque "testes" também começa com "test".
import unittest
from loja import preco_com_desconto
class TestDesconto(unittest.TestCase):
def test_dez_por_cento(self):
self.assertEqual(preco_com_desconto(100, 10), 90)
def verifica_sem_desconto(self):
self.assertEqual(preco_com_desconto(80, 0), 1000) # errado de propósito
$ python3 -m unittest -v test_esquecido test_dez_por_cento (test_esquecido.TestDesconto.test_dez_por_cento) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
OK, mas o segundo teste (que está errado de propósito: 80 sem desconto não dá 1000) nem rodou. O método se chama verifica_…, e o unittest só roda métodos que começam com test. É o erro mais perigoso: tudo parece verde. Como consertar: def test_sem_desconto(self):. Dica: confira se o número em Ran N tests é o número de testes que você escreveu.
self
import unittest
from loja import preco_com_desconto
class TestDesconto(unittest.TestCase):
def test_dez_por_cento():
self.assertEqual(preco_com_desconto(100, 10), 90)
$ python3 -m unittest test_sem_self E ====================================================================== ERROR: test_dez_por_cento (test_sem_self.TestDesconto.test_dez_por_cento) ---------------------------------------------------------------------- TypeError: TestDesconto.test_dez_por_cento() takes 0 positional arguments but 1 was given ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (errors=1)
"test_dez_por_cento() recebe 0 argumentos, mas foi dado 1." O unittest chama o método passando o objeto do teste (é isso que o self recebe), e o método não tinha lugar para ele. Repare que o resultado é E, não F: o teste quebrou antes de conferir qualquer coisa. Como consertar: def test_dez_por_cento(self):.
assertEqual com float
import unittest
from loja import preco_com_desconto
class TestCentavos(unittest.TestCase):
def test_igual(self):
self.assertEqual(0.1 + 0.2, 0.3)
def test_quase_igual(self):
self.assertAlmostEqual(0.1 + 0.2, 0.3)
def test_desconto_quebrado(self):
self.assertAlmostEqual(preco_com_desconto(19.90, 15), 16.915)
$ python3 -m unittest test_centavos
.F.
======================================================================
FAIL: test_igual (test_centavos.TestCentavos.test_igual)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/ana/test_centavos.py", line 9, in test_igual
self.assertEqual(0.1 + 0.2, 0.3)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
AssertionError: 0.30000000000000004 != 0.3
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
Para o computador, 0.1 + 0.2 dá 0.30000000000000004: os números com casas decimais são guardados com um errinho minúsculo (a aula Números explica por quê). O assertEqual exige igualdade exata e falha. Como consertar: para float, use assertAlmostEqual, que compara até a 7ª casa decimal; os outros dois testes passaram assim.
Missão: os testes acharam um erro em total_do_carrinho(). Leia o relatório (o F), descubra o que está errado na função e conserte. Os testes estão certos: não mexa neles.
import unittest
def total_do_carrinho(itens):
"""Soma preço × quantidade de cada item do carrinho."""
total = 0
for preco, quantidade in itens:
total = preco * quantidade
return total
class TestCarrinho(unittest.TestCase):
def test_vazio(self):
self.assertEqual(total_do_carrinho([]), 0)
def test_um_item(self):
self.assertEqual(total_do_carrinho([(4.50, 2)]), 9.00)
def test_tres_itens(self):
self.assertEqual(total_do_carrinho([(4.50, 2), (10.00, 1), (2.00, 3)]), 25.00)
unittest.main()
import unittest
def total_do_carrinho(itens):
"""Soma preço × quantidade de cada item do carrinho."""
total = 0
for preco, quantidade in itens:
total += preco * quantidade
return total
class TestCarrinho(unittest.TestCase):
def test_vazio(self):
self.assertEqual(total_do_carrinho([]), 0)
def test_um_item(self):
self.assertEqual(total_do_carrinho([(4.50, 2)]), 9.00)
def test_tres_itens(self):
self.assertEqual(total_do_carrinho([(4.50, 2), (10.00, 1), (2.00, 3)]), 25.00)
unittest.main()
Referência rápida
| Escrita | O que faz |
|---|---|
assert condição | Para com AssertionError se a condição for falsa |
assert condição, "mensagem" | O mesmo, com uma mensagem |
class TestAlgo(unittest.TestCase): | Uma caixa de testes do unittest |
def test_algo(self): | Um teste (o nome tem de começar com test) |
self.assertEqual(veio, esperado) | Confere igualdade e mostra os dois valores se falhar |
with self.assertRaises(ValueError): | Confere que o bloco lança o erro |
unittest.main() | Roda os testes do arquivo |
python3 -m unittest [-v] [alvo] | Descobre e roda os testes (test*.py) |
>>> código + resposta, na docstring | Um exemplo que o doctest confere |
python3 -m doctest [-v] arquivo.py | Roda os exemplos das docstrings (silêncio = passou) |
doctest.testmod() | O mesmo, de dentro do programa |
| No relatório | Quer dizer |
|---|---|
. | Um teste passou |
F / FAIL: | Falha: um assert… não se confirmou (a resposta veio errada) |
E / ERROR: | Erro: o teste estourou uma exceção antes de terminar |
s | Teste pulado de propósito (@unittest.skip) |
OK / FAILED (failures=…, errors=…) | Tudo passou / quantos falharam |
NO TESTS RAN | Nenhum teste encontrado: confira os nomes (test*.py, test…) |
Expected: / Got: | No doctest: o que estava escrito / o que veio |
Exercícios
1. O método de igualdade
Complete o teste que confere que dobro(4) dá 8:
2. O comando
Complete o comando que descobre e roda todos os testes da pasta:
3. O relatório mostrou ..E.. O que aconteceu no terceiro teste?
4. Num TestCase, qual destes métodos o unittest não roda?
5. Um doctest espera [25, 25] e a função devolve [25.0, 25.0]. O que acontece?
6. Para comparar 0.1 + 0.2 com 0.3 num teste, o melhor é:
Missão final: os testes da agenda
Missão: a função formatar_telefone() recebe só os números de um celular e devolve o telefone formatado. Escreva quatro testes para ela num TestCase: o exemplo do comentário; um telefone que já vem com espaços (" 86999990002 "); um com número de menos (tem de lançar ValueError); e um com letras (também ValueError). O relatório tem de dizer Ran 4 tests e OK.
import unittest
def formatar_telefone(numeros):
"""'86999990001' vira '(86) 99999-0001'."""
numeros = numeros.strip()
if len(numeros) != 11 or not numeros.isdigit():
raise ValueError(f"telefone inválido: {numeros!r}")
return f"({numeros[:2]}) {numeros[2:7]}-{numeros[7:]}"
# escreva aqui a classe com os quatro testes
unittest.main()
import unittest
def formatar_telefone(numeros):
"""'86999990001' vira '(86) 99999-0001'."""
numeros = numeros.strip()
if len(numeros) != 11 or not numeros.isdigit():
raise ValueError(f"telefone inválido: {numeros!r}")
return f"({numeros[:2]}) {numeros[2:7]}-{numeros[7:]}"
class TestTelefone(unittest.TestCase):
def test_formata(self):
self.assertEqual(formatar_telefone("86999990001"), "(86) 99999-0001")
def test_com_espacos(self):
self.assertEqual(formatar_telefone(" 86999990002 "), "(86) 99999-0002")
def test_curto_demais(self):
with self.assertRaises(ValueError):
formatar_telefone("8699999")
def test_com_letras(self):
with self.assertRaises(ValueError):
formatar_telefone("86abc990001")
unittest.main()
Agora faça uma experiência: "estrague" a função (troque numeros[7:] por numeros[8:]) e rode de novo. Os testes pegam o defeito na hora. É para isso que eles servem.
Resumo
- Um teste chama o seu código com valores de resposta conhecida e compara. Passou, silêncio (ou um ponto); falhou, um relatório.
assert condição, "mensagem"é a conferência mais simples; para no primeiro erro e some compython3 -O.unittest: classes que herdam deunittest.TestCase, métodostest…(self), conferênciasself.assertEqual,assertRaises,assertAlmostEqual…python3 -m unittestacha os arquivostest*.pye roda tudo. No relatório:.passou,Fresposta errada,Eexceção no meio. Confira oRan N tests.doctesttesta os exemplos>>>das docstrings, comparando o texto. Silêncio quer dizer que passou.pytest(instalado num venv) usa funções eassertsimples, com um relatório detalhado.