pytest fixture

5 minute read

test fixture를 쓰는 목적은 테스트가 안정적이고 반복적으로 실행될 수 잇는 고정된 기준점을 제공하는 것입니다. pytest fixture는 고전적인 xUnit 스타일의 설정/해제 기능을 극적으로 향상시킵니다.

  • fixture는 명시적인 이름을 가지며, 테스트 함수, 모듈, class, 혹은 전체 프로젝트에서 사용을 선언함으로써 활성화됩니다.
  • fixture는 모듈화 방식으로 구현됩니다. 각 fixture의 이름은 다른 fixture에서 사용 가능하도록 fixture 함수를 만들어냅니다.
  • fixture 관리는 단순한 단위에서 복잡한 기능 테스트에 이르기까지 다양하며 구성 및 구성 요소 옵션에 따라 fixture 및 테스트를 매개 변수화하거나 함수, 클래스, 모듈 또는 전체 테스트 session 범위에서 fixture를 재사용 할 수 있습니다.

또한 pytest는 고전적인 xunit스타일 설정을 계속 지원합니다. 원하는대로 고전적인 스타일에서 새로운 스타일로 점진적으로 이동하면서 두 스타일을 혼합하여 사용할 수 있습니다. 기존 unittest.TestCase스타일 또는 nose 기반 프로젝트에서 사용할 수도 있습니다.

함수의 매개변수 Fixture

테스트 함수는 fixture 객체를 매개 변수로 받을 수 있습니다. 각 매개 변수 이름에 대해 그 이름을 가진 fixture 함수는 fixture 객체를 제공합니다. fixture 함수는 @pytest로 표시되어 등록됩니다. 아래 코드는 test 모듈을 포함한 fixture, test 함수의 간단한 예시입니다.

# content of ./test_smtpsimple.py
import pytest

@pytest.fixture
def smtp_connection():
	import smtplib
	return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
def test_ehlo(smtp_connection):
	response, msg = smtp_connection.ehlo()
	assert response == 250
	assert 0 # for demo purposes

여기서 test_ehlostmp_connection fixture 값을 필요로합니다. pytest는 @pytest가 표시된 같은 함수를 찾아서 호출합니다. 이 테스트를 실행한 결과는 다음과 같습니다.

$ pytest test_smtpsimple.py
======================== test session starts =========================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_smtpsimple.py F 							[100%]

============================== FAILURES ==============================
_____________________________ test_ehlo ______________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

	def test_ehlo(smtp_connection):
		response, msg = smtp_connection.ehlo()
		assert response == 250
> 		assert 0 # for demo purposes
E 		assert 0

test_smtpsimple.py:11: AssertionError
====================== 1 failed in 0.12 seconds ======================

실패 상황에서 stmp_connection에 포함되어 있는 smtlib을 사용하여 테스트 함수가 호출되었음을 알 수 있습니다. fixture 함수로 생성된 SMTP()는 assert 0 에서 실패합니다. pytest가 테스트 함수를 이런 방식으로 호출하기 위해 사용한 정확한 프로토콜은 다음과 같습니다.

  1. pytest는 test_ 접두어로 test_ehlo를 찾습니다. 테스트 함수에는 smtp_connection이라는 매개 변수가 필요합니다. 일치하는 fixture를 사용하기 위해 @pytest가 표시된 같은 이름의 함수를 찾습니다.
  2. smtp_connection이 call 되어 객체가 생성됩니다.
  3. test_ehlo(<smtp_connection instance>) 가 call 되어 테스트에 실패합니다.

매개 변수의 철자를 잘못 입력했거나 사용할 수 없는 것을 사용하려는 경우 smtp_connection 이라는 사용 가능한 매개 변수 목록에 오류가 표시됩니다.


사용할 수 있는 fixture를 보기 위해서는 아래와 같이 하십시오.

pytest --fixtures test_simplefactory.py

-v 옵션을 입력하면 fixture 이름 앞의 _ 가 무시됩니다.


fixture: dependency injection의 대표적인 예

fixture를 통해 가져오기, 설정, 정리 등 세부사항을 신경쓰지 않고도 사전에 초기화된 특정 응용 프로그램 객체를 쉽게 받아 작업할 수 있습니다. fixture 함수가 injector 역할을 하고 테스트 함수가 fixture 객체의 소비자인 대표적인 dependency injection의 예 입니다.

conftest.py: ficture 함수의 공유

테스트를 구현하는 동안 여러 테스트 파일의 fixture 함수를 사용하려는 경우 이를 conftest.py 파일로 옮길 수 있습니다. 테스트에 사용할 fixture 함수를 가져 올 필요가 없으며, 자동으로 pytest에 의해 발견됩니다. Fixture 함수의 발견은 테스트 class, 테스트 모듈, 그리고 conftest.py 파일 그리고 마지막으로 내장 플러그인과 third-party 플러그인 순으로 진행됩니다. 또한, local per-directory plugins를 사용하여 ‘conftest.py’ 를 구현할 수 있습니다.

테스트 정보의 공유

