3.2. Quatro dicas úteis para o pytest#

3.2.1. Motivação#

Pytest é o framework ideal para escrever pequenos testes em Python. Nós cobrimos os benefícios de testes unitários e de como começar com o pytest na seção anterior.

Nesta seção, iremos mostrar como:

  • Filtrar os avisos (warnings);

  • Imprimir a saída da função sendo testada;

  • Verificar a performance do seu código;

  • Repetir o teste várias vezes.

3.2.2. Filtrando os avisos (warnings)#

É comum vermos avisos (warnings) informando que uma determinada funcionalidade será desativada no futuro durante a execução dos testes com o pytest. Estes avisos não parecem ser relevantes durante a fase de testes e chegam a ser um tanto quanto desagradáveis, pois poluem muito a tela, quando queremos só debugar o nosso código. Por sorte, é possível filtrar esses avisos quando usamos o pytest.

Vamos usar o exemplo da nossa seção anterior sobre o pytest. Neste exemplo vamos testar se o TextBlob faz previsões acuradas dos sentimentos de um dado texto. Se você ainda não conhece o TextBlob, você pode pesquisar mais sobre a ferramenta neste link.

Nota

Até o momento da tradução deste livro, a biblioteca textblob não apresenta suporte à língua portuguesa. Portanto, os exemplos serão em inglês.

# test_example.py

from textblob import TextBlob

def extract_sentiment(text: str):
        '''Extrai um sentimento usando textblob. 
        Polaridade está no intervalo [-1, 1]'''

        text = TextBlob(text)

        return text.sentiment.polarity

def test_extract_sentiment():

    # Tradução: Eu acredito que hoje será um grande dia
    text = "I think today will be a great day"

    sentiment = extract_sentiment(text)

    assert sentiment > 0

Ao rodarmos esse script no terminal com:

$ pytest test_example.py

… nós veremos os avisos (warnings).

Para filtrar esses avisos, basta criar um arquivo pytest.ini no mesmo diretório em que rodamos o nosso script:

.
├── pytest.ini
└── test_example.py

E então inserir o conteúdo abaixo no arquivo pytest.ini criado.

[pytest]
addopts = -p no:warnings

Pronto! Agora é só executar os testes novamente. Nós veremos uma saída sem aqueles warnings!

3.2.3. Verificar a saída da função sendo testada#

Às vezes desejamos ver algum código intermediário antes do assert no teste. Por que não simplesmente imprimir essa saída intermediária?

Este pequeno ajuste nos permite desenvolver uma função que tenha uma saída conforme a desejada.

Por exemplo, nós podemos dar um print(text.sentiment) e saber exatamente o conteúdo de text.sentiment:

from textblob import TextBlob

def extract_sentiment(text: str):
    '''Extrai um sentimento usando textblob. 
    Polaridade está no intervalo [-1, 1]'''

  text = TextBlob(text)

  print(text.sentiment)

  return text.sentiment.polarity

def test_extract_sentiment():

    text = "I think today will be a great day"

    sentiment = extract_sentiment(text)

    assert sentiment > 0

Apesar dos prints, quando rodamos pytest test_example.py não veremos nenhuma saída diferente. Para mostrar esse nosso print, precisamos incluir -s ao final do nosso comando:

$ pytest test_example.py -s
======================= 1 passed in 0.49s =======================

Para mais informações sobre este e outros comandos, rode pytest -h que a lista de todos os comandos extras do pytest será exibida no terminal.

3.2.4. Repetindo um teste múltiplas vezes#

Às vezes o nosso desejo é rodar o mesmo teste 100 vezes para garantir reprodutibilidade, ou seja, que nossa função sempre funcione conforme esperado.

Um exemplo básico, nós descobrimos agora o método para combinar duas listas em Python.

l1 = [1,2,3]
l2 = [4,5,6]
l1.extend(l2)
l1
[1,2,3,4,5,6]

A partir da saída acima, parece que os valores da lista l2 são sempre inseridos após os valores da lista l1. Mas será que a ordem é sempre essa com quaisquer pares de listas?

Para garantir que a ordem de inserção é preservada quando usamos o método extend, nós podemos criar duas listas aleatórias e testá-las 100 vezes. Se todos os testes passarem, teremos certeza de que o método funciona conforme o esperado.

Para repetir um determinado teste, instale pytest-repeat:

$ pip install pytest-repeat
$ poetry add pytest-repeat

Agora podemos usar pytest.mark.repeat(100) como um decorador no teste que gostaríamos de repetir.

import pytest 
import random 

def extend(l1,l2):
    l1.extend(l2)
    return l1
