이번 문서에서는 Pytest의 hook 기능에 대해서 알아보고자 Pytest 공식 사이트의 latest 버전의(2.4) hook reference 페이지를 번역해 보기로 했다.

Note:

Hook 함수들은 conftest.py에 추가해야 동작한다.

이 외의 pytest 관련 문서

  1. Python fixture
  2. Pytest with unittest

Contents

  1. Hook specification and validation

  2. Initialization, command line and configuration hooks

  3. Generic “runtest” hooks


Hook specification and validation

pytest는 초기화, 동작, 테스트 실행과 reporting을 hooking하기 위해서 hook 함수를 호출한다. pytest는 각각의 hook 함수는 그 hook specification에 따라서 유효성 검사를 하는 plugin을 불러온다. 각각의 hook 함수 이름과 함수의 매개변수 이름은 hook의 specification에 맞쳐주어야 한다. 하지만 hook 함수는 spec에 비해서 더 적게 매개변수를 갖고 있어도 일반적으로 상관없다. 만약 매개변수 이름이나 hook 함수의 이름을 잘못 입력한 경우 에러가 발생할 것이다.

밑에 있는 함수들의 테스트를 위해서 간략한 테스트 코드를 작성하였다.

test_example.py

1
2
3
4
5
6
import pytest

@pytest.mark.tryfirst
def test_example():

  assert 0

Initialization, command line and configuration hooks

pytest_cmdline_preparse(config, args) [deprecated]

opetion들이 파싱되기 전에 변경할 수 있다.

각 매개변수가 어떤 값을 보여주는지 확인해보기 위해서 print함수를 이용해서 출력해보았다.

conftest.py

1
2
3
4
5
def pytest_cmdline_preparse(config, args):
  print("cmdline preparse")

  print(config)
  print(args)

이 코드를 conftest.py에 추가하고 testing을 시작하면,

$ py.test -s
cmdline preparse
<_pytest.config.Config object at 0x10fa66790>
['-s']
========================================= test session starts =========================================
platform darwin -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2
rootdir: /Users/Luavis/Projects/pytest-examples, inifile:
collected 1 items

test_example.py F

============================================== FAILURES ===============================================
____________________________________________ test_example _____________________________________________

    @pytest.mark.tryfirst
    def test_example():

>     assert 0
E     assert 0

test_example.py:7: AssertionError
====================================== 1 failed in 0.01 seconds =======================================

결과를 얻을 수 있다. -s라는 option을 붙혀서 py.test를 실행했을때, args에는 -s가 list형식으로 들어가 있는것을 볼 수 있다.


pytest_addoption(parser)

ini 파일이나 argument로 등록되는 설정들을 새롭게 등록할 수 있다. 이 함수는 플러그 인에서 구현되어 있어야하며, 테스트가 시작될 때 오직 한 번만 실행된다.

Parameters

  • parser – 커맨드 라인 형식의 설정을 추가하고 싶다면, parser.addoption(…)를 호출하여 설정들을 추가할 수 있다. 만약 ini파일 형식의 설정을 추가하고 싶다면, parser.addini(…)를 호출하면 가능하다.

Note

옵션들은 나중에 config 객체를 통하여 접근할 수 있다.

config.getoption(name) 커맨드 라인으로 부터 받은 설정 값을 받을때

config.getini(name) ini file로 부터 받은 설정 값을 받을때

config 객체는 많은 pytest 내부 객체에 .config를 이용하여 접근할 수 있고, pytestconfig라는 fixture를 이용하여 접근할 수 있다. pytest module를 이용하여 pytest.config를 이용하여 접근할 수 있지만 deprecated된 기능이다.


pytest_cmdline_main(config)

main 커맨드 라인 동작이 실행될때 호출된다. 기본적인 구현은 configure hooks과 runtest_mainloop을 호출한다.

위의 pytest_addoption과 합친 예제를 보자면:

1
2
3
4
5
6
def pytest_addoption(parser):
  parser.addoption("--cmdopt", action="store", default="type1",
        help="my option: type1 or type2")

def pytest_cmdline_main(config):
  print(config.getoption("--cmdopt"))

add option hook을 이용하여 커맨드라인 형식의 옵션 –cmdopt를 추가하였고, 이를 기본값으로 실행되었을때 cmdline_main에서는 config에서 확인 할 수 있어야 한다.

$ py.test
type1
========================================= test session starts =========================================
platform darwin -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2
rootdir: /Users/Luavis/Projects/pytest-examples, inifile:
collected 1 items

test_example.py F

============================================== FAILURES ===============================================
____________________________________________ test_example _____________________________________________

    @pytest.mark.tryfirst
    def test_example():
