🌈 프로그래밍/Django

unmanaged table에 대한 django test 하는 방법

반응형

장고의 모델 클래스에는 Meta 옵션 중 하나로 managed라는 값이 존재한다.

이는 True 또는 False이고 기본값은 True로 설정된다.

True로 설정된다면 그 뜻은 다음과 같다.

쉽게 말해 장고가 해당 모델 클래스를 관리한다는 뜻이다.

이를 공식문서에서는 어떻게 표현하고 있냐면,

https://docs.djangoproject.com/en/4.2/ref/models/options/#managed

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

That is, Django manages the database tables’ lifecycles.

 

즉, 테이블의 수명 주기를 관리한다라고 한다.

테이블을 관리한다라는 의미는 데이터베이스에 정의된 테이블을 마이그레이션 하는 작업등을 의미한다.

따라서, False 옵션으로 정의한다면 해당 모델 클래스에 대해서는 DB에 반영하지 않도록 하는 것이다.

False 옵션은 모델이 다른 방법으로 생성된 기존 테이블을 바라보게 할 때 사용할 수 있는 옵션 중 하나이다.

이렇게 하면 기존 테이블에 대한 ORM을 작성할 수 있게된다.

하지만 managed 옵션이 False로 정의된 모델에 대해서 테스트할 때에는 설정의 일부로 올바른 테이블이 생성되었는지 확인하는 것은 개발자 본인 책임이라고 말한다.

For tests involving models with managed=False, it’s up to you to ensure the correct tables are created as part of the test setup.

django에서 테스트 명령을 사용해서 개발자가 작성한 테스트 코드들을 실행할 때 명시된 테스트 DB에 테스트할 모델들의 정보를 알고 있어야 한다.

따라서, managed = False로 정의된 모델이 있고, 해당 모델을 사용한 테스트코드가 있다면 정상적으로 테스트가 실행되지 않는다.

테스트를 돌리기 위해 다양한 방법이 존재하는데 상황에 따라서 적절하게 사용하면 좋을 것 같다.

 

1. managed 옵션 없이 makemigrations 명령어 비활성화하기

만약 서로 다른 프로젝트에서 동일한 데이터베이스를 사용하고 기존 테이블을 정의한다고 했을 때, 두 프로젝트에서 특정 테이블에 대하여 테이블 수명주기를 관리한다고 한다면 굉장히 위험하다.

따라서 한쪽에서는 반드시 해당 테이블에 대한 변경을 하지 못하도록 해야 한다.

Django에서는 모델 클래스에 변경이 있다면 반드시 데이터베이스에 반영해주어야 한다.

즉, makemigrations 명령어로 데이터베이스에 적용하기 위한 파일을 만들고,

migrate 명령어로 반영시키는 작업을 필수로 수행해주어야 한다.

위의 두 명령어를 프로젝트 내에서 사용하지 못하도록 막아두면 managed 옵션이 필요 없이 테스트 코드를 실행할 수 있다.

프로젝트 내에서 해당 django 명령어를 비활성화하기 위해서는 다음과 같은 파일을 작성해 준다.

# apps/no_migration_commands/management/commands/makemigrations.py

from typing import Any

from django.core.management.base import BaseCommand, CommandError


class Command(BaseCommand):
    help = "This command has been disabled."

    def handle(self, *args: Any, **options: Any) -> None:
        raise CommandError(
            "The 'makemigrations' and 'migrate' commands have been disabled in this project."
        )

이렇게 작성 후 프로젝트에서 아래 명령어를 수행하면 makemigrationsmigrate 명령을 하지 못하도록 할 수 있다.

❯ python manage.py makemigrations             
CommandError: The 'makemigrations' and 'migrate' commands have been disabled in this project.

❯ python manage.py migrate       
CommandError: The 'makemigrations' and 'migrate' commands have been disabled in this project.

프로젝트 내에서는 위의 두 명령어를 제한했지만, 테스트를 위해서는 테스트 데이터베이스에 모든 모델을 을 마이그레이션할 수 있어야 한다.

따라서, 추가적인 설정이 필요하다.

우선, 커스텀 테스트 러너를 정의해야 한다.

