Pytest-django: μ—¬λŸ¬ λ°μ΄ν„°λ² μ΄μŠ€λ‘œ ν…ŒμŠ€νŠΈ κ°„ 정리 지원

에 λ§Œλ“  2014λ…„ 03μ›” 20일  Β·  20μ½”λ©˜νŠΈ  Β·  좜처: pytest-dev/pytest-django

pytest-djangoλŠ” μ—¬λŸ¬ λ°μ΄ν„°λ² μ΄μŠ€μ™€ ν•¨κ»˜ Djangoλ₯Ό μ‚¬μš©ν•  λ•Œ ν…ŒμŠ€νŠΈ 사이λ₯Ό μ •λ¦¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ¬Έμ œλŠ” 이 StackOverflow 질문과 관련이 μžˆμŠ΅λ‹ˆλ‹€. http://stackoverflow.com/questions/10121485/django-testcase-not-using-transactions-on-secondary-database

db κ³ μ • μž₯μΉ˜λŠ” Django의 TestCaseλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

λ‹€μŒμ€ μ›λž˜ db κ³ μ • μž₯치의 μ½”λ“œμž…λ‹ˆλ‹€.

case = TestCase(methodName='__init__')
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

λ‹€μŒμ€ λ‚΄ μ½”λ“œμ—μ„œ ν•΄κ²° λ°©λ²•μœΌλ‘œ νŒ¨μΉ˜ν•œ λ°©λ²•μž…λ‹ˆλ‹€.

case = TestCase(methodName='__init__')
case.multi_db = True
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

λΆ„λͺ…νžˆ multi_db ν”Œλž˜κ·Έκ°€ μ‘΄μž¬ν•˜κ³  이유 λ•Œλ¬Έμ— 기본값은 Falseμž…λ‹ˆλ‹€. multi_db 지원을 pytest_django에 ν†΅ν•©ν•˜λŠ” μ˜¬λ°”λ₯Έ 방법이 ν™•μ‹€ν•˜μ§€ μ•ŠμœΌλ©° κ·ΈλŸ¬ν•œ 변경을 ν…ŒμŠ€νŠΈν•˜λŠ” μ˜¬λ°”λ₯Έ 방법도 ν™•μ‹€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

μ œμ•ˆ 사항이 있으면 λŒμ–΄μ˜€κΈ° μš”μ²­μ„ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ°€μž₯ μœ μš©ν•œ λŒ“κΈ€

μž„μ‹œ μ†”λ£¨μ…˜μ„ μ°ΎλŠ” μ‚¬λžŒλ“€μ„ μœ„ν•΄ multi_dbλ₯Ό ν™œμ„±ν™”ν•˜λŠ” 또 λ‹€λ₯Έ μ˜΅μ…˜μž…λ‹ˆλ‹€. 이것을 conftest.py에 λ„£μœΌμ‹­μ‹œμ˜€.

def pytest_sessionstart(session):
    from django.test import TransactionTestCase
    TransactionTestCase.multi_db = True

λͺ¨λ“  20 λŒ“κΈ€

두 개의 λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ‚¬μš©ν•˜λŠ” 패치 버전을 μ‚¬μš©ν•˜μ—¬ 예제 ν…ŒμŠ€νŠΈλ₯Ό μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆκΉŒ? μ—¬λŸ¬ λ°μ΄ν„°λ² μ΄μŠ€κ°€ μ‚¬μš©λ˜λŠ” 방식과 multi_dbλ₯Ό true둜 μ„€μ •ν•œ λ‹€μŒ λ‹€λ₯Έ λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ μ œμ–΄κ°€ ν•„μš”ν•œ 사항을 μ΄ν•΄ν•˜λŠ” 데 도움이 될 것이라고 μƒκ°ν•©λ‹ˆλ‹€.

DATABASES = {
    'default': {
        'ENGINE'   : 'django.db.backends.sqlite3',
        'NAME'     : 'default.db',
    },
    'operations': {
        'ENGINE'   : 'django.db.backends.sqlite3',
        'NAME'     : 'operations.db',
    },
}

