Teste de Python com pytest. Acessórios Internos, Capítulo 4

Voltar Próximo


Os acessórios embutidos que acompanham o pytest podem ajudá-lo a fazer algumas coisas bastante úteis em seus testes com facilidade e naturalidade. Por exemplo, além de manipular arquivos temporários, o pytest inclui acessórios internos para acessar parâmetros de linha de comando, comunicação entre sessões de teste, verificação de fluxos de saída, alteração de variáveis ​​de ambiente e alertas de pesquisa.



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 para baixar o trabalho. Lá, na página do livro, há um link para o post da errata e o fórum de discussão .

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



No capítulo anterior, você examinou o que são acessórios, como escrevê-los e como usá-los para dados de teste, bem como para códigos de configuração e desmontagem.


Você também usou o conftest.py para compartilhar acessórios entre testes em vários arquivos de teste. No final do capítulo 3, os seguintes acessórios foram instalados nos acessórios pytest na página 49 do projeto Tasks: tasks_db_session , tasks_just_a_few , tasks_mult_per_owner , tasks_db , db_with_3_tasks e db_with_multi_per_owner definidos em conftest.py que podem ser usados ​​por qualquer função de teste precisa deles.


Reutilizar equipamentos regulares é uma idéia tão boa que os desenvolvedores do pytest incluíram alguns acessórios normalmente necessários no pytest. Você já viu como tmpdir e tmpdir_factory são usados ​​pelo projeto Tasks na seção de mudança de escopo para acessórios do projeto Tasks na página 59. Você os discutirá com mais detalhes neste capítulo.


Os acessórios internos que acompanham o pytest podem ajudá-lo a fazer algumas coisas bastante úteis em seus testes com facilidade e naturalidade. Por exemplo, além de manipular arquivos temporários, o pytest inclui acessórios internos para acessar parâmetros de linha de comando, comunicação entre sessões de teste, verificação de fluxos de saída, alteração de variáveis ​​de ambiente e alertas de pesquisa. Acessórios embutidos são extensões da funcionalidade principal do pytest. Vejamos agora alguns dos equipamentos embutidos mais usados ​​em ordem.


Usando tmpdir e tmpdir_factory


Se você estiver testando algo que lê, grava ou modifica arquivos, é possível usar o tmpdir para criar arquivos ou diretórios usados ​​por um único teste e o tmpdir_factory quando desejar configurar um diretório para vários testes.


O dispositivo tmpdir tmpdir possui um escopo de função e o dispositivo tmpdir_factory possui um escopo de sessão. Qualquer teste único que exija um diretório ou arquivo temporário para apenas um teste pode usar tmpdir . Isso também se aplica aos equipamentos, que personalizam o diretório ou arquivo que deve ser recriado para cada função de teste.


Aqui está um exemplo simples usando o tmpdir :


ch4/test_tmpdir.py

 def test_tmpdir(tmpdir): # tmpdir    ,    # join()  ,    , #     a_file = tmpdir.join('something.txt') #    a_sub_dir = tmpdir.mkdir('anything') #      (  ) another_file = a_sub_dir.join('something_else.txt') #    'something.txt' a_file.write('contents may settle during shipping') #    'anything/something_else.txt' another_file.write('something different') #      assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different' 

O valor retornado de tmpdir é um objeto do tipo py.path.local.1 que parece ser tudo o que precisamos para diretórios e arquivos temporários. No entanto, há um truque. Como o tmpdir elétrico tmpdir definido como um escopo de função , o tmpdir não pode ser usado para criar pastas ou arquivos que devem estar disponíveis por mais de uma função de teste. Para equipamentos com escopo diferente de uma função (classe, módulo, sessão), tmpdir_factory está disponível.