파일에서 테스트 정보를 테스트에 사용할 수 있게 하려면, 이 정보를 테스트에서 사용할 수 있도록 fixture에 load해야 합니다. 이것은 pytest의 자동 캐싱 메커니즘을 사용합니다. 또 다른 좋은 방법은 테스트 폴더에 데이터 파일을 추가 하는 것 입니다. 이러한 방식에 도움이 되는 커뮤니티 플러그인이 있습니다.(예: pytest-datadir 및 pytest-datafile)

범위: class, 모듈, session 내부에서의 fixture 객체의 공유

네트워크 연결이 필요한 fixture는 연결성에 달려 있으며 대개 시간이 많이 소요됩니다. 앞의 예제를 확장하면 @pytest.fixture 호출에 scope=”modlue” 매개 변수를 추가하여 smtp_connection fixture 함수가 테스트 모듈당 한 번만 호출되도록 할 수 있습니다.(기본 값은 테스트 함수 당 한번 호출하는 것 입니다.) 테스트 모듈의 여러 테스트 기능은 각각 동일한 smtp_connection fixture 인스턴스를 수신하므로 시간이 절약됩니다. 범위로 사용할 수 있는 값은 fucntion, class, module, package, 또는 session 입니다. 다음 예제는 fixture 함수를 별도의 conftest.py 파일에 저장하여 디렉토리에 있는 여러 테스트 모듈의 테스트가 fixture 함수에 접근 할 수 있도록합니다.

# content of conftest.py
import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp_connection():
	return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

fixture의 이름은 smtp_connection이며, 테스트 또는 conftest.py가 있는 디렉토리 혹은 그 하위 디렉토리에 있는 fixture 함수에 smtp_connection이라는 이름을 입력 매개 변수로 나열하여 결과에 접근 할 수 있습니다.

# content of test_module.py

def test_ehlo(smtp_connection):
	response, msg = smtp_connection.ehlo()
	assert response == 250
	assert b"smtp.gmail.com" in msg
	assert 0 # for demo purposes


def test_noop(smtp_connection):
	response, msg = smtp_connection.noop()
	assert response == 250
	assert 0 # for demo purposes

우리는 무슨 일이 일어나고 있는지 검사하기 위해 실패할 assert 0 문을 삽입하고 테스트를 진행합니다.

$ pytest test_module.py
========================= test session starts ==========================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 2 items

test_module.py FF 							[100%]

=============================== FAILURES ===============================
______________________________ test_ehlo _______________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

	def test_ehlo(smtp_connection):
		response, msg = smtp_connection.ehlo()
		assert response == 250
		assert b"smtp.gmail.com" in msg
> 		assert 0 # for demo purposes
E 		assert 0
	test_module.py:6: AssertionError
______________________________ test_noop _______________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

	def test_noop(smtp_connection):
		response, msg = smtp_connection.noop()
		assert response == 250
> 		assert 0 # for demo purposes
E 		assert 0

test_module.py:11: AssertionError
======================= 2 failed in 0.12 seconds =======================

위의 결과를 확인함으로써 동일한 smtp_connection 객체가 두개의 테스트 함수로 전달되었음을 볼 수 있습니다. 결과적으로 smtp_connection을 사용하는 두 테스트 함수는 동일한 인스턴스를 다시 사용하기 때문에 단일 테스트만큼 빠르게 실행됩니다. 세션 범위로 지정된 smtp_connection 인스턴스를 갖고 싶다면 간단히 선언하면 됩니다.

@pytest.fixture(scope="session")
def smtp_connection():
	# the returned fixture value will be shared for
	# all tests needing it
	...

마지막으로, class 범위는 test class가 실해될때 마다 실행됩니다.


주의: pytest는 한번에 하나의 fixture만 캐시합니다. 즉, 매개 변수화 된 fixture를 사용할 때 pytest는 주어진 범위 내에서 fixture를 두 번 이상 호출할 수 있습니다.


상위 범위의 fixture가 먼저 인스턴스화

feature에 대한 함수의 요청에서, 상위 범위(예: session)의 fixture는 함수 또는 class와 같은 낮은 범위의 fixture보다 먼저 인스턴스화됩니다. 동일한 범위의 fixture의 상대적 순서는 테스트 함수에서 선언 된 순서를 따르고 fixture들 사이의 의존성을 존중합니다. 아래의 코드를 살펴봅시다.

@pytest.fixture(scope="session")
def s1():
	pass

@pytest.fixture(scope="module")
def m1():
	pass

@pytest.fixture
def f1(tmpdir):
	pass

@pytest.fixture
def f2():
	pass

def test_foo(f1, m1, f2, s1):
	...

test_foo에 의해 요청된 fixture는 다음과 같은 순서로 인스턴스화 됩니다.

  • s1: 제일 상위 범위의 fixture(session)
  • m1: 두번째로 높은 범위의 fixture(module)
  • tmpdir: f1에 의해 요청되는 함수 범위의 fixture, f1에 의해 사용되기 때문에 이 시점에서 인스턴스화 되어야 합니다.
  • f1: test_foo의 첫번째 매개 변수인 function 범위의 fixture
  • f2: test_foo의 마지막 매개 변수인 function 범위의 fixture