class NoMigrationsTestRunner(DiscoverRunner):
    """
    테스트를 위해 각 앱에 대해 마이그레이션 폴더를 생성하고 마이그레이션 파일을 생성합니다.
    테스트가 끝나면 각 앱에 대해 생성된 마이그레이션 폴더를 제거합니다.
    """

    def get_test_migrations_dir(self, app_name: str) -> Path:
        return Path.joinpath(Path(BASE_DIR), "apps", app_name, "test_migrations")

    def setup_databases(self, **kwargs: Any) -> Any:
        # 각 앱에 대해 마이그레이션 폴더 생성 후 마이그레이션 파일 생성
        for app_name in settings.MIGRATION_MODULES.keys():
            test_migrations_dir = self.get_test_migrations_dir(app_name)
            test_migrations_dir.mkdir(exist_ok=True)
            call_command("makemigrations", app_name)

        call_command("migrate")

        return super().setup_databases(**kwargs)

    def teardown_databases(self, old_config: Any, **kwargs: Any) -> None:
        # 각 앱에 대해서 테스트용 마이그레이션 폴더 제거

        for app_name in settings.MIGRATION_MODULES.keys():
            test_migrations_dir = self.get_test_migrations_dir(app_name)
            shutil.rmtree(test_migrations_dir)

        super().teardown_databases(old_config, **kwargs)

그리고 테스트 키워드가 인자로 넘어온다면 다음과 같이 정의한 테스트 러너로 처리하도록 명시해 준다.

# settings.py
TEST_RUNNER = (
    "apps.no_migration_commands.management.commands.test.NoMigrationsTestRunner"
)

if "test" in sys.argv:
    DATABASES = {}
    MIGRATION_MODULES = {
        "authentication": "apps.authentication.test_migrations",
        "manager": "apps.manager.test_migrations",
        "match": "apps.match.test_migrations",
        "plab_zone": "apps.plab_zone.test_migrations",
        "stadium": "apps.stadium.test_migrations",
        "user": "apps.user.test_migrations",
    }

    DATABASES["default"] = {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": ":memory:",
    }

    # makemigrations와 migrate 명령을 비활성화한 앱을 제거
    INSTALLED_APPS.remove("apps.no_migration_commands")

MIGRATION_MODULES에서는 앱 하위에 test_migrations라는 폴더에 테스트 시 마이그레이션 파일을 생성해 주는 경로를 정의한다.

 

2. managed = False 옵션 적용

다음 방법으로는 테이블 수명 주기 옵션을 비활성화한 상태로 모델 클래스를 정의하도록 한다.

class Meta:
    db_table = "my_table"
    managed = False

이 상태로 테스트 코드를 돌린다면 다음과 같은 에러 메시지를 확인할 수 있다.

    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: no such table: my_table

----------------------------------------------------------------------

managed = False로 정의된 테이블은 말 그대로 관리 대상이 아니기 때문에 테스트 데이터베이스에서 반영이 되지 않아 해당 테이블을 사용한 코드는 모두 에러를 내뱉는다.

다음과 같이 테스트 러너를 정의해 준다.

from api.settings import *
from django.apps import apps
from django.test.runner import DiscoverRunner

class UnManagedModelTestRunner(DiscoverRunner):
    def setup_test_environment(self, **kwargs: Any) -> None:
        self.unmanaged_models = [m for m in apps.get_models() if not m._meta.managed]
        for m in self.unmanaged_models:
            m._meta.managed = True
        return super().setup_test_environment(**kwargs)

    def teardown_test_environment(self, **kwargs: Any) -> None:
        super().teardown_test_environment(**kwargs)
        # reset unmanaged models
        for m in self.unmanaged_models:
            m._meta.managed = False

# Set Django's test runner to the custom class defined above
TEST_RUNNER = 'api.test_settings.UnManagedModelTestRunner'

MIGRATION_MODULES = {
    'api':'api.migrations_not_used_in_tests',
    'v1':'v1.migrations_not_used_in_tests'
}

apps 모듈에 정의된 get_models() 함수를 통해서 managed = False 옵션으로 정의된 모든 모델 클래스에 대한 정보를 가져온다.

그리고, 순회하면서 테스트 시에는 managed = True로 변경해 준다.

관련 이슈 참고 자료

반응형