O dispositivo tmpdir_factory tmpdir_factory tmpdir_factory muito semelhante ao tmpdir , mas possui uma interface diferente. Conforme descrito na seção “Especificação do acessório de escopo”, na página 56, os acessórios de área de função são executados uma vez para cada função de teste, os acessórios de região do módulo são executados uma vez por módulo, os acessórios de classe uma vez para cada classe e os testes de validação de área trabalhe uma vez por sessão. Assim, os recursos criados nos registros da área da sessão têm uma vida útil de toda a sessão. Para mostrar como tmpdir e tmpdir_factory , tmpdir exemplo tmpdir , onde tmpdir_factory :


ch4 / test_tmpdir.py

 def test_tmpdir_factory(tmpdir_factory): #      . a_dir   # ,    tmpdir a_dir = tmpdir_factory.mktemp('mydir') # base_temp    'mydir'    #  getbasetemp(),  # ,    base_temp = tmpdir_factory.getbasetemp() print('base:', base_temp) #       , #    ' test_tmpdir ()',   , #    a_dir  tmpdir a_file = a_dir.join('something.txt') a_sub_dir = a_dir.mkdir('anything') another_file = a_sub_dir.join('something_else.txt') a_file.write('contents may settle during shipping') another_file.write('something different') assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different' 

A primeira linha usa mktemp('mydir') para criar o diretório e o armazena em a_dir . Para o restante da função, você pode usar a_dir da mesma maneira que tmpdir retornado do tmpdir .


Na segunda linha do exemplo tmpdir_factory , a função getbasetemp() retorna o diretório base usado para esta sessão. A declaração de impressão no exemplo é necessária para que você possa visualizar o diretório em seu sistema. Vamos ver onde fica:


 $ cd /path/to/code/ch4 $ pytest -q -s test_tmpdir.py::test_tmpdir_factory base: /private/var/folders/53/zv4j_zc506x2xq25l31qxvxm0000gn/T/pytest-of-okken/pytest-732 . 1 passed in 0.04 seconds 

Esse diretório base depende do sistema e do usuário, e pytest - NUM muda para cada sessão com o aumento de NUM . O diretório base é deixado sozinho após a sessão. O pytest o limpa e apenas os poucos diretórios base temporários mais recentes permanecem no sistema, o que é bom se você estiver impaciente para verificar os arquivos após a execução do teste.


Você também pode especificar seu próprio diretório base se precisar com pytest --basetemp=mydir .


Usando diretórios temporários para outras áreas


tmpdir_factory diretórios temporários e arquivos de área de sessão do tmpdir_factory , e diretórios de função e arquivos de região do tmpdir . Mas e outras áreas? E se precisarmos de um diretório temporário do escopo de um módulo ou classe? Para fazer isso, criamos outro dispositivo elétrico da região do tamanho desejado e, para isso, devemos usar tmpdir_factory .


Por exemplo, suponha que tenhamos um módulo cheio de testes e muitos deles devem poder ler alguns dados de um arquivo json . Conseguimos colocar o volume do aparelho no próprio módulo ou no arquivo conftest.py , que configura o arquivo de dados da seguinte maneira:


ch4 / autores / conftest.py

 """Demonstrate tmpdir_factory.""" import json import pytest @pytest.fixture(scope='module') def author_file_json(tmpdir_factory): """     .""" python_author_data = { 'Ned': {'City': 'Boston'}, 'Brian': {'City': 'Portland'}, 'Luciano': {'City': 'Sau Paulo'} } file = tmpdir_factory.mktemp('data').join('author_file.json') print('file:{}'.format(str(file))) with file.open('w') as f: json.dump(python_author_data, f) return file 

O author_file_json() cria um diretório temporário chamado data e cria um arquivo chamado author_file.json no diretório de dados. Em seguida, ele escreve o dicionário python_author_data como json . Como este é um módulo de área de fixação, um arquivo json será criado apenas uma vez para cada módulo usando o teste:


ch4 / autores / test_authors.py

 """ ,    .""" import json def test_brian_in_portland(author_file_json): """,   .""" with author_file_json.open() as f: authors = json.load(f) assert authors['Brian']['City'] == 'Portland' def test_all_have_cities(author_file_json): """        .""" with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]['City']) > 0 

