Exercício

Concluído

Neste exercício, você usará o Pytest para testar uma função. Em seguida, você encontrará e corrigirá alguns problemas potenciais na função que causam falhas nos testes. É essencial examinar as falhas e usar os relatórios de erros avançados do Pytest para identificar e corrigir bugs ou testes problemáticos no código em produção.

Neste exercício, usaremos uma função chamada admin_command() que aceita um comando do sistema como entrada e, opcionalmente, recebe a ferramenta sudo como prefixo. A função tem um bug, que você descobrirá escrevendo testes.

Etapa 1 – Adicionar um arquivo com testes para este exercício

  1. Usando as convenções de nome de arquivo do Python para arquivos de teste, crie um novo arquivo de teste. Nomeie o arquivo de teste test_exercise.py e adicione o seguinte código:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            ["sudo"] + command
        return command
    

    A função admin_command() usa uma lista como entrada usando o argumento command e, opcionalmente, pode prefixá-la com sudo. Se o argumento de palavra-chave sudo estiver definido como False, ele retornará o mesmo comando fornecido como entrada.

  2. No mesmo arquivo, anexe os testes para a função admin_command(). Os testes usam um método auxiliar que retorna um comando de exemplo:

    class TestAdminCommand:
    
    def command(self):
        return ["ps", "aux"]
    
    def test_no_sudo(self):
        result = admin_command(self.command(), sudo=False)
        assert result == self.command()
    
    def test_sudo(self):
        result = admin_command(self.command(), sudo=True)
        expected = ["sudo"] + self.command()
        assert result == expected
    

Observação

Não é comum colocar os testes no mesmo arquivo que o código real. Para simplificar, os exemplos neste exercício terão o código real no mesmo arquivo. Em projetos do Python do mundo real, os testes costumam ser separados por arquivos e diretórios do código que estão sendo testados.

Etapa 2 – Executar os testes e identificar a falha

Agora que o arquivo de teste tem uma função para testar e alguns testes para verificar seu comportamento, é hora de executar os testes e trabalhar nas falhas.

  • Execute o arquivo com Python:

    $ pytest test_exercise.py
    

    A execução deve ser concluída com uma aprovação no teste e uma falha, e a saída de falha deve ser semelhante à seguinte saída:

    =================================== FAILURES ===================================
    __________________________ TestAdminCommand.test_sudo __________________________
    
    self = <test_exercise.TestAdminCommand object at 0x10634c2e0>
    
        def test_sudo(self):
            result = admin_command(self.command(), sudo=True)
            expected = ["sudo"] + self.command()
    >       assert result == expected
    E       AssertionError: assert ['ps', 'aux'] == ['sudo', 'ps', 'aux']
    E         At index 0 diff: 'ps' != 'sudo'
    E         Right contains one more item: 'aux'
    E         Use -v to get the full diff
    
    test_exercise.py:24: AssertionError
    =========================== short test summary info ============================
    FAILED test_exercise.py::TestAdminCommand::test_sudo - AssertionError: assert...
    ========================= 1 failed, 1 passed in 0.04s ==========================
    

    A saída apresenta falha no teste test_sudo(). Pytest está fornecendo detalhes sobre as duas listas que estão sendo comparadas. Nesse caso, a variável result não tem o comando sudo, que é o que o teste espera.

Etapa 3 – Corrigir o bug e fazer com que os testes passem

Antes de fazer alterações, você precisa entender por que há uma falha. Embora você saiba que a expectativa não está sendo atendida (sudo não está no resultado), é preciso descobrir o motivo.

Examine as seguintes linhas de código da função admin_command() quando a condição sudo=True é atendida:

    if sudo:
        ["sudo"] + command

A operação das listas não está sendo usada para retornar o valor. Como ela não está sendo retornada, a função sempre acaba retornando o comando sem sudo.

  1. Atualize a função admin_command() para retornar a operação de lista para que o resultado modificado seja usado ao solicitar um comando sudo. A função atualizada deve ter esta aparência:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            return ["sudo"] + command
        return command
    
  2. Repita o teste com Pytest. Tente aumentar o detalhamento da saída usando o sinalizador -v com Pytest:

    $ pytest -v test_exercise.py
    
  3. Agora verifique a saída. Os dois testes aprovados devem ser exibidos agora:

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
    cachedir: .pytest_cache
    rootdir: /private
    collected 2 items
    
    test_exercise.py::TestAdminCommand::test_no_sudo PASSED                  [ 50%]
    test_exercise.py::TestAdminCommand::test_sudo PASSED                     [100%]
    
    ============================== 2 passed in 0.00s ===============================
    

Observação

Como a função é capaz de trabalhar com mais valores de diferentes usos linguísticos, mais testes devem ser adicionados para cobrir essas variações. Isso evitaria que alterações futuras à função causassem um comportamento diferente (inesperado).

Etapa 4 – Adicionar novo código com testes

Depois de adicionar testes nas etapas anteriores, você deve se sentir confortável para fazer mais alterações na função e verificá-las com testes. Mesmo que as alterações não sejam tratadas por testes existentes, você pode se sentir confiante de que não está violando nenhuma suposição anterior.

Nesse caso, a função admin_command() confia cegamente que o argumento command sempre é uma lista. Vamos melhorar isso garantindo que uma exceção com uma mensagem de erro útil seja gerada.

  1. Primeiro, crie um teste que captura o comportamento. Embora a função ainda não esteja atualizada, tente usar a abordagem de testar primeiro (também conhecida como TDD ou Desenvolvimento Orientado por Testes).

    • Atualize o arquivo test_exercise.py para que ele importe pytest na parte superior. Este teste usa um auxiliar interno da estrutura pytest:
    import pytest
    
    • Agora, acrescente um novo teste à classe para verificar a exceção. Este teste deve esperar um TypeError da função quando o valor transmitido para ela não é uma lista:
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Execute os testes novamente com Pytest, todos eles devem ser aprovados:

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
    rootdir: /private/
    collected 3 items
    
    test_exercise.py ...                                                     [100%]
    
    ============================== 3 passed in 0.00s ===============================
    

    O teste é bom o suficiente para verificar TypeError, mas seria bom adicionar o código com uma mensagem de erro útil.

  3. Atualize a função para gerar explicitamente um TypeError com uma mensagem de erro útil:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if not isinstance(command, list):
            raise TypeError(f"was expecting command to be a list, but got a {type(command)}")
        if sudo:
            return ["sudo"] + command
        return command
    
  4. Por fim, atualize o método test_non_list_commands() para verificar a mensagem de erro:

    def test_non_list_commands(self):
        with pytest.raises(TypeError) as error:
            admin_command("some command", sudo=True)
        assert error.value.args[0] == "was expecting command to be a list, but got a <class 'str'>"
    

    O teste atualizado usa error como uma variável que contém todas as informações de exceção. Usando error.value.args, você pode examinar os argumentos da exceção. Nesse caso, o primeiro argumento tem a cadeia de caracteres de erro que o teste pode verificar.

Verificar seu trabalho

Neste ponto, você terá um arquivo de teste do Python chamado test_exercise.py que inclui:

  • Uma função admin_command() que aceita um argumento e um argumento de palavra-chave.
  • Uma exceção TypeError com uma mensagem de erro útil na função admin_command().
  • Uma classe de teste TestAdminCommand() que tem um método auxiliar command() e três métodos de teste que verificam a função admin_command().

Todos os testes serão aprovados sem erros ao serem executados no terminal.