# 빠르게 시작하는 pytest
pytest를 적용하면서 공부한 내용을 정리하는거라 간단하게 정리합니다. 추후 더 공부하며 업데이트 하겠습니다.
pytest is a framework that makes building simple and scalable tests easy. Tests are expressive and readable—no boilerplate code required. Get started in minutes with a small unit test or complex functional test for your application or library.
- 보일러플레이트 없이 바로 테스트 할 수 있는 테스트 프레임워크
공식 문서 : https://docs.pytest.org/en/latest/contents.html (opens new window)
# Installation
$ pip install -U pytest
# Quick start
# test_function.py
def func(x):
return x + 1
def test_answer():
assert func(3) == 5
$ pytest
# or
$ python -m pytest
# 프로젝트 구조
크게 어플리케이션 내부에 혹은 바깥에 테스트 코드를 두는 두가지 방법이 있다.
# 외부에 둘 경우
tests/
하위에 테스트 파일들
setup.py
mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
...
만약, 어플리케이션과 동일한 이름의 모듈(폴더)가 있다면 __init__.py
가 필요하다.
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
# 어플리케이션 코드와 함께둘 경우
setup.py
mypkg/
__init__.py
app.py
view.py
test/
__init__.py
test_app.py
test_view.py
...
reference: https://docs.pytest.org/en/latest/goodpractices.html (opens new window)
# 설정파일
설정파일은 가장 상위 rootdir
에 만든다. (혹은 --rootdir=path
와 같이 직접 값을 넘겨줄 수도 있다. rootdir에 대한 좀 더 상세한 내용은 여기 (opens new window)서 확인 할 수 있다.)
# 테스트 할 파일들 설정
# pytest.ini
[pytest]
python_files = tests.py test_*.py *_tests.py
# 정의한 형태의 파일만 테스트 파일로 읽는다.
# default command line options
# pytest.ini
[pytest]
addopts = -ra -q
그 밖의 command-line option 들 ...
--maxfail=2
: 몇 개까지 실패할때까지 테스트 할 것인지. 이 경우, 2개 실패하면 더이상 테스트를 진행하지 않고 멈춘다.-s
: 테스트 내 print, logging 다 보기
더 많은 설정 옵션들은 여기 (opens new window)에서 확인 할 수 있다.
# @pytest.mark.skip(reason="Just give me a reason")
- skip 하고자 하는 테스트
- 컨디션에 따라 테스트를 스킵하고 싶을 경우 아래와 같이 사용
@pytest.mark.skipif(os.environ.get("PROFILE", "local") != 'local', reason="run this test only at local")
# @pytest.fixture
- 테스트 시에 필요한 변수, 함수, 모듈, 클래스 등을 쉽게 가져다 쓸 수 있게 만들고, 함수의 arguments로 받아 바로 쓸 수 있게 해주는 기능.
- https://docs.pytest.org/en/latest/fixture.html (opens new window)
- https://docs.pytest.org/en/latest/reference.html#pytest-fixture-api (opens new window)
- 한글로 잘 정리된 fixture 글 (opens new window)
# 사용방법
@pytest.fixture()
def random_number():
import random
return random.randrange(1,10)
def test_random_range(random_number):
assert random_number > 1 and random_number < 10
# 사용할 수 있는 fixture 확인하는 방법
$ pytest --fixtures
# @pytest.mark.asyncio
- install
$ pip install pytest-asyncio
- reference : https://medium.com/ideas-at-igenius/testing-asyncio-python-code-with-pytest-a2f3628f82bc (opens new window)
# 다른 Framework들과의 테스트
# with Sanic
- Sanic에서 플러그인 제공 : pytest-sanic (opens new window)
- Fixtures : https://github.com/yunstanford/pytest-sanic#fixtures (opens new window)
@pytest.yield_fixture
def app():
app = Sanic("test_sanic_app")
@app.route("/test_get", methods=['GET'])
async def test_get(request):
return response.json({"GET": True})
@app.route("/test_post", methods=['POST'])
async def test_post(request):
return response.json({"POST": True})
yield app
@pytest.fixture
def test_cli(loop, app, test_client):
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
#########
# Tests #
#########
async def test_fixture_test_client_get(test_cli):
"""
GET request
"""
resp = await test_cli.get('/test_get')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"GET": True}
j = lambda **kwargs: json.dumps(kwargs)
async def test_fixture_test_client_post(test_cli):
"""
POST request with graphql
"""
data = j(query='''
query{
user(id:"123") {
id
}
}
''')
resp = await test_cli.post('/graphql', data=data , headers={'content-type': 'application/json', 'token': '1234')
assert resp.status == 200
resp_json = await resp.json()
assert resp_json == {"POST": True}
위 방법은 test route를 다시 만드는 방식.
테스트하려는 어플리케이션 Sanic App 만드는 부분을 app에 주입해주면 어플리케이션의 라우트 테스트 가능
from app.app import create_app
@pytest.fixture
def app():
app = create_app()
yield app
# with Peewee
DB를 테스트 하는 방법은 크게 3가지 방법이 있다.
- in-memory DB를 이용해 만들었다가 없애는 방법
- test용 database를 새로 만들었다가 지우는 방법
- transaction을 이용해 테스트가 끝나고 rollback하는 방법
peewee는 SQLite에 대해 in-memory DB를 제공하는데 (opens new window), 같이 사용중인 peewee-async 에서는 Postgresql과 Mysql (opens new window)만 지원하여 transaction을 이용하는 방법을 사용하였다.
transaction을 이용한 테스트에서 create 할 경우, auto_increment 하는 필드들이 영향을 받을수 있으므로 테스트 환경에 유의 한다.
class TestAsyncDatabase:
manager = None
database = MySQLDatabase(None)
@classmethod
async def setup_database(cls, app):
app.database = cls.database
cls.database.init(database="database")
app.database.set_allow_sync(False)
app.objects = cls.manager = Manager(app.database)
# transaction/rollback decorator
def db_unittest_run_loop(func, *args, **kwargs):
async def do_transaction(func, self, *inner_args, **inner_kwargs):
async with TestAsyncDatabase.manager.atomic() as txn:
await func(self, *inner_args, **inner_kwargs)
await txn.rollback()
@functools.wraps(func, *args, **kwargs)
def new_func(self, *inner_args, **inner_kwargs):
task = do_transaction(func, self, *inner_args, **inner_kwargs)
return self.loop.run_until_complete(task)
return new_func
class TestModel(BaseDatabaseTestCase):
@db_unittest_run_loop
async def test_db_connect(self): # connction 테스트
async def get_conn(objects):
await objects.connect()
return objects.database._async_conn
manager = TestAsyncDatabase.manager
c1 = await get_conn(manager)
c2 = await get_conn(manager)
assert c1 == c2
assert manager.is_connected
@pytest.mark.asyncio
@db_unittest_run_loop
async def test_create(self):
manager = TestAsyncDatabase.manager
async with manager.atomic():
obj1 = await manager.create(PeeweeModel, name="name")
obj2 = await manager.get(PeeweeModel, id=obj1.id)
assert obj1 == obj2
assert obj1.id == obj2.id
- references
# describe-context-it pattern
from pytest import mark as m
@m.describe("예시용 클래스")
class TestExample(object):
@m.context("@pytest.mark.it을 이용할 때")
@m.it("'- It: ' 데코레이터에 맞게 보여준다")
def test_it_decorator(self):
pass
- Describe: 예시용 클래스...
- Context: @pytest.mark.it을 이용할 때...
- ✓ It: '- It: ' 데코레이터에 맞게 보여준다