Ambos os testes usarão o mesmo arquivo JSON. Se um arquivo de dados de teste funcionar para vários testes, não faz sentido recriá-lo para os dois testes.


Usando pytestconfig


Usando o acessório pytestconfig interno, você pode controlar como o pytest funciona com argumentos e opções de linha de comando, arquivos de configuração, plugins e o diretório a partir do qual você iniciou o pytest. O acessório pytestconfig é um atalho para request.config e às vezes é referido na documentação do pytest como " o objeto de configuração do pytest " (objeto de configuração do pytest).


Para descobrir como o pytestconfig funciona, você pode ver como adicionar um parâmetro de linha de comando personalizado e ler o valor do parâmetro no teste. Você pode ler o valor dos parâmetros da linha de comando diretamente do pytestconfig, mas para adicionar um parâmetro e analisá-lo, é necessário adicionar uma função de gancho. As funções de gancho , descritas em mais detalhes no Capítulo 5, “Plugins”, na página 95, são outra maneira de controlar o comportamento do pytest e são frequentemente usadas em plugins. No entanto, a adição de uma opção de linha de comando personalizada e a leitura do pytestconfig é bastante difundida, por isso quero abordar isso aqui.


Usaremos o gancho pytest_addoption para adicionar vários parâmetros aos parâmetros já disponíveis na linha de comando pytest:


ch4 / pytestconfig / conftest.py

 def pytest_addoption(parser): parser.addoption("--myopt", action="store_true", help="some boolean option") parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz") 

A adição de parâmetros da linha de comando via pytest_addoption deve ser feita por meio de plugins ou no arquivo conftest.py, localizado na parte superior da estrutura de diretórios do projeto. Você não deve fazer isso no subdiretório de teste.


As --myopt e --foo <value> foram adicionadas ao código anterior e a linha de ajuda foi alterada conforme mostrado abaixo:


 $ cd /path/to/code/ch4/pytestconfig $ pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt some boolean option --foo=FOO foo: bar or baz ... 

Agora podemos acessar essas opções do teste:


ch4 / pytestconfig / test_config.py

 import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption('foo')) print('"myopt" set to:', pytestconfig.getoption('myopt')) 

Vamos ver como funciona:


 $ pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False .1 passed in 0.01 seconds $ pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True .1 passed in 0.01 seconds $ pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True .1 passed in 0.01 seconds 

Como o pytestconfig é um equipamento, ele também pode ser obtido em outros equipamentos. Você pode criar acessórios para os nomes das opções, se desejar, por exemplo:


ch4 / pytestconfig / test_config.py

 @pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(foo, myopt): print('"foo" set to:', foo) print('"myopt" set to:', myopt) 

Você também pode acessar os parâmetros internos, não apenas os adicionados, além de informações sobre como o pytest foi iniciado (diretório, argumentos etc.).


Aqui está um exemplo de vários valores e opções de configuração:


 def test_pytestconfig(pytestconfig): print('args :', pytestconfig.args) print('inifile :', pytestconfig.inifile) print('invocation_dir :', pytestconfig.invocation_dir) print('rootdir :', pytestconfig.rootdir) print('-k EXPRESSION :', pytestconfig.getoption('keyword')) print('-v, --verbose :', pytestconfig.getoption('verbose')) print('-q, --quiet :', pytestconfig.getoption('quiet')) print('-l, --showlocals:', pytestconfig.getoption('showlocals')) print('--tb=style :', pytestconfig.getoption('tbstyle')) 

Voltaremos ao pytestconfig quando eu demonstrar os arquivos ini no capítulo 6, “Configuração” na página 113.


Usando cache


Geralmente, nós testadores pensamos que cada teste é o mais independente possível dos outros testes. Você deve se certificar de que as dependências da contabilidade de pedidos não entraram. Gostaria de poder executar ou reiniciar qualquer teste em qualquer ordem e obter o mesmo resultado. Além disso, as sessões de teste devem ser repetíveis e não alterar o comportamento com base nas sessões de teste anteriores.


