Exercício
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
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 argumentocommand
e, opcionalmente, pode prefixá-la comsudo
. Se o argumento de palavra-chavesudo
estiver definido comoFalse
, ele retornará o mesmo comando fornecido como entrada.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ávelresult
não tem o comandosudo
, 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
.
Atualize a função
admin_command()
para retornar a operação de lista para que o resultado modificado seja usado ao solicitar um comandosudo
. 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
Repita o teste com Pytest. Tente aumentar o detalhamento da saída usando o sinalizador
-v
com Pytest:$ pytest -v test_exercise.py
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.
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 estruturapytest
:
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)
- Atualize o arquivo test_exercise.py para que ele importe
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.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
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. Usandoerror.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çãoadmin_command()
. - Uma classe de teste
TestAdminCommand()
que tem um método auxiliarcommand()
e três métodos de teste que verificam a funçãoadmin_command()
.
Todos os testes serão aprovados sem erros ao serem executados no terminal.