>     assert 0
E     assert 0

test_example.py:7: AssertionError
====================================== 1 failed in 0.01 seconds =======================================

pytest_configure(config)

커맨드 라인 형식의 옵션이 모두 파싱되었고, 모든 플러그인과 initaial conftest가 호출된 뒤에 호출된다.


pytest_unconfigure(config)

테스트가 종료되기 전에 호출된다.


Generic “runtest” hooks

모든 runtest들은 pytest.Item 객체를 받을 수 있는 훅으로 연관되어 있다.

pytest_runtest_protocol(item, nextitem)

runtest의 setup, test의 호출, teardown을 예외처리, hook의 report 호출까지 포함하여 모두 item으로 받을 수 있다.

Parameters

  • item – runtest 가 현재 실행되고 있는 test item
  • nextitem – 다음에 실행될 것이라고 계획되어 있는 test item(마지막인 경우에는 None이다). 이 매개변수는 pytest_runtest_teardown()로 부터 온다.

Return [boolean]

만약 True를 반환하면 그 후의 hook이 동작하지 않는다.


pytest_runtest_setup(item)

pytest_runtest_call(item)가 호출되기 전에 호출된다.


pytest_runtest_call(item)

test item이 실행될때 호출된다.


pytest_runtest_teardown(item, nextitem)

pytest_runtest_call 호출된 뒤에 호출된다.

Parameters

  • nextitem – 다음에 실행될 것이라고 계획되어 있는 test item(마지막인 경우에는 None이다).

위의 모든 hook 함수들을 테스트 해보기 위하여, testcase code와 conftest.py를 수정했다.

test_example.py

1
2
3
4
5
6
7
import pytest

def test_example():
  pass

def test_example2():
  pass

conftest.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def pytest_runtest_protocol(item, nextitem):
  print("protocol")
  print(item)
  print(nextitem)

def pytest_runtest_setup(item):
  print("setup")
  print(item)

def pytest_runtest_call(item):
  print("call")
  print(item)

def pytest_runtest_teardown(item, nextitem):
  print("teardown")
  print(item)
  print(nextitem)

teardown에서 발생하는 로그를 capturing되지 않도록 하기 위해서 -s(–capture=no) option을 주어 실행해보면,

$ py.test -s
========================================= test session starts =========================================
platform darwin -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2
rootdir: /Users/Luavis/Projects/pytest-examples, inifile:
collected 2 items
protocol
<Function 'test_example'>
<Function 'test_example2'>

test_example.py setup
<Function 'test_example'>
call
<Function 'test_example'>
.teardown
<Function 'test_example'>
<Function 'test_example2'>
protocol
<Function 'test_example2'>
None
setup
<Function 'test_example2'>
call
<Function 'test_example2'>
.teardown
<Function 'test_example2'>
None


====================================== 2 passed in 0.01 seconds =======================================

test_example이 실행되고 후 test_example2가 실행될 것이다 그리고 protocol에서 이번 itemtest_example과 다음 실행될 test_example2nextitem인 것을 확인할 수 있다. 그리고 그 다음 protocol에서는 itemtest_example2이고 그 다음 실행될 run test는 없음으로 nextitem은 None으로 설정된것을 볼 수 있다.


pytest_runtest_makereport(item, call)

itempytest.Item의 instance이고, call_pytest.runner.CallInfo의 instance이다. 그리고 return 값은 _pytest.runner.TestReport의 instance여야 한다.

동작 방식을 보고자 아까 수정했던 코드에 조금 수정을 더하여 보았다.

conftest.py

1
2
3
4
5
6
7
8
9
10
11
12
# ...

def pytest_runtest_teardown(item, nextitem):
  return "tear down test"

def pytest_runtest_makereport(item, call):

  print("item")
  print(item)

  print("call")
  print(call)

test_example.py

1
2
3
4
5
6
7
import pytest

def test_example():
  pass

def test_example2():
  pass

그리고 test_example.py에서 test_example2를 지웠다. 그 결과를 출력해보면:

$ py.test
========================================= test session starts =========================================
platform darwin -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2
rootdir: /Users/Luavis/Projects/pytest-examples, inifile:
collected 1 items

test_example.py item
<Function 'test_example'>
call
<CallInfo when='setup' result: []>
item
<Function 'test_example'>
call
<CallInfo when='call' result: []>
.item
<Function 'test_example'>
call
<CallInfo when='teardown' result: ['tear down test']>


====================================== 1 passed in 0.01 seconds =======================================

이런 결과화면을 얻을 수 있다. teardown이 위에서 hook function에 return 값으로 처리된 값이 report쪽에서 result로 접근하여 볼 수 있다.