3.1. Pytest para cientista de dados#
Fonte dos vídeos
Todos os vídeos são da própria autora do livro original.

3.1.1. O que é o Pytest?#
Pytest é um framework que facilita a escrita de testes unitários em Python. A autora original do livro afirma gostar do pytest pois permite a escrita de teses com pouco código, e eu, autor da tradução, concordo plenamente com ela. Se você já está familiarizado com testes unitários, pytest é uma grande ferramenta para sair da inércia.
Para instalar o pytest, basta rodar no terminal:
pip install pytest
Caso estiver usando o poetry:
poetry add pytest
Para testar a função mostrada anteriormente, nós podemos simplesmente criar outra função que se inicia com test_
seguida do nome da função que desejamos testar, que é extract_sentiment
.
# sentiment.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
Nota
Até o momento da tradução deste livro, a biblioteca textblob
não apresenta suporte à língua portuguesa.
Dentro da função de teste, nós usamos a função extract_sentiment
em uma frase de exemplo: “I think today will be a great day”, que em português significa “Eu acredito que hoje será um grande dia”. Nós, então, usamos um assert sentiment > 0
para garantir que aquele sentimento é positivo, pois esta é nossa expectativa.
E tudo pronto! Hora de rodar nosso teste.
Se o nome do nosso script for sentiment.py
, podemos rodar:
pytest sentiment.py
O pytest irá procurar e rodar por todas as funções que começam com test_
no script. A saída do teste acima será parecida com esta:
========================================== 1 passed in 0.68s ===========================================
Legal, não? Não precisamos nem especificar qual função rodar. Desde que colocamos test_
no começo da função de teste, o pytest detecta e executa automaticamente aquela função. Não precisamos nem sequer importar o pytest no script para rodar os testes.
Qual será a saída caso algum dos testes do pytest falhar?
#sentiment.py
def test_extract_sentiment():
text = "I think today will be a great day"
sentiment = extract_sentiment(text)
assert sentiment < 0
$ pytest sentiment.py
_______________________________________ test_extract_sentiment ________________________________________
def test_extract_sentiment():
text = "I think today will be a great day"
sentiment = extract_sentiment(text)
> assert sentiment < 0
E assert 0.8 < 0
========================================== 1 failed in 0.84s ===========================================
A partir da saída acima, podemos perceber que o teste falhou porque o sentimento retornado pela função é 0.8 e não é menor do que zero! Nós agora somos capazes de não só saber se a nossa função funciona como esperado, mas também o porquê ela não funciona. Com este insight maravilhoso, nós conseguimos consertar a nossa função para que ela funcione de acordo com o esperado.
3.1.2. Múltiplos testes para a mesma função#
Nós gostaríamos de testar a nossa função com outros exemplos. Como seriam os nomes destes novos testes?
O nome do segundo teste poderia ser algo do tipo test_extract_sentiment_2
ou test_extract_sentiment_negative
caso o teste seja com um texto com sentimento negativo. O importante é que qualquer nome significativo é valido, desde que comece com test_
:
#sentiment.py
def test_extract_sentiment_positive():
# 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
def test_extract_sentiment_negative():
# Tradução: Isso não vai acabar bem
text = "I do not think this will turn out well"
sentiment = extract_sentiment(text)
assert sentiment < 0
$ pytest sentiment.py
___________________________________ test_extract_sentiment_negative ____________________________________
def test_extract_sentiment_negative():
# Tradução: Isso não vai acabar bem
text = "I do not think this will turn out well"
sentiment = extract_sentiment(text)
> assert sentiment < 0
E assert 0.0 < 0
===================================== 1 failed, 1 passed in 0.80s ======================================
O resultado dos testes nos mostra que apenas um dos testes passou e o outro falhou, e sabemos o porquê. Esperávamos que a saída da sentença “I do not think this will turn out well”, que em português significa “Isso não vai acabar bem”, fosse negativo, mas a função retornou 0, classificando a frase como neutra.
Esse tipo de análise nos indica que a função pode não ter uma assertividade de 100 % e, portanto, devemos ter cuidado ao usá-la para extrair sentimentos de um texto.
3.1.3. Parametrização: combinando testes#
O exemplo acima mostra 2 testes para a mesma função. Será que não podemos combinar os 2 exemplos em uma única função de testes? É o que vamos aprender com parametrização de testes.
3.1.3.1. Parametrizando com uma lista de exemplos#
Com o decorador pytest.mark.parametrize()
, podemos executar testes com diferentes exemplos passando uma lista como argumento.
# sentiment.py
from textblob import TextBlob
import pytest
def extract_sentiment(text: str):
'''Extract sentiment using textblob.
Polarity is within range [-1, 1]'''
text = TextBlob(text)
return text.sentiment.polarity
# Traduções: "Eu acredito que hoje será um grande dia", "Isso não vai acabar bem"
testdata = ["I think today will be a great day","I do not think this will turn out well"]
@pytest.mark.parametrize('sample', testdata)
def test_extract_sentiment(sample):
sentiment = extract_sentiment(sample)
assert sentiment > 0
No código acima, nós associamos à variável sample
uma lista de frases, e então, passamos essa variável como argumento da função de teste. Agora cada frase da lista será testada uma de cada vez. Vejamos abaixo:
_____ test_extract_sentiment[I do not think this will turn out well] _____
sample = 'I do not think this will turn out well'
@pytest.mark.parametrize('sample', testdata)
def test_extract_sentiment(sample):
sentiment = extract_sentiment(sample)
> assert sentiment > 0
E assert 0.0 > 0
====================== 1 failed, 1 passed in 0.80s ===================
Usando parametrize()
é possível testar 2 exemplos diferentes na mesma função!
3.1.3.2. Parametrizando uma lista de entradas diferentes com diferentes resultados esperados#
E se nós testássemos diferentes exemplos para ter diferentes saídas? O pytest permite também adicionar exemplos e resultados esperados como argumentos da nossa função de teste!
Por exemplo, a função abaixo checa se um texto contém uma determinada palavra.
def text_contain_word(word: str, text: str):
'''Procura por uma determinada palavra em um texto'''
return word in text
Ela deve retornar True
se o texto ou frase contiver a palavra.
Caso a palavra seja “pato” e o texto for “Há um pato no meio do texto”, nossa expectativa é que a função retorne True
.
Por outro lado, se a palavra for “pato” e o texto agora for “Não há nada aqui”, nossa expectativa é que a função retorne False
.
Vamos usar o parametrize()
, mas com uma lista de tuplas agora.
# process.py
import pytest
def text_contain_word(word: str, text: str):
'''Procura por uma determinada palavra em um texto'''
return word in text
testdata = [
('Há um pato no meio do texto',True),
('Não há nada aqui', False)
]
@pytest.mark.parametrize('sample, expected_output', testdata)
def test_text_contain_word(sample, expected_output):
word = 'pato'
assert text_contain_word(word, sample) == expected_output
A estrutura dos parâmetros da nossa função será parametrize(‘sample, expected_out’, testdata)
com testdata=[(<exemplo1>, <saida1>), (<exemplo2>, <saida2>)
$ pytest process.py
========================================== 2 passed in 0.04s ===========================================
Maravilha! Ambos os testes passaram!
3.1.4. Testando uma função de cada vez#
Conforme o número de testes em nosso script começa a crescer, você pode desejar testar uma única função ao invés de vários ou todos de uma única vez. Isso pode ser feito através da sintaxe pytest arquivo.py::nome_da_funcao_de_teste
.
# process.py
testdata = ["I think today will be a great day","I do not think this will turn out well"]
@pytest.mark.parametrize('sample', testdata)
def test_extract_sentiment(sample):
sentiment = extract_sentiment(sample)
assert sentiment > 0
testdata = [
('Há um pato no meio do texto',True),
('Não há nada aqui', False)
]
@pytest.mark.parametrize('sample, expected_output', testdata)
def test_text_contain_word(sample, expected_output):
word = 'pato'
assert text_contain_word(word, sample) == expected_output
Por exemplo, se quisermos rodar apenas o teste test_text_contain_word
, basta rodar no terminal:
$ pytest process.py::test_text_contain_word
E o pytest executará apenas o teste especificado!
3.1.5. Fixtures: usando os mesmos dados para testar diferentes funções#
E se quiséssemos usar os mesmos dados para testar diferentes funções? Por exemplo, gostaríamos de usar a frase “Today I found a duck and I am happy” para testar se ela contém a palavra “duck” e também se seu sentimento é positivo. A ideia é aplicar as duas funções à mesma frase “Today I found a duck and I am happy”. É quando as fixture
mostram o seu potencial.
As fixtures
do pytest são uma forma de usar os mesmos dados para duas ou mais funções diferentes.
@pytest.fixture
def example_data():
return 'Today I found a duck and I am happy'
def test_extract_sentiment(example_data):
sentiment = extract_sentiment(example_data)
assert sentiment > 0
def test_text_contain_word(example_data):
word = 'duck'
assert text_contain_word(word, example_data) == True
No exemplo acima, criamos uma frase de exemplo com o decorador @pytest.fixture
acima da função example_data
. Essa função transforma example_data
em uma variável com o valor “Today I found a duck and I am happy”
Agora conseguimos usar essa variável como parâmetro de quaisquer outros testes!
3.1.6. Estruture seus projetos#
Por último, mas não menos importante, conforme o nosso código cresce, é ideal que coloquemos as nossas funções de ciência de dados e os seus respectivos testes em duas pastas diferentes. Isso facilita a localização de cada função e do teste correspondente.
Nomeando as funções de teste seja com test_<nome_da_funcao>.py
ou <nome_da_funcao>_test.py
, o pytest encontrará os scripts que começam ou terminam com test
e executará as funções associadas a cada teste.
Há diferentes formas de organizar seus arquivos. Você pode ou organizar seus scripts e funções na mesma pasta ou em duas pastas diferentes, uma com o código-fonte e outra com os testes.
Método 1:
test_structure_example/
├── process.py
└── test_process.py
Método 2:
test_structure_example/
├── src
│ └── process.py
└── tests
└── test_process.py
Tanto eu, autor da tradução, como a autora original recomendamos o método dois, pois torna mais fácil a organização conforme um projeto cresce.
A organização do código e teste no método 2 ficará desta forma:
# src/process.py
from textblob import TextBlob
def extract_sentiment(text: str):
'''Extract sentiment using textblob.
Polarity is within range [-1, 1]'''
text = TextBlob(text)
return text.sentiment.polarity
# tests/test_process.py
import sys
import os.path
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
from src.process import extract_sentiment
import pytest
def test_extract_sentiment():
text = 'Today I found a duck and I am happy'
sentiment = extract_sentiment(text)
assert sentiment > 0
O objetivo do comando sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
é simplesmente adicionar o módulo src
à variável de ambiente PYTHONPATH
para permitir importações dos módulos e funções.
Estando na pasta raiz do projeto (test_structure_example/
), rode pytest tests/test_process.py
ou então pytest test_process.py
.
=========================== 1 passed in 0.69s ============================