λŒ€λΆ€λΆ„μ˜ / λͺ¨λ“  λͺ¨λΈμ€ 두 번째 λ°μ΄ν„°λ² μ΄μŠ€ "μž‘μ—…"κ³Ό μ—°κ²°λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. Django의 TestCaseλŠ” κΈ°λ³Έ dbμ—μ„œ νŠΈλžœμž­μ…˜μ„ μ˜¬λ°”λ₯΄κ²Œ ν”ŒλŸ¬μ‹œν•˜μ§€λ§Œ multi_db=True μ•„λ‹Œ ν•œ λ‹€λ₯Έ dbμ—μ„œλŠ” ν”ŒλŸ¬μ‹œν•˜μ§€ μ•Šμ„ 것이라고 μƒκ°ν•©λ‹ˆλ‹€.

λ‹€μŒμ€ λͺ‡ 가지 μƒ˜ν”Œ ν…ŒμŠ€νŠΈμž…λ‹ˆλ‹€.

from collection.models import Foo

def test_one(db):
    Foo.objects.create()
    assert Foo.objects.count() == 1

def test_two(db):
    assert Foo.objects.count() == 0

λ‹€μŒμ€ μœ„μ—μ„œ μ„€λͺ…ν•œ 패치된 db κ³ μ • μž₯치의 좜λ ₯μž…λ‹ˆλ‹€.

$ py.test multi_db_test/ -vv
======================================= test session starts =======================================
platform linux2 -- Python 2.7.2 -- py-1.4.20 -- pytest-2.5.2 -- bin/python
plugins: django
collected 2 items

multi_db_test/test_foo.py:3: test_one PASSED
multi_db_test/test_foo.py:7: test_two PASSED

==================================== 2 passed in 5.75 seconds =====================================

λ‹€μŒμ€ pytest_django 자체 db κ³ μ • μž₯치의 좜λ ₯μž…λ‹ˆλ‹€.

$ py.test multi_db_test/ -vv
======================================= test session starts =======================================
platform linux2 -- Python 2.7.2 -- py-1.4.20 -- pytest-2.5.2 -- bin/python
plugins: django
collected 2 items

multi_db_test/test_foo.py:3: test_one PASSED
multi_db_test/test_foo.py:7: test_two FAILED

============================================ FAILURES =============================================
____________________________________________ test_two _____________________________________________

db = None

    def test_two(db):
>       assert Foo.objects.count() == 0
E       assert 1 == 0
E        +  where 1 = <bound method Manager.count of <django.db.models.manager.Manager object at 0x3cd0890>>()
E        +    where <bound method Manager.count of <django.db.models.manager.Manager object at 0x3cd0890>> = <django.db.models.manager.Manager object at 0x3cd0890>.count
E        +      where <django.db.models.manager.Manager object at 0x3cd0890> = Foo.objects

multi_db_test/test_foo.py:8: AssertionError
=============================== 1 failed, 1 passed in 5.85 seconds ===============================

λ”°λΌμ„œ κ°„λ‹¨νžˆ 보면 multi_db κ³ μ • μž₯μΉ˜μ™€ pytest.mark.django_db(multi=True) κ°€ API둜 μž‘λ™ν•  수 μžˆλŠ” κ²ƒμ²˜λŸΌ λ³΄μž…λ‹ˆλ‹€. 더 λ‚˜μ€ 아이디어가 μžˆλŠ” μ‚¬λžŒμ΄ μžˆμŠ΅λ‹ˆκΉŒ?

ν•©λ¦¬μ μœΌλ‘œ λ“€λ¦½λ‹ˆλ‹€. 더 μ’‹λ“  그렇지 μ•Šλ“  λ‚˜λŠ” ν™•μ‹€νžˆ λ‹€λ₯Έ 아이디어가 μ—†μŠ΅λ‹ˆλ‹€.

μ—¬λ³΄μ„Έμš”. 이 문제 외에 multi_db 지원에 λŒ€ν•œ λ…Όμ˜κ°€ μžˆμŠ΅λ‹ˆκΉŒ? λ¬Έμ„œ μ—λŠ” 연락해야 ν•œλ‹€κ³  λ‚˜μ™€ μžˆμ§€λ§Œ ν˜„μž¬ μƒνƒœλŠ” μ–΄λ–»μŠ΅λ‹ˆκΉŒ?

@slafs 이 외에 닀쀑 λ°μ΄ν„°λ² μ΄μŠ€ 지원에 λŒ€ν•œ λ…Όμ˜/μš”μ²­μ΄ λ§Žμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

