Teste de Python com pytest. Usando pytest com outras ferramentas, CAPÍTULO 7

Voltar


Normalmente, o pytest não é usado independentemente, mas em um ambiente de teste com outras ferramentas. Este capítulo discute outras ferramentas que são frequentemente usadas em conjunto com o pytest para testes eficazes e eficientes. Embora essa não seja de forma alguma uma lista exaustiva, as ferramentas discutidas aqui fornecerão uma idéia do sabor do poder de misturar o pytest com outras ferramentas.



Os exemplos deste livro foram escritos usando Python 3.6 e pytest 3.2. O pytest 3.2 suporta Python 2.6, 2.7 e Python 3.3+.


O código-fonte do projeto Tarefas, bem como todos os testes mostrados neste livro, estão disponíveis no link na página da Web do livro em pragprog.com . Você não precisa fazer o download do código-fonte para entender o código de teste; o código de teste é apresentado de forma conveniente nos exemplos. Mas, para acompanhar as tarefas do projeto ou adaptar exemplos de teste para testar seu próprio projeto (suas mãos estão desamarradas!), Você deve acessar a página da Web do livro e fazer o download do trabalho. Lá, na página do livro, há um link para mensagens de errata e um fórum de discussão .

Sob o spoiler, há uma lista de artigos desta série.



PDB: falhas de teste de depuração


O módulo pdb é um depurador Python na biblioteca padrão. Você usa --pdb para que o pytest inicie uma sessão de depuração no ponto de falha. Vejamos o pdb em ação no contexto do projeto Tarefas.


Em “Parametrizando o dispositivo elétrico” na página 64, deixamos o projeto Tarefas com alguns erros:


 $ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds 

Antes de pdb como o pdb pode nos ajudar a depurar esse teste, vamos dar uma olhada nas opções pytest disponíveis para acelerar a depuração dos erros de teste, que examinamos pela primeira vez na seção “Usando opções” na página 9:


  • --tb=[auto/long/short/line/native/no] : controla o estilo de rastreamento.
  • -v / --verbose : exibe todos os nomes de teste que foram aprovados ou falharam.
  • -l / --showlocals : exibe variáveis ​​locais ao lado do rastreamento de pilha.
  • -lf / --last-failed : Executa apenas testes que falham.
  • -x / --exitfirst : interrompe a sessão de teste na primeira falha.
  • --pdb : inicia uma sessão de depuração interativa no ponto de falha.



Instalando o MongoDB




Conforme mencionado no Capítulo 3, “Acessórios Pytest”, na página 49, a instalação do MongoDB e do pymongo é necessária para executar os testes do MongoDB.


Testei a versão do Community Server encontrada em https://www.mongodb.com/download-center . O pymongo é instalado com pip : pip install pymongo . No entanto, este é o último exemplo de um livro que usa o MongoDB. Para testar o depurador sem usar o MongoDB, você pode executar os comandos pytest a partir do code/ch2/ , pois esse diretório também contém vários testes com falha.




Acabamos de executar os testes do code/ch3/c para garantir que alguns deles não estejam funcionando. Não vimos tracebacks ou nomes de teste porque --tb=no desativa o rastreamento e não tínhamos --verbose ativado. Vamos repetir os erros (não mais que três) com o texto detalhado:


 $ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ==================== 

Agora sabemos quais testes falharam. Vejamos apenas um deles, usando -x , ativando o rastreamento, não usando --tb=no e mostrando variáveis ​​locais com -l :


 $ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============ 

Muitas vezes, isso é suficiente para entender por que o teste falhou. Nesse caso específico, é bastante claro que task_id não task_id um número inteiro - é uma instância do ObjectId. ObjectId é o tipo usado pelo MongoDB para identificadores de objeto no banco de dados. Minha intenção com a camada tasksdb_pymongo.py era ocultar certos detalhes da implementação do MongoDB do restante do sistema. É claro que, neste caso, não funcionou.


No entanto, queremos ver como usar o pdb com o pytest, então vamos imaginar que não está claro por que esse teste falhou. Podemos fazer com que o pytest inicie uma sessão de depuração e nos inicie no ponto da falha usando --pdb :


 $ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb) 

Agora que estamos no prompt (Pdb), temos acesso a todos os recursos interativos de depuração de pdb. Ao visualizar falhas, uso regularmente estes comandos:


  • p/print expr : imprime o valor da exp.
  • pp expr : Pretty imprime o valor de expr.
  • l/list : lista o ponto de falha e cinco linhas de código acima e abaixo.
  • l/list begin,end : enumera números de linha específicos.
  • a/args : imprime os argumentos da função atual com seus valores.
  • u/up : Move um nível acima do caminho da pilha.
  • d/down : desce um nível no rastreamento de pilha.
  • q/quit : finaliza uma sessão de depuração.

