올바른 계정을 사용하여 로그인하는 상황을 검증하기 위한 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