No entanto, às vezes transferir informações de uma sessão de teste para outra pode ser muito útil. Quando queremos passar informações para futuras sessões de teste, podemos fazer isso com o dispositivo de cache interno.


O cache dispositivo cache projetado para armazenar informações sobre uma sessão de teste e obtê-la na próxima. Um ótimo exemplo de uso de permissões de cache para o benefício do caso é a funcionalidade incorporada - --last-failed e --failed-first --last-failed . Vamos ver como os dados para esses sinalizadores são armazenados no cache.


Aqui está o texto de ajuda para as opções --failed-first --last-failed e --failed-first --last-failed , bem como algumas opções de cache :


 $ pytest --help ... --lf, --last-failed rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first run all tests but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown --cache-show show cache contents, don t perform collection or tests --cache-clear remove all cache contents at start of test run. ... 

Para vê-los em ação, usaremos estes dois testes:


ch4 / cache / test_pass_fail.py


 def test_this_passes(): assert 1 == 1 def test_this_fails(): assert 1 == 2 

Vamos executá-los usando --verbose para ver os nomes das funções e --tb=no para ocultar o rastreamento da pilha:


 $ cd /path/to/code/ch4/cache $ pytest --verbose --tb=no test_pass_fail.py ==================== test session starts ==================== collected 2 items test_pass_fail.py::test_this_passes PASSED test_pass_fail.py::test_this_fails FAILED ============ 1 failed, 1 passed in 0.05 seconds ============= 

Se você executá-los novamente com o --ff ou --failed-first , os testes que falharam anteriormente serão executados primeiro e depois a sessão inteira:


 $ pytest --verbose --tb=no --ff test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures first collected 2 items test_pass_fail.py::test_this_fails FAILED test_pass_fail.py::test_this_passes PASSED ============ 1 failed, 1 passed in 0.04 seconds ============= 

Ou você pode usar --lf ou --lf --last-failed para apenas executar testes que falharam na última vez:


 $ pytest --verbose --tb=no --lf test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures collected 2 items test_pass_fail.py::test_this_fails FAILED ==================== 1 tests deselected ===================== ========== 1 failed, 1 deselected in 0.05 seconds =========== 

Antes de --lf como os dados de travamento são armazenados e como você pode usar o mesmo mecanismo, vejamos outro exemplo que torna o valor de --lf e --ff ainda mais óbvio.


Aqui está um teste parametrizado com uma falha:


ch4 / cache / test_few_failures.py

 """Demonstrate -lf and -ff with failing tests.""" import pytest from pytest import approx testdata = [ # x, y, expected (1.01, 2.01, 3.02), (1e25, 1e23, 1.1e25), (1.23, 3.21, 4.44), (0.1, 0.2, 0.3), (1e25, 1e24, 1.1e25) ] @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y assert sum_ == approx(expected) 

E na saída:


 $ cd /path/to/code/ch4/cache $ pytest -q test_few_failures.py .F... ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) test_few_failures.py:17: AssertionError 1 failed, 4 passed in 0.06 seconds 

Talvez você possa identificar o problema imediatamente. Mas vamos imaginar que o teste seja mais longo e complicado, e não é tão óbvio o que está errado aqui. Vamos executar o teste novamente para ver o erro novamente. O caso de teste pode ser especificado na linha de comandos:


 $ pytest -q "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]" 

Se você não deseja copiar e colar (copiar / colar ) ou existem vários casos infelizes que você gostaria de reiniciar, então --lf muito mais fácil. E se você está realmente depurando uma falha de teste, outro sinalizador que pode facilitar a situação é --showlocals ou -l para abreviar:


 $ pytest -q --lf -l test_few_failures.py F ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) expected = 1.1e+25 sum_ = 1.01e+25 x = 1e+25 y = 1e+23 test_few_failures.py:17: AssertionError ================= 4 tests deselected ================= 1 failed, 4 deselected in 0.05 seconds 