def test_extend():
    l1 = [1,2,3]
    l2 = [4,5,6]
    res = extend(l1, l2)
    assert res == [1,2,3,4,5,6]
    
@pytest.mark.repeat(100)
def test_extend_random():
  '''Gera listas aleatórias e repete o teste 100 vezes.'''
    l1 = []
    l2 = []
    for _ in range(0,3):
        n = random.randint(1,10)
        l1.append(n)
        n = random.randint(1,10)
        l2.append(n)

    res = extend(l1,l2)
    
    assert res[-(len(l2)):] == l2
$ pytest test_repeat.py 
======================= 101 passed in 0.09s =======================

Maravilha. Agora temos certeza de que a ordem de inserção será sempre preservada usando o método extend(), já que 101 testes rodaram sem falhas.

Se quiser ter ainda mais certeza, aumente o número de repetições para 1000 e veja o resultado. Você pode ler mais sobre o pytest-repeat aqui.

3.2.5. Medindo a performance do código#

Às vezes o nosso desejo é não só que a saída seja igual ao esperado, mas também gostaríamos de comparar a performance entre duas funções diferentes.

Um benchmark é um teste que usamos para medir o quão rápido o código executa.

Por sorte, temos uma biblioteca chamada pytest-benchmark que permite realizar o benchmark do nosso código enquanto testamos as nossas funções com pytest!

Vamos instalar o pytest-benchmark:

$ pip install pytest-benchmark
$ poetry add pytest-benchmark

Vamos usar o exemplo deste artigo, escrito em inglês pela autora original do livro, sobre performance em código Python para mostrar como podemos usar o pytest-benchmark.

Aqui, vamos mensurar o quanto tempo demora para criar uma lista usando o método concat:

# benchmark.py

def concat(len_list=1000):
    l = []
    for i in range(len_list):
        l = l + [i]
    return l 
    
def test_concat(benchmark):

    res = benchmark(concat)

benchmark1

Conforme apresentado no gif acima, podemos obter várias estatísticas de performance como tempo mínimo, máximo, médio e desvio padrão do tempo de execução do método concat.

Podemos ainda inserir parâmetros para a função de teste e repetir o teste 100 vezes para coletar uma acurácia ainda melhor da performance do código.

# benchmark2.py

def concat(len_list=1000):
    l = []
    for i in range(len_list):
        l = l + [i]
    return l 
    
def test_concat(benchmark):

    len_list = 1000 
    res = benchmark.pedantic(concat, kwargs={'len_list': len_list}, iterations=100)
    assert res == list(range(len_list))

benchmark2

Agora vamos usar o pytest-benchmark para comparar múltiplos métodos de criação de listas, todas elas com 1000 elementos (de 0 a 999).

# test_benchmark.py

def concat(len_list):
    l = []
    for i in range(len_list):
        l = l + [i]
    return l 

def append(len_list):
    l = []
    for i in range(len_list):
        l.append(i)
    return l 

def comprehension(len_list):
    l = [i for i in range(len_list)]
    return l 
    
def list_range(len_list):
    l = list(range(len_list))
    return l 

def test_concat(benchmark):

    len_list = 1000
    res = benchmark.pedantic(concat, kwargs={'len_list': len_list}, iterations=100)
    assert res == list(range(len_list))

def test_append(benchmark):

    len_list = 1000
    res = benchmark.pedantic(append, kwargs={'len_list': len_list}, iterations=100)
    assert res == list(range(len_list))

def test_comprehension(benchmark):

    len_list = 1000
    res = benchmark.pedantic(comprehension, kwargs={'len_list': len_list}, iterations=100)
    assert res == list(range(len_list))

def test_list_range(benchmark):
    len_list = 1000
    res = benchmark.pedantic(list_range, kwargs={'len_list': len_list}, iterations=100)
    assert res == list(range(len_list))

A saída é fácil de interpretar e ler!

benchmark3

Vimos que o método concat leva mais tempo do que os outros métodos. Mas quão mais longo ele é?

Podemos usar o parâmetro --benchmark-histogram para visualizar o resultado.

pytest test_benchmark.py --benchmark-histogram

Aviso de possível erro

Caso você obtenha o seguinte erro:

ImportError: (("No module named 'pygal'",), 'Please install pygal and pygaljs or pytest-benchmark[histogram]')

Basta reinstalar o pytest-benchmark acrescentando a extensão histogram desta forma:

pip install pytest-benchmark[histogram]

Ou usando o poetry:

poetry add pytest-benchmark[histogram]

Ou então instalar as bibliotecas sugeridas no erro acima: pygal e pygalsj

Uau! O gráfico mostra claramente que o método concat é muito mais lento que os outros!

Você pode aprender mais sobre o pytest-benchmark aqui.