Exercício

Concluído

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

  1. 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. Retorna True para valores verdadeiros reconhecidos, retorna False para valores falsos reconhecidos, e levanta um ValueError para qualquer outro valor.

  2. No mesmo arquivo, anexe os testes para a função str_to_bool(). Use @pytest.mark.parametrize primeiro 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 True
    
  3. Em 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 False
    

    Agora há duas funções de teste que abrangem entradas representativas para os valores de retorno de True e False.

  4. 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 quando str_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 match argumento 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

  1. Acrescente um teste baseado em classe ao arquivo test_advanced.py . Este teste usa métodos no estilo xUnit setup_method() e teardown_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-xdist ou 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.

  2. Adicione um fixture no nível do módulo que utilize o acessório tmp_path integrado 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 testes str_to_bool parametrizados e antes da classe TestFile. 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 write
    

    O acessório tmp_file() utiliza o acessório tmp_path do pytest, que fornece um diretório temporário pathlib.Path exclusivo 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.

  3. Exclua a classe anterior TestFile, incluindo seus métodos setup_method() e teardown_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 Path importação adicionada na etapa anterior, pois o teste não cria mais um Path objeto 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 valores True, outro que testa valores False e um que verifica valores inválidos, gerando uma exceção ValueError.
  • Um recurso personalizado do pytest que utiliza o acessório integrado tmp_path para criar um arquivo concluído alguns conteúdos.
  • Uma única classe TestFile com um método que usa o fixador personalizado tmp_file para criar e ler o arquivo.
  • Nenhuma from pathlib import Path importação e nenhum setup_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.