Exercício

Concluído

Neste exercício, você usa o Pytest para testar uma função. Em seguida, você encontrar e corrigir alguns problemas potenciais com a função que causam testes de falha. Olhar para falhas e usar o rico relatório de erros do Pytest é essencial para identificar e corrigir testes problemáticos ou bugs no código de produção.

Para este exercício, usamos uma função chamada admin_command() que aceita um comando do sistema como entrada e, opcionalmente, o prefixa com a sudo ferramenta. A função tem um bug, que você descobre 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 command argumento e, opcionalmente, pode prefixar a lista com sudo. Se o argumento da palavra-chave estiver definido como , ele retornará o sudo mesmo comando dado como Falseentrada.

  2. No mesmo arquivo, anexe os testes para a admin_command() função. 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
    

Nota

Não é comum ter testes dentro do mesmo arquivo como código real. Para simplificar, os exemplos neste exercício terão código real no mesmo arquivo. Em projetos Python do mundo real, os testes geralmente são separados por arquivos e diretórios do código que estão testando.

Passo 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 com 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 da 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 falha no test_sudo() teste. Pytest está dando detalhes sobre as duas listas que estão sendo comparadas. Neste caso, a result variável não tem o comando, que é o que o sudo teste espera.

Passo 3 - Corrigir o bug e fazer os testes passarem

Antes de fazer qualquer alteração, você deve entender por que há uma falha em primeiro lugar. Embora você possa ver que a expectativa não está sendo atendida (sudo não está no resultado), você tem que descobrir o porquê.

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

    if sudo:
        ["sudo"] + command

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

  1. Atualize a função para retornar a admin_command() operação de lista para que o resultado modificado seja usado ao solicitar um sudo comando. 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. Execute novamente o teste com Pytest. Tente aumentar a detalhamento da saída usando o -v sinalizador com Pytest:

    $ pytest -v test_exercise.py
    
  3. Agora verifique a saída. Ele deve mostrar dois testes de aprovação 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 ===============================
    

Nota

Uma vez que a função é capaz de trabalhar com mais valores de invólucros diferentes, mais testes devem ser adicionados para cobrir essas variações. Isso impediria que alterações futuras na 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 cobertas por testes existentes, você pode se sentir confiante de que não está quebrando nenhuma suposição anterior.

Neste caso, a admin_command() função é confiar cegamente que o command argumento é 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 capture o comportamento. Embora a função ainda não esteja atualizada, tente uma abordagem test-first (também conhecida como Test Driven Development ou TDD).

    • Atualize o arquivo test_exercise.py para que ele seja importado pytest na parte superior. Este teste usa um auxiliar interno da pytest estrutura:
    import pytest
    
    • Agora, anexe um novo teste à classe para verificar a exceção. Este teste deve esperar um TypeError da função quando o valor passado 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 o Pytest, todos eles devem passar:

    ============================= 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 uma mensagem de erro explícita com uma TypeError útil mensagem:

    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. Finalmente, atualize o test_non_list_commands() método 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.argso , você pode examinar os argumentos da exceção. Nesse caso, o primeiro argumento tem a cadeia de erro que o teste pode verificar.

Verifique o seu trabalho

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

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

Todos os testes devem ser aprovados sem erros quando você executá-los no terminal.