Um básico de pytest

pytest-1_abertura

O pytest é um framework que permite criar pequenas rotinas de teste mas que também pode ser usado para dar suporte a uma sofisticada rotina de testes funcionais para aplicações e bibliotecas. Não é a única biblioteca de testes disponível em Python mas é  considerada como a alternativa a mais “pythônica” para se escrever testes.

E isto é uma introdução bem simplificada para quem quer começar a escrever testes para seus programas e não sabe exatamente como iniciar…

Criando uma função

Começando com algo bem simples, uma biblioteca contendo uma função que calcula a média aritmética de uma lista de números, o arquivo “medias.py”.

#!/usr/bin/env python

def media_aritmetica(valores):
    return sum(valores) / len(valores)

Um programa para utilizá-la, o arquivo “programa.py”.

#!/usr/bin/env python
from __future__ import print_function
from medias import media_aritmetica

print(media_aritmetica([1, 2, 3, 4, 5]))

Que sendo algo simples, funciona tal qual o esperado já na primeira tentativa.

$ python programa.py
3.0

E agora é possível instalar o pytest e criar uma rotina de testes.

Instalação do pytest

A instalação é simples, basta recorrer ao pip (ou seu instalador de módulos predileto).

$ pip install pytest
Collecting pytest
...
Installing collected packages: pytest
Successfully installed pytest-3.8.

Claro que é possível, e também recomendável, instalá-lo a partir de um ambiente virtual, como o virtualenv.

Testando a função

Para facilitar um pouco criarei uma classe de testes dentro do arquivo “media.py”, o pytest procurará por qualquer classe iniciada com “Test” e executará todos os métodos dela que começarem com “test”.

class TestMedia:
    def test_media_ok(self):
        assert media_aritmetica([1, 1]) == 1.0

O teste executará a função media_aritmetica() para um conjunto de valores cujo resultado é comparado com o resultado esperado. A verificação será feita através do comando assert que verificará a expressão e provocará uma exceção de AssertionError caso o resultado não seja verdadeiro.

Daí é executar a rotina de testes.

$ pytest -v medias.py 
======================== test session starts =========================
...
collected 1 item

medias.py::TestMedias::test_media_ok PASSED                    [100%]
====================== 1 passed in 0.01 seconds ======================

E verificar que a função passou neste “nosso” primeiro teste. 🙂

Sofisticando um pouco mais os testes

Mas a função tem uma falha, ela não verifica se a lista de números  contém elementos antes de tentar efetuar o cálculo e o resultado é divisão por zero e consequentemente a interrupção do programa.

$ python ./programa.py 
Traceback (most recent call last):
File "./programa.py", line 5, in 
print(media_aritmetica([]))
...
ZeroDivisionError: division by zero

A solução é verificar a quantidade de itens antes de efetivamente efetuar o cálculo.

def media_aritmetica(valores):
    quantidade = len(valores)
    if quantidade:
        return sum(valores) / quantidade
    else:
        return None

Daí é executar novamente a rotina de testes.

$ pytest -v medias.py 
======================== test session starts =========================
...
collected 2 items

medias.py::TestMedias::test_media_com_lista_numerica PASSED    [ 50%]
medias.py::TestMedias::test_media_com_lista_vazia PASSED       [100%]

====================== 2 passed in 0.01 seconds ======================

E pronto, as duas condições da função foram devidamente testadas.

E se algo der errado?

Digamos que acidentalmente o retorno da função seja alterado de None para False. Em tese isto não afetaria o funcionamento do programa mas em Python o False pode ser interpretado como zero¹ criando um resultado errôneo.

O programa continuaria rodando normalmente, exceto que apresentando um comportamento diferente do esperado. Como no outro programa de exemplo, o “alunos.py”.

Retornando None:

$ alunos.py
Adão Nogueira	8.258
Bruno Tavares	sem notas
João da Silva	6.412
Jose Queiroz	8.511

Retornando False:

$ alunos.py
Adão Nogueira	8.258
Bruno Tavares	0.000
João da Silva	6.412
Jose Queiroz	8.511

Porém, a rotina de testes detectaria esta mudança na função…

$ pytest -v medias.py
======================== test session starts =========================
...
collected 2 items

medias.py::TestMedias::test_media_com_lista_numerica PASSED    [ 50%]
medias.py::TestMedias::test_media_com_lista_vazia FAILED       [100%]

============================== FAILURES ==============================
_______________ TestMedias.test_media_com_lista_vazia ________________
...
    def test_media_com_lista_vazia(self):
>       assert media_aritmetica([]) == False
E       assert False == None
E        + where None = media_aritmetica([])

medias.py:20: AssertionError
================= 1 failed, 1 passed in 0.03 seconds =================

…e notificaria o erro imediatamente.

(¹) Abra o console do Python, digite int(False) e veja um 0 aparecer!

Considerações finais

Claro, a rotina que estava sendo verificada era simples, os testes que poderiam ser feitos nela também não poderiam ser diferentes mas, como dito no começo, isto aqui é apenas uma introdução. O pytest tem muito mais recursos do que os que foram demonstrados aqui², conforme pode ser observado na sua documentação e também nos exemplos disponíveis na página do projeto.

Ah sim, os arquivos usados aqui estão no repositório do blog no GitHub.

(²) Acredito que o correto seria escrever “do que o demonstrado aqui”… 🙂

Anúncios