O motivo da falha deve ser mais óbvio.


Para ter em mente que o teste falhou da última vez, há um pequeno truque. O pytest armazena informações de erro de teste da última sessão de teste e você pode visualizar as informações salvas com --cache-show :


 $ pytest --cache-show ===================== test session starts ====================== ------------------------- cache values ------------------------- cache/lastfailed contains: {'test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]': True} ================= no tests ran in 0.00 seconds ================= 

Ou você pode procurar no diretório de cache:


 $ cat .cache/v/cache/lastfailed { "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]": true } 

A opção --clear-cache permite limpar o cache antes de uma sessão.


O cache pode ser usado não apenas para --lf e --ff . Vamos escrever um suporte que registre quanto tempo os testes demoram, economiza tempo e na próxima vez que relatar um erro em testes que demoram o dobro do tempo, digamos, da última vez.


A interface para o dispositivo de cache é simples.


 cache.get(key, default) cache.set(key, value) 

Por convenção, os nomes das chaves começam com o nome do seu aplicativo ou plugin, seguido por / , e continuam a separar as seções dos nomes das chaves com / . O valor que você armazena pode ser qualquer um que seja convertido em json , conforme representado no .cache directory .


Aqui está nosso acessório usado para corrigir o tempo de teste:


ch4 / cache / test_slower.py


 @pytest.fixture(autouse=True) def check_duration(request, cache): key = 'duration/' + request.node.nodeid.replace(':', '_') #   (nodeid)    #      .cache #    -     start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = "       2-  " assert this_duration <= last_duration * 2, errorstring 

Como o equipamento é autouse , ele não precisa ser referenciado no teste. O objeto de solicitação é usado para obter o nodeid para usar na chave. nodeid é um identificador exclusivo que funciona mesmo com testes parametrizados. Adicionamos uma chave com 'duration /' para ser residentes respeitáveis ​​do cache. O código acima do rendimento é executado antes da função de teste; o código após o rendimento é executado após a função de teste.


Agora, precisamos de alguns testes com intervalos de tempo diferentes:


ch4 / cache / test_slower.py


 @pytest.mark.parametrize('i', range(5)) def test_slow_stuff(i): time.sleep(random.random()) 

Como você provavelmente não deseja escrever vários testes para isso, usei random e parametrização para gerar facilmente alguns testes que dormem por um período aleatório de tempo, todos menores que um segundo. Vamos ver algumas vezes como isso funciona:


 $ cd /path/to/code/ch4/cache $ pytest -q --cache-clear test_slower.py ..... 5 passed in 2.10 seconds $ pytest -q --tb=line test_slower.py ...E..E =================================== ERRORS ==================================== ___________________ ERROR at teardown of test_slow_stuff[1] ___________________ E AssertionError: test duration over 2x last duration assert 0.35702 <= (0.148009 * 2) ___________________ ERROR at teardown of test_slow_stuff[4] ___________________ E AssertionError: test duration over 2x last duration assert 0.888051 <= (0.324019 * 2) 5 passed, 2 error in 3.17 seconds 

Bem, isso foi divertido. Vamos ver o que está no cache:


 $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.958055 duration\test_slower.py__test_slow_stuff[1] contains: 0.214012 duration\test_slower.py__test_slow_stuff[2] contains: 0.19001 duration\test_slower.py__test_slow_stuff[3] contains: 0.725041 duration\test_slower.py__test_slow_stuff[4] contains: 0.836048 no tests ran in 0.03 seconds 

Você pode ver facilmente os dados de duration separadamente dos dados em cache devido ao prefixo dos nomes dos dados em cache. No entanto, é interessante que a funcionalidade lastfailed possa funcionar com uma única entrada de cache. Nossos dados de duração ocupam uma entrada de cache para cada teste. Vamos seguir o último exemplo falhado e colocar nossos dados em um registro.