multi_db=True λ₯Ό μ „λ‹¬ν•˜λŠ” 것은 맀우 κ°„λ‹¨ν•˜μ§€λ§Œ 이에 λŒ€ν•œ μΌμ’…μ˜ ν…ŒμŠ€νŠΈλ„ ν•„μš”ν•˜λ―€λ‘œ 보쑰 λ°μ΄ν„°λ² μ΄μŠ€ 섀정도 ν•„μš”ν•©λ‹ˆλ‹€. λˆ„κ΅¬λ“ μ§€ 이 μž‘μ—…μ„ μˆ˜ν–‰ν•  의ν–₯이 μžˆλ‹€λ©΄ ν›Œλ₯­ν•  κ²ƒμž…λ‹ˆλ‹€. :)

μ‹œμž‘ κ²Œμ‹œλ¬Όμ—μ„œ @ftobiaκ°€ μ–ΈκΈ‰ν•œ ν•΄κ²° 방법을 μ‚¬μš©ν–ˆμ§€λ§Œ 1.7μ—μ„œ Django 1.8.4둜 μ—…κ·Έλ ˆμ΄λ“œν•œ ν›„ μž‘λ™μ΄ μ€‘μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

setup/tearDown을 μ²˜λ¦¬ν•˜λŠ” μ½”λ“œκ°€ μΈμŠ€ν„΄μŠ€ 속성 λŒ€μ‹  multi_db 클래슀 속성을 찾도둝 Djangoκ°€ λ³€κ²½ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

@ftobia 의 ν•΄κ²° 방법을 λ‹€μŒμœΌλ‘œ λ³€κ²½ν•˜λ©΄

case = TestCase(methodName='__init__')
case.__class__.multi_db = True
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

+1:+1:

이것은 Django ν…ŒμŠ€νŠΈ λŸ¬λ„ˆμ—μ„œ pytest둜 μ „ν™˜ν•˜λŠ” 것을 막고 μžˆμŠ΅λ‹ˆλ‹€.

기본적으둜 https://github.com/pytest-dev/pytest-django/blob/master/pytest_django/fixtures.py#L107μ—μ„œ django_case.multi_db = True μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” μ΄μœ λŠ” λ¬΄μ—‡μž…λ‹ˆκΉŒ?

+1

+1

django_db λ§ˆμ»€μ— λ§ˆμ»€κ°€ 있으면 쒋은 APIκ°€ 될 것이라고 μƒκ°ν•©λ‹ˆλ‹€.

@pytest.mark.django_db(transactional=True, multi_db=True)
def test_a():
    pass

