Exercício
Neste exercício, você usará pytest@pytest.mark.parametrize para testar uma função, incluindo casos que esperam uma exceção. Em seguida, você refatorará um teste baseado em classe, passando da abordagem no estilo xUnit setup_method() e teardown_method() para uma abordagem com fixture. O uso de parametrização e acessórios ajuda você a escrever testes que abrangem mais casos com menos repetição e configuração mais clara.
Para obter ajuda de instalação, consulte a documentação Python para ambientes virtual, a documentação pip para pip install e a documentação pytest para instalando o pytest. Para obter mais detalhes sobre os recursos pytest neste exercício, consulte a documentação pytest para parametrizar funções de teste, declarações sobre exceções esperadas, acessórios, configuração no estilo xUnit e diretórios e arquivos temporários.
Antes de começar
Use Python 3.10 ou posterior, conforme descrito nos pré-requisitos do módulo. Se você já tiver um ambiente virtual com o pytest instalado e ativado, continue para a Etapa 1. Caso contrário, crie e ative um ambiente virtual na pasta em que você criará test_advanced.py.
No Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
No macOS, Linux ou no Subsistema do Windows para Linux:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
O comando de versão deve imprimir uma versão pytest, semelhante a esta saída:
pytest x.y.z
Os comandos de instalação não fixam pytest em uma versão específica. O Pip usa metadados de pacote para escolher uma versão estável do pytest compatível com a versão do Python no ambiente virtual.
Use python -m pytest durante todo este exercício para que o pytest seja executado a partir do ambiente ativo. Se o Windows PowerShell bloquear Activate.ps1, consulte a documentação do Python venv para a opção de política de execução do PowerShell ou execute o Python do ambiente virtual diretamente para os comandos pip e pytest. Por exemplo:
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
Etapa 1 – Adicionar um arquivo com testes para este exercício
Na mesma pasta em que você executará o pytest, crie um novo arquivo de teste chamado test_advanced.py. Não coloque o arquivo dentro da pasta .venv . Adicione o seguinte código:
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")A função
str_to_bool()aceita uma cadeia de caracteres como entrada. RetornaTruepara valores verdadeiros reconhecidos, retornaFalsepara valores falsos reconhecidos, e levanta umValueErrorpara qualquer outro valor.No mesmo arquivo, anexe os testes para a função
str_to_bool(). Use@pytest.mark.parametrizeprimeiro para testar os valores verdadeiros:@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is TrueEm seguida, acrescente outro teste com os valores falsos:
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is FalseAgora há duas funções de teste que abrangem entradas representativas para os valores de retorno de
TrueeFalse.Por fim, acrescente um teste parametrizado para valores que a função não deve aceitar. Use
pytest.raises()como um gerenciador de contexto para que o teste seja aprovado somente quandostr_to_bool()gerar a exceção esperada:@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)O
matchargumento verifica a mensagem de exceção com uma expressão regular. Aqui, o teste verifica apenas a parte estável da mensagem para que ela não dependa do valor exato inválido.
Observação
Para simplificar, este exercício mantém o código em teste e os testes no mesmo arquivo. Em projetos de Python reais, o código do aplicativo e os testes geralmente são separados em diferentes arquivos e diretórios, como um pacote src/ e um diretório tests/.
Etapa 2 – Executar os testes e explorar o relatório
Depois de adicionar os testes, execute pytest e inspecione a saída. Use o sinalizador de verbosidade maior (-v) para que você possa ver cada valor de entrada tratado como um teste separado.
No Windows PowerShell:
python -m pytest -v test_advanced.py
No macOS, Linux ou no Subsistema do Windows para Linux:
python -m pytest -v test_advanced.py
A saída deve ser semelhante ao seguinte relatório:
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
Seu sistema operacional, versão do Python, versão do pytest, caminho do executável, diretório raiz, linhas de plug-ins e cache, formatação do progresso da coleta, quebra automática de linha e tempo de execução podem variar. Embora você tenha escrito apenas três funções de teste, o pytest coletou 10 testes porque cada valor de entrada parametrizado é executado como seu próprio caso de teste.
Etapa 3 - Reestruturar a configuração e a desmontagem em um fixture
Acrescente um teste baseado em classe ao arquivo test_advanced.py . Este teste usa métodos no estilo xUnit
setup_method()eteardown_method()para criar e remover um arquivo para cada método de teste:from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"Esse teste pode ser aprovado, mas grava um nome de arquivo fixo no diretório de trabalho atual e requer um código de limpeza separado. Se uma execução de teste for interrompida ou os testes forem executados simultaneamente, como com
pytest-xdistou sobrepondo execuções locais, essa abordagem poderá deixar arquivos obsoletos, substituir um arquivo existente nomeado feito ou fazer com que os testes afetem uns aos outros.Adicione um fixture no nível do módulo que utilize o acessório
tmp_pathintegrado para criar um arquivo temporário. Na próxima etapa, você atualizará a classe para usar esse fixture em vez dos métodos de inicialização e desmontagem. Para facilitar a leitura, coloque este fixture de nível de módulo após os testesstr_to_boolparametrizados e antes da classeTestFile. O Pytest identifica fixtures no nível do módulo independentemente de onde elas apareçam no módulo; portanto, a posição serve para facilitar a leitura por pessoas, e não para o Pytest:@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return writeO acessório
tmp_file()utiliza o acessóriotmp_pathdo pytest, que fornece um diretório temporáriopathlib.Pathexclusivo para cada chamada da função de teste, incluindo cada caso parametrizado. O Pytest gerencia o diretório temporário, portanto, o teste não depende de um caminho de arquivo embutido em código.Exclua a classe anterior
TestFile, incluindo seus métodossetup_method()eteardown_method(), e substitua-a por esta versão final que usa a fixture em vez dos métodos de configuração e desmontagem.class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"Você também pode remover a
from pathlib import Pathimportação adicionada na etapa anterior, pois o teste não cria mais umPathobjeto diretamente.
Verifique seu trabalho
Agora você deve ter um arquivo Python chamado test_advanced.py que contenha:
- Uma
str_to_bool()função que aceita uma cadeia de caracteres e retorna um valor booliano para valores verdadeiros e falsos reconhecidos. - Três testes parametrizados para a função:
str_to_bool()um que testa valoresTrue, outro que testa valoresFalsee um que verifica valores inválidos, gerando uma exceçãoValueError. - Um recurso personalizado do pytest que utiliza o acessório integrado
tmp_pathpara criar um arquivo concluído alguns conteúdos. - Uma única classe
TestFilecom um método que usa o fixador personalizadotmp_filepara criar e ler o arquivo. - Nenhuma
from pathlib import Pathimportação e nenhumsetup_method()/teardown_method()método na versão final.
O arquivo final deve ser semelhante ao seguinte:
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
Execute o arquivo de teste novamente.
No Windows PowerShell:
python -m pytest -v test_advanced.py
No macOS, Linux ou no Subsistema do Windows para Linux:
python -m pytest -v test_advanced.py
O relatório final deve mostrar 11 testes coletados: 10 casos dos testes parametrizados e um teste baseado em classe.
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
Todos os testes devem ser aprovados sem erros.