Lemos e armazenamos em cache para cada teste. Podemos dividir o acessório no acessório do escopo da função para medir a duração e o acessório do escopo da sessão para leitura e gravação no cache. No entanto, se fizermos isso, não poderemos usar o dispositivo de cache porque ele tem o escopo da função. Felizmente, uma rápida olhada na implementação no GitHub mostra que o dispositivo de cache apenas retorna request.config.cache . Está disponível em qualquer área.


:


ch4/cache/test_slower_2.py


 Duration = namedtuple('Duration', ['current', 'last']) @pytest.fixture(scope='session') def duration_cache(request): key = 'duration/testdurations' d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid, None) is not None: errorstring = "test duration over 2x last duration" assert duration <= (d.last[nodeid] * 2), errorstring 

duration_cache . , , - . , namedtuple Duration current last . namedtuple test_duration , . , namedtuple , d.current . .


, :


 $ pytest -q --cache-clear test_slower_2.py ..... 5 passed in 2.80 seconds $ pytest -q --tb=no test_slower_2.py ...EE.. 7 passed, 2 error in 3.21 seconds $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower_2.py::test_slow_stuff[2]': True, 'test_slower_2.py::test_slow_stuff[3]': True} duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.483028, 'test_slower_2.py::test_slow_stuff[1]': 0.198011, 'test_slower_2.py::test_slow_stuff[2]': 0.426024, 'test_slower_2.py::test_slow_stuff[3]': 0.762044, 'test_slower_2.py::test_slow_stuff[4]': 0.056003, 'test_slower_2.py::test_slow_stuff[5]': 0.18401, 'test_slower_2.py::test_slow_stuff[6]': 0.943054} no tests ran in 0.02 seconds 

.


capsys


capsys builtin : stdout stderr , . stdout stderr.


, stdout:


ch4/cap/test_capsys.py

 def greeting(name): print('Hi, {}'.format(name)) 

, . - stdout. capsys:


ch4/cap/test_capsys.py

 def test_greeting(capsys): greeting('Earthling') out, err = capsys.readouterr() assert out == 'Hi, Earthling\n' assert err == '' greeting('Brian') greeting('Nerd') out, err = capsys.readouterr() assert out == 'Hi, Brian\nHi, Nerd\n' assert err == '' 

stdout stderr capsys.redouterr() . — , , .


stdout . , stderr :


 def yikes(problem): print('YIKES! {}'.format(problem), file=sys.stderr) def test_yikes(capsys): yikes('Out of coffee!') out, err = capsys.readouterr() assert out == '' assert 'Out of coffee!' in err 

pytest . print . . -s , stdout . , , . , pytest , , . capsys . capsys.disabled() , .


Aqui está um exemplo:


ch4/cap/test_capsys.py

 def test_capsys_disabled(capsys): with capsys.disabled(): print('\nalways print this') #    print('normal print, usually captured') #  ,   

, 'always print this' :


 $ cd /path/to/code/ch4/cap $ pytest -q test_capsys.py::test_capsys_disabled 

, always print this , capys.disabled() . print — print, normal print, usually captured ( , ), , -s , --capture=no , .


monkeypatch


"monkey patch" — . "monkey patching" — , , . monkeypatch . , , , , . , . API , monkeypatch .


monkeypatch :


  • setattr(target, name, value=<notset>, raising=True) : .
  • delattr(target, name=<notset>, raising=True) : .
  • setitem(dic, name, value) : .
  • delitem(dic, name, raising=True) : .
  • setenv(name, value, prepend=None) : .
  • delenv(name, raising=True) : .
  • syspath_prepend(path) : sys., Python.
  • chdir(path) : .

raising pytest, , . prepend setenv() . , + prepend + <old value> .


monkeypatch , , dot- . , dot- . , cheese- :