Outros comandos de navegação, como step e next, não são muito úteis, pois estamos sentados na declaração assert. Você também pode simplesmente inserir nomes de variáveis ​​e obter valores.


Você pode usar p/print expr maneira semelhante à -l/--showlocals para visualizar os valores em uma função:


 (Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb) 

Agora você pode sair do depurador e continuar testando.


 (Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds =========== 

Se não usássemos - , o pytest abriria novamente o Pdb no próximo teste. Mais informações sobre o uso do módulo pdb estão disponíveis na documentação do Python .


Coverage.py: determinando a quantidade de código de teste


A cobertura do código é um indicador da porcentagem de código testado que é testado por um conjunto de testes. Quando você executa testes para o projeto Tarefas, algumas funções de Tarefas são executadas em cada teste, mas não em todos.


As ferramentas de cobertura de código são ótimas para informar quais partes do sistema são completamente ignoradas pelos testes.


Coverage.py é a ferramenta de cobertura Python preferida que mede a cobertura de código.


Você o utilizará para verificar o código do projeto Tarefas com pytest.


Para usar o coverage.py você precisa instalá-lo. Não pytest-cov nada instalar um plug-in chamado pytest-cov , que permite chamar a coverage.py do pytest com algumas opções adicionais de pytest. Como a coverage é uma das dependências do pytest-cov , basta instalar o pytest-cov e a coverage.py será pytest-cov :


 $ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1 

Vamos executar o relatório de cobertura para a segunda versão da tarefa. Se você ainda tiver a primeira versão do projeto Tarefas instalada, desinstale-a e instale a versão 2:


 $ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ... 

Agora que a próxima versão das tarefas está instalada, você pode executar o relatório de cobertura base:


 $ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds =================== 

Como o diretório atual é tasks_proj_v2 e o código-fonte em teste está em src, a adição da opção --cov=src gera um relatório de cobertura apenas para esse diretório em teste.


Como você pode ver, alguns arquivos têm cobertura muito baixa e até 0%. Estes são lembretes úteis: tasksdb_pymongo.py 0% porque desativamos o teste do MongoDB nesta versão. Alguns deles são bastante baixos. O projeto certamente terá que realizar testes para todas essas áreas antes de estar pronto para o horário nobre.


Acredito que vários arquivos tenham uma porcentagem maior de cobertura: api.py e tasksdb_tinydb.py . Vamos dar uma olhada em tasksdb_tinydb.py e ver o que está faltando. Eu acho que a melhor maneira de fazer isso é usar relatórios HTML.


Se você executar o --cov-report=html novamente com a opção --cov-report=html , um --cov-report=html será gerado:


 $ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds =================== 

Você pode abrir o htmlcov/index.html em um navegador que exibe a saída na seguinte tela:



Clicar em tasksdb_tinydb.py exibirá um relatório para um arquivo. A porcentagem de linhas cobertas é exibida na parte superior do relatório, mais quantas linhas são cobertas e quantas não, como mostrado na próxima tela:



Rolando para baixo, você pode ver as linhas ausentes, conforme mostrado na tela a seguir:



Mesmo que essa tela não seja uma página completa para esse arquivo, basta dizer que:


  1. Não testamos list_tasks() com o conjunto de proprietários.
  2. Não testamos update() ou delete() .
  3. Talvez não unique_id() testando completamente unique_id() .

Ótimo. Podemos incluí-los em nossa lista de testes de tarefas, juntamente com o teste do sistema de configuração.


Embora as ferramentas de cobertura de código sejam extremamente úteis, buscar 100% de cobertura pode ser perigoso. Quando você vê um código que não está sendo testado, isso pode significar a necessidade de um teste. Mas também pode significar que existem algumas funções do sistema que não são necessárias e podem ser removidas. Como todas as ferramentas de desenvolvimento de software, a análise de cobertura de código não substitui o pensamento.


Consulte a coverage.py e pytest-cov obter mais detalhes.


mock: substituição de peças do sistema


O pacote simulado é usado para substituir partes do sistema para isolar partes do código de teste do restante do sistema. Mock - objetos às vezes são chamados de dobras de teste, espiões, falsificações ou tocos.


Entre o seu próprio dispositivo pytest monkeypatch (descrito em Usando o monkeypatch na página 85) e o mock, você deve ter toda a funcionalidade de teste duplo necessária.


Atenção! Simulado e muito estranho
Se esta é a primeira vez que você encontra gêmeos de teste, como zombarias, tocos e espiões, prepare-se! Será muito estranho muito rápido, engraçado, embora muito impressionante.

O pacote mock vem com a biblioteca padrão do Python, como unittest.mock desde o Python 3.3. Nas versões anteriores, ele está disponível como um pacote separado instalado através do PyPI. Isso significa que você pode usar a versão mock PyPI do Python 2.6 para a versão mais recente do Python e obter a mesma funcionalidade do último mock Python. No entanto, para uso com o pytest, um plug-in chamado pytest-mock possui alguns recursos que o tornam minha interface preferida para o sistema de mock.


Para o projeto Tarefas, usaremos mock para nos ajudar a testar a interface da linha de comandos. Em Coverage.py: ao determinar quanto código está sendo testado, na página 129 você viu que nosso arquivo cli.py não foi testado. Vamos começar a consertar isso agora. Mas vamos falar sobre estratégia primeiro.


A primeira solução no projeto Tarefas foi fazer a maioria dos testes de funcionalidade por meio do api.py Portanto, uma solução razoável é que o teste de linha de comando não precise ser um teste funcional completo. Podemos ter certeza de que o sistema funcionará através da CLI se atingirmos o nível da API úmida durante o teste da CLI. Também é uma solução conveniente que nos permite examinar o moki nesta seção.


A implementação de tarefas da CLI usa um pacote de interface de linha de comando de terceiros. Existem muitas alternativas para implementar a interface da linha de comandos, incluindo um módulo embutido no Python argparse . Um dos motivos pelos quais escolhi o Click é porque ele inclui um mecanismo de teste que nos ajuda a testar os aplicativos Click. No entanto, o código no cli.py , embora esperemos que seja típico dos aplicativos Click, não é óbvio.


Vamos diminuir a velocidade e instalar a terceira versão do Tasks:


 $ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks 

No restante desta seção, você desenvolverá vários testes para testar a funcionalidade da "lista".
Vamos vê-lo em ação para entender o que vamos verificar:


Nota do tradutor: Ao usar a plataforma Windows, encontrei vários problemas ao testar a sessão abaixo.
  1. Uma pasta deve ser criada para o banco de dados denominado tasks_db na pasta do seu usuário. Por exemplo c:\Users\User_1\tasks_db\
    Caso contrário, obteremos - >> FileNotFoundError: [Erro 2] Não existe esse arquivo ou diretório: 'c: \ Users \ User_1 // tasks_db // tasks_db.json'
  2. Use aspas duplas em vez de um apóstrofo. Caso contrário, obtenha um erro
    'faça algo ótimo'
    Uso: tarefas adicionam [OPTIONS] RESUMO
    Tente "task add -h" para obter ajuda.

    Erro: obtive argumentos extras inesperados (algo ótimo ')


 $ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat 

Parece bem simples. O comando tasks list exibe uma lista de todas as tarefas sob o cabeçalho.
O título é impresso mesmo se a lista estiver vazia. O comando exibe apenas dados de um proprietário, se -o ou --owner . E como verificamos isso? Existem muitas maneiras, mas vamos usar o moki.


Testes que usam MOKs são necessariamente testes de caixa branca , e precisamos examinar o código para decidir o que e onde iremos bater. O principal ponto de entrada está aqui:


ch7 / tasks_proj_v2 / src / tasks / cli.py

 if __name__ == '__main__': tasks_cli() 

Esta é apenas uma chamada para tasks_cli() :


ch7 / tasks_proj_v2 / src / tasks / cli.py

 @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass 

Obviamente? Não. Mas espere, fica bom (ou ruim, dependendo do seu ponto de vista). Aqui está um dos comandos da list :


ch7 / tasks_proj_v2 / src / tasks / cli.py

 @tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """    .   ,      . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary)) 

Quando você se acostumar a escrever o código Click, verifique se esse código não é tão ruim. Não vou explicar aqui o que e como funciona nesta função, pois o desenvolvimento do código da linha de comando não é o foco do livro; no entanto, embora eu tenha quase certeza absoluta de que tenho esse código correto, há sempre muito espaço para erro humano. É por isso que um bom conjunto de testes automatizados é importante para garantir que esse recurso funcione corretamente.
Essa função list_tasks(owner) depende de várias outras funções: tasks_db() , que é o gerenciador de contexto, e tasks.list_tasks(owner) , que é a função da API.


Vamos usar o mock para colocar funções falsas no tasks_db() e tasks.list_tasks() . Em seguida, podemos chamar o método list_tasks por meio da interface da linha de comandos e garantir que ele chame a função tasks.list_tasks() , que funciona corretamente e processa corretamente o valor de retorno.
Para abafar tasks_db() , vamos ver uma implementação real:


Voltar

Source: https://habr.com/ru/post/pt448798/


All Articles