pytest를 활용한 단위 테스트 작성 중 mock 객체 인식 안되는 문제 Troubleshooting

김동욱·2024년 4월 28일
0

Troubleshooting

목록 보기
8/14
post-thumbnail

상황

올바른 계정을 사용하여 로그인하는 상황을 검증하기 위한 serializer의 단위 테스트를 작성하던 중 마주한 문제이다.

serializer는 아래와 같이 구성돼있다.

[accout/serializers.py]

class UserLoginSerializer(serializers.Serializer):
    email = serializers.EmailField(
        required=True,
        error_messages={
            "required": "이메일은 필수 입력 항목입니다.",
            "blank": "이메일은 비워 둘 수 없습니다.",
        },
    )
    password = serializers.CharField(
        required=True,
        error_messages={
            "required": "비밀번호는 필수 입력 항목입니다.",
            "blank": "비밀번호는 비워 둘 수 없습니다.",
        },
    )

    def validate_email(self, value: str) -> str:
        if not User.objects.filter(email=value).exists():
            raise InvalidAccountException("등록되지 않은 이메일입니다.")
        return value

    def validate(self, data: dict[str, Any]) -> dict[str, Any]:
        user = authenticate(email=data["email"], password=data["password"])
        if user is None:
            raise InvalidFieldException("이메일 또는 비밀번호가 유효하지 않습니다.")

        data["user"] = user
        return data

로그인 성공에 대한 단위 테스트를 아래와 같이 작성했다.

테스트에서 사용할 Mock 객체를 선언했다.

[test_account/conftest.py]

@pytest.fixture
def mock_exists():
    with patch("django.db.models.query.QuerySet.exists") as mock:
        yield mock


@pytest.fixture
def user_login_data():
    return {
        "email": "test@example.com",
        "password": "Password1!",
    }


@pytest.fixture
def mock_authenticate():
    with patch("django.contrib.auth.authenticate") as mock:
        yield mock

선언한 Mock 객체를 활용하여 단위 테스트 코드를 아래와 같이 작성했다.

[test_account/test_serializers.py]

...

	def test_user_login_serializer_with_valid_data(self, user_login_data, mock_exists, mock_authenticate):
        mock_exists.return_value = True
        mock_authenticate.return_value = User(email="test@example.com")

        serializer = UserLoginSerializer(data=user_login_data)
        assert serializer.is_valid(raise_exception=True)

        validated_data = serializer.validated_data
        assert validated_data["email"] == "test@example.com"
        assert validated_data["password"] == "Password1!"

        mock_authenticate.assert_called_once_with(email="test@example.com", password="Password1!")
...

실행 결과 아래와 같은 에러가 발생했다.

self = <DatabaseWrapper vendor='postgresql' alias='default'>, name = None

    def _cursor(self, name=None):
        self.close_if_health_check_failed()
>       self.ensure_connection()
E       RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.

../../venv/lib/python3.9/site-packages/django/db/backends/base/base.py:306: RuntimeError

해결

결론부터 말하면 잘못된 모듈 경로를 참조했기 때문이다. authenticate 모듈을 직접 참조했는데, 내가 선언한 위치에서 간접 참조 방식으로 선언해야 올바르게 동작한다.

(X)
@pytest.fixture
def mock_authenticate():
    with patch("django.contrib.auth.authenticate") as mock:
        yield mock
        
(O)        
@pytest.fixture
def mock_authenticate():
    with patch("account.serializers.authenticate") as mock:
        yield mock

account.serializers 모듈에서 authenticate를 직접 임포트해서 사용하는 경우, Python은 authenticate 함수의 인스턴스를 account.serializers의 네임스페이스에 로컬로 저장한다. 이 경우, django.contrib.auth.authenticate를 목킹하는 것만으로는 충분하지 않고, 실제로 account.serializers 모듈에서 사용되는 authenticate의 참조를 목킹해야 한다.

같은 이유로 exists를 내가 임포트한 것이 아니기 때문에 직접 참조하여 Mock 객체를 생성해야 한다.

@pytest.fixture
def mock_exists():
    with patch("django.db.models.query.QuerySet.exists") as mock:
        yield mock





참고 자료

https://stackoverflow.com/questions/31377207/django-authenticate-unit-test

profile
안녕하세요! 질문과 피드백은 언제든지 환영입니다:)

0개의 댓글