κ΅¬ν˜„μ€ reset_sequences (PR #308) λ˜λŠ” serialized_rollback (PR #353)와 비ꡐ적 μœ μ‚¬ν•΄μ•Ό ν•©λ‹ˆλ‹€.

"μ–΄λ €μš΄" 뢀뢄은 μ—¬λŸ¬ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό ν¬ν•¨ν•˜λ„λ‘ pytest-django의 λ‚΄λΆ€ ν…ŒμŠ€νŠΈ μ œν’ˆκ΅°μ„ κ°œμ„ ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

λˆ„κ΅°κ°€μ΄ μž‘μ—…μ„ μ›ν•˜λ©΄ ν™•μ‹€νžˆ κ²€ν† ν•˜κ³  λ³‘ν•©ν•˜λŠ” 데 도움이 될 κ²ƒμž…λ‹ˆλ‹€. 이것이 λ‹Ήμ‹ μ—κ²Œ ν₯미둜울 것이라면 PR μž‘μ—…μ„ μ‹œμž‘ν•˜μ‹­μ‹œμ˜€!

https://github.com/pytest-dev/pytest-django/pull/397#issuecomment -261987751에 λͺ‡ 가지 μ˜κ²¬μ„ 남기고 일반적인 λ°©μ‹μœΌλ‘œ λ³€κ²½ν•  수 μžˆλŠ” PR을 λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€: https://github. com/pytest-dev/pytest-django/pull/431.com/pytest-dev/pytest-django/pull/431.com/pytest-dev/pytest-django/pull/431

μž„μ‹œ μ†”λ£¨μ…˜μ„ μ°ΎλŠ” μ‚¬λžŒλ“€μ„ μœ„ν•΄ multi_dbλ₯Ό ν™œμ„±ν™”ν•˜λŠ” 또 λ‹€λ₯Έ μ˜΅μ…˜μž…λ‹ˆλ‹€. 이것을 conftest.py에 λ„£μœΌμ‹­μ‹œμ˜€.

def pytest_sessionstart(session):
    from django.test import TransactionTestCase
    TransactionTestCase.multi_db = True

ν•΄κ²° 방법을 μ°ΎλŠ” λ‹€λ₯Έ μ‚¬λžŒμ„ μœ„ν•΄ multi_db λŠ” Django 2.2μ—μ„œ 더 이상 μ‚¬μš©λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‚˜λ₯Ό μœ„ν•΄ μΌν•œ κ΅μ²΄λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

TransactionTestCase.databases = set(settings.DATABASES.keys())

@jcushman μ‚¬μš©ν•œ 전체 κ³ μ • μž₯μΉ˜λ‚˜ μ½”λ“œ 쑰각을 κ³΅μœ ν•΄λ„ λ κΉŒμš”?

μ΄μœ κ°€ 무엇이든 우리 기지 conftest.py pytest_sessionstart() conftest.py 은(λŠ”) 아무 일도 ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν‘œμ€€ pytest monkeypatch κ³Ό ν•¨κ»˜ #342μ—μ„œ νŽ˜μ΄μ§€λ₯Ό κ°€μ Έ μ™€μ„œ 닀쀑 λ°μ΄ν„°λ² μ΄μŠ€ μ‚¬μš©μ„ μœ„ν•΄ λ‹€μŒμ„ λ°©λ¬Έν–ˆμŠ΅λ‹ˆλ‹€.

# OPTION 1: Function fixture; must be included with tests
@pytest.fixture
def django_db_multiple(monkeypatch, request, settings):
    """
    Ensure all test functions using Django test cases have multiple database
    support. This is mostly/only so that Django will wrap ALL database use with
    atomic blocks like it does for DEFAULT_DB_ALIAS.

    https://github.com/django/django/blob/master/django/test/testcases.py#L903
    https://github.com/pytest-dev/pytest-django/issues/76
    """
    from django.test import TestCase
    from django.test import TransactionTestCase

    db_keys = set(settings.DATABASES.keys())

    monkeypatch.setattr(TestCase, 'databases', db_keys)
    monkeypatch.setattr(TransactionTestCase, 'databases', db_keys)

@pytest.mark.django_db
def test_some_test(django_db_multiple):
    pass

# OPTION 2: Session fixture
@pytest.fixture(autouse=True, scope='session')
def django_db_multiple():
    """
    Ensure all test functions using Django test cases have multiple database
    support. This is mostly/only so that Django will wrap ALL database use with
    atomic blocks like it does for DEFAULT_DB_ALIAS.

    https://github.com/django/django/blob/master/django/test/testcases.py#L903
    https://github.com/pytest-dev/pytest-django/issues/76
    https://github.com/pytest-dev/pytest/issues/1872
    """
    from _pytest.monkeypatch import MonkeyPatch
    from django.test import TestCase
    from django.test import TransactionTestCase
    from django.conf import settings

    db_keys = set(settings.DATABASES.keys())

    monkeypatch = MonkeyPatch()
    monkeypatch.setattr(TestCase, 'databases', db_keys)
    monkeypatch.setattr(TransactionTestCase, 'databases', db_keys)

    yield monkeypatch

    monkeypatch.undo()

_νŽΈμ§‘: μ„Έμ…˜ κ³ μ • μž₯치둜 μ΄λ™ν–ˆμŠ΅λ‹ˆλ‹€._

unittest Django ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λ‘œ μ „ν™˜ν•˜μ—¬ pytest 초λŠ₯λ ₯을 μžƒμ§€ μ•ŠκΈ° μœ„ν•΄ pytest-django λ‚΄λΆ€λ₯Ό 이 λͺ»μƒκΈ΄(아직 μž‘λ™ν•˜λŠ”!) 해킹에 λ³΅μ‚¬ν•˜κ³  νŒ¨μΉ˜ν–ˆμŠ΅λ‹ˆλ‹€.

from typing import Optional, Type, TypeVar

import pytest
from django.test import TransactionTestCase
from pytest_django.django_compat import is_django_unittest

TestCase = TypeVar('TestCase', bound=TransactionTestCase)


def _django_db_fixture_helper(
    request, django_db_blocker, transactional: bool = False, reset_sequences: bool = False,
) -> Optional[Type[TestCase]]:
    if is_django_unittest(request):
        return None

    if not transactional and 'live_server' in request.fixturenames:
        # Do nothing, we get called with transactional=True, too.
        return None

    django_db_blocker.unblock()
    request.addfinalizer(django_db_blocker.restore)

    if transactional:
        from django.test import TransactionTestCase as DjangoTestCase  # noqa: WPS433

        if reset_sequences:

            class ResetSequenceTestCase(DjangoTestCase):  # noqa: WPS431
                reset_sequences = True

            DjangoTestCase = ResetSequenceTestCase  # type: ignore[misc] # noqa: N806
    else:
        from django.test import TestCase as DjangoTestCase  # type: ignore[no-redef] # noqa: WPS433

    return DjangoTestCase  # type: ignore[return-value]


@pytest.fixture()
def db_case(request, django_db_setup, django_db_blocker):
    """Require a django test database.

    This database will be setup with the default fixtures and will have
    the transaction management disabled. At the end of the test the outer
    transaction that wraps the test itself will be rolled back to undo any
    changes to the database (in case the backend supports transactions).
    This is more limited than the ``transactional_db`` resource but
    faster.

    If multiple database fixtures are requested, they take precedence
    over each other in the following order (the last one wins): ``db``,
    ``transactional_db``, ``django_db_reset_sequences``.
    """
    if 'django_db_reset_sequences' in request.fixturenames:
        request.getfixturevalue('django_db_reset_sequences')
    if (
        'transactional_db' in request.fixturenames
        or 'live_server' in request.fixturenames
    ):
        request.getfixturevalue('transactional_db')
    else:
        django_case: Optional[Type[TransactionTestCase]] = _django_db_fixture_helper(
            request, django_db_blocker, transactional=False,
        )

        def factory(dbs=None):  # noqa: WPS430
            if django_case is None:
                return
            CaseType: Type[TransactionTestCase] = django_case  # noqa: N806
            if dbs is not None:
                class DatabasesSetTestCase(  # noqa: WPS431
                    CaseType,  # type: ignore[valid-type, misc]
                ):
                    databases = dbs
                CaseType = DatabasesSetTestCase  # noqa: N806
            test_case: TransactionTestCase = CaseType(methodName='__init__')
            test_case._pre_setup()  # type: ignore[attr-defined] # noqa: WPS437
            request.addfinalizer(
                test_case._post_teardown,  # type: ignore[attr-defined] # noqa: WPS437
            )

        return factory

κ·Έκ²ƒμœΌλ‘œ λ‹€μŒκ³Ό 같은 것을 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

class TestCase(object):

    def test_ok(
        self,
        db_case,
        ...
    ):
        db_case(dbs=('non_default_db_alias',))

이것은 db κ³ μ •μž₯μΉ˜λ‚˜ pytest.mark.django_db 마크 λŒ€μ‹ μ— μ‚¬μš©λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

@jcushman μ‚¬μš©ν•œ 전체 κ³ μ • μž₯μΉ˜λ‚˜ μ½”λ“œ 쑰각을 κ³΅μœ ν•΄λ„ λ κΉŒμš”?

λ‹€λ₯Έ μ‚¬λžŒμ΄ 이에 λΆ€λ”ͺμΉ  경우λ₯Ό λŒ€λΉ„ν•˜μ—¬ λ‹΅μž₯을 μΆ”κ°€ν•©λ‹ˆλ‹€.

conftest.py μ•„λž˜ μ½”λ“œλ₯Ό μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이 κ³ μ • μž₯μΉ˜λŠ” μžλ™μœΌλ‘œ μ„ νƒλ˜λ―€λ‘œ ν…ŒμŠ€νŠΈμ— μΆ”κ°€ν•  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.

```파이
def pytest_sessionstart(μ„Έμ…˜):
django.testμ—μ„œ TransactionTestCase κ°€μ Έμ˜€κΈ°

TransactionTestCase.databases = set(settings.DATABASES.keys())

````

λ”°λΌμ„œ 이 λ¬Έμ œλŠ” μ μ ˆν•œ 닀쀑 DB μ§€μ›μœΌλ‘œ κ·€κ²°λœλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€. λ‚˜λŠ” 이 문제의 μ€‘λ³΅μœΌλ‘œ 이 문제λ₯Ό 닫을 κ²ƒμž…λ‹ˆλ‹€.

이 νŽ˜μ΄μ§€κ°€ 도움이 λ˜μ—ˆλ‚˜μš”?
0 / 5 - 0 λ“±κΈ‰