ch4/monkey/cheese.py

 import os import json def read_cheese_preferences(): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'r') as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'w') as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { 'slicing': ['manchego', 'sharp cheddar'], 'spreadable': ['Saint Andre', 'camembert', 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 'salads': ['crumbled feta'] } 

, write_default_cheese_preferences() . , . , . .


, . , read_cheese_preferences() , , write_default_cheese_preferences() :


ch4/monkey/test_cheese.py

 def test_def_prefs_full(): cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, , , cheese- . .


HOME set , os.path.expanduser() ~ , HOME . HOME , :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv('HOME', tmpdir.mkdir('home')) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, HOME . - expanduser() , , «On Windows, HOME and USERPROFILE will be used if set, otherwise a combination of….» . ! , Windows. , .


, HOME , expanduser :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, cheese os.path.expanduser() -. re.sub ~ . setenv() setattr() . , setitem() .


, , , . , , write_default_cheese_preferences() :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_defaults(tmpdir, monkeypatch): #      fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() defaults_before = copy.deepcopy(cheese._default_prefs) #     monkeypatch.setitem(cheese._default_prefs, 'slicing', ['provolone']) monkeypatch.setitem(cheese._default_prefs, 'spreadable', ['brie']) monkeypatch.setitem(cheese._default_prefs, 'salads', ['pepper jack']) defaults_modified = cheese._default_prefs #       cheese.write_default_cheese_preferences() #    actual = cheese.read_cheese_preferences() assert defaults_modified == actual assert defaults_modified != defaults_before 

_default_prefs - , monkeypatch.setitem() , .


setenv() , setattr() setitem() . del . , , - . monkeypatch .


syspath_prepend(path) sys.path , , . stub-. monkeypatch.syspath_prepend() , , stub-.


chdir(path) . , . , , monkeypatch.chdir(the_tmpdir) .


monkeypatch unittest.mock , . 7 " pytest " . 125.


doctest_namespace


doctest Python docstrings , , . pytest doctest Python --doctest-modules . doctest_namespace , autouse , pytest doctest . docstrings .


doctest_namespace , Python . , numpy import numpy as np .


. , unnecessary_math.py multiply() divide() , . , docstring , docstrings :


ch4/dt/1/unnecessary_math.py

 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> um.divide(10, 5) 2.0 """ return a / b 

unnecessary_math , um , import noecessary_math as um -. docstrings import , um . , pytest docstring . docstring , docstrings :


 $ cd /path/to/code/ch4/dt/1 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply FAILED ================================== FAILURES =================================== ______________________ [doctest] unnecessary_math.divide ______________________ 034 035 Returns a divided by b. 036 037 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined ... _____________________ [doctest] unnecessary_math.multiply _____________________ 022 023 Returns a multiplied by b. 024 025 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined /path/to/code/ch4/dt/1/unnecessary_math.py:23: UnexpectedException ================ 2 failed, 1 passed in 0.03 seconds ================= 

- import docstring:


ch4/dt/2/unnecessary_math.py

 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b 

:


 $ cd /path/to/code/ch4/dt/2 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide PASSED [ 66%] unnecessary_math.py::unnecessary_math.multiply PASSED [100%] ===================== 3 passed in 0.03 seconds ====================== 

docstrings .


doctest_namespace , autouse conftest.py , :


ch4/dt/3/conftest.py

 import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace['um'] = unnecessary_math 

pytest um doctest_namespace , unnecessary_math . conftest.py, doctests, conftest.py um .


doctest pytest 7 " pytest " . 125.


recwarn


recwarn , . Python , , , . , , , , . :


ch4/test_warnings.py

 import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning) # rest of function 

, :


ch4/test_warnings.py

 def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this' 

recwarn , category (), message (), filename ( ) lineno ( ), .


. , , , . recwarn.clear() , , .


recwarn , pytest pytest.warns() :


ch4/test_warnings.py

 def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this' 

pytest.warns() . recwarn pytest.warns() , , , .


Exercícios


  1. ch4/cache/test_slower.py autouse , check_duration() . ch3/tasks_proj/tests/conftest.py .
  2. 3.
  3. , 2x . 2x , 0.1 2x .
  4. pytest . ?

O que vem a seguir


pytest. . ; pytest.


Voltar Próximo

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


All Articles