개요
api를 테스트할 때는 pytest-django에는 client라는 fixture를 통해 api를 테스트하는데 이 fixture는 내부적으로 뭘로 구현한 거지 싶어서 살펴보고 있었다.
내부를 살펴보니 django의 test 패키지의 client 모듈의 RequestFactory를 사용하는 형태였다. 그렇다면 django의 view를 테스트하는 다른 형태의 코드를 작성할 수 있지 않을까 싶은 호기심이 생겼다.
Setting
테스트 대상인 프로젝트의 구조는 대충 다음과 같이 생겼다.
╰─$ tree -L 2
.
├── Makefile
├── docker
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── init-db.sh
├── pytest.ini
├── readme.md
├── requirements.txt
├── src
│ ├── __init__.py
│ ├── __pycache__
│ ├── apps
│ ├── config
│ └── manage.py
├── test
│ ├── __pycache__
│ └── test_api.py
test_api.py에서 src.apps의 view를 테스트해볼것이다. 테스트해볼 view의 코드는 다음과 같다
from django.views.generic import View
from django.http.response import JsonResponse
class NumberView(View):
def get(self, request):
return JsonResponse(
data={
'number': 1
}
)
Client Fixuture
개요에서 언급했듯이 먼저 client fixture를 이용해서 테스트 하는 코드이다.
from src.apps.urls import greeting
def test_get_view(client):
response = client.get(path="/api/number")
assert response.status_code == 200
╰─$ pytest 1 ↵
======================================================== test session starts ========================================================
platform darwin -- Python 3.8.16, pytest-7.3.2, pluggy-1.0.0 -- /Users/jako/private/git-repo/django-base-configure/venv/bin/python3
cachedir: .pytest_cache
django: settings: src.config.settings.local (from ini)
rootdir: /Users/jako/private/git-repo/django-base-configure
configfile: pytest.ini
plugins: django-4.5.2
collected 1 item
test/test_api.py::test_get_view PASSED [100%]
client fixture를 사용하는 가장 간단한 테스트 코드 모양이다. 여기서 눈여겨볼 점은 client fixture를 타고 들어가면 내부의 코드는 다음과 같이 생겼다는 것이다.
# path: /Users/jako/private/git-repo/django-base-configure/venv/lib/python3.8/site-packages/pytest_django/fixtures.py
@pytest.fixture()
def client() -> "django.test.client.Client":
"""A Django test client instance."""
skip_if_no_django()
from django.test.client import Client
return Client()
client fixture의 타입이 django.test.client.Client를 반환한다는 것이 보인다. 즉 pytest-django는 기본적으로 django의 test client 모듈을 이용한다 것을 알 수 있다.
Django Client
그렇다면 test 코드를 작성할 때 pytest-django에서도 django test client을 이용해서도 view를 테스트할 수 있을 것이다.
from django.test.client import Client
def test_get_view_with_django_test_client():
client = Client()
response = client.get(path="/api/number")
assert response.status_code == 200
╰─$ pytest
======================================================== test session starts ========================================================
platform darwin -- Python 3.8.16, pytest-7.3.2, pluggy-1.0.0 -- /Users/jako/private/git-repo/django-base-configure/venv/bin/python3
cachedir: .pytest_cache
django: settings: src.config.settings.local (from ini)
rootdir: /Users/jako/private/git-repo/django-base-configure
configfile: pytest.ini
plugins: django-4.5.2
collected 1 item
test/test_api.py::test_get_view_with_django_test_client PASSED [100%]
무엇을 사용할까?
pytest-django의 client fixture는 fixture의 장점을 얻기 위해 django test.client를 fixture로 따로 구현해놓은것이다. 하지만 테스트 코드를 작성하다 보면 테스트 코드에 파라미터가 과다하게 사용되는 경우가 종종 생기곤 한다.
def test_some_view(client, test_param1, test_param2, test_param3, test_param4): ...
이럴 경우에는 굳이 fixture를 사용하는 것이 아니라 test 함수에는 어떤 파라미터를 테스트하는지와 테스트 코드 안에서 어떤 클라이언트를 사용하게 되는지 구분해서 사용하는 형태로 구성하면 조금 더 보기 좋았다.
def test_some_view(test_param1, test_param2, test_param3, ...):
client = Client() # django.test.client
mock_clinet = MockClient # MockClient
Test Client도 내부를 파보자.
이제 앞서 살펴본 django.test.client.Client 모듈을 타고 또 들어가 보자.
# path: /Users/jako/private/git-repo/django-base-configure/venv/lib/python3.8/site-packages/django/test/client.py
class Client(ClientMixin, RequestFactory):
...
def get(self, path, data=None, follow=False, secure=False, **extra):
"""Request a response from the server using GET."""
self.extra = extra
response = super().get(path, data=data, secure=secure, **extra)
if follow:
response = self._handle_redirects(response, data=data, **extra)
return response
위 코드는 django test client 모듈을 Client Class이다 RequestFactory라는 걸 상속받고 있는데 이 RequestFactory는 DRF 문서에도 그 설명이 나와있다. 이때 한 가지 궁금증이 생겼다. RequestFactory를 이용하면 View Class에 직접적으로 호출할 수 있지 않을까 싶은 점이다.
원래 View의 Request는 뭐지?
RequestFactory를 이용하여 Django View를 직접적으로 호출해도 문제가 없는지 알아보기 위해 원래 Django View에서 받아들이는 request가 어떤 객체인지 알아볼 필요가 있다.
class NumberView(View):
def get(self, request):
print(type(request)) # request는 어떤 객체타입일까?
return JsonResponse(
data={
'number': 1
}
)
django 공식문서를 통해 확인하는 것이 가장 좋지만 검색하는 시간을 아끼기 위해 curl을 이용하여 호출해 보면 다음과 같은 결과를 보여준다.
<class 'django.core.handlers.wsgi.WSGIRequest'>
django 내부 모듈의 WSGIRequest 객체를 사용하는 것으로 보인다. 위에서 언급된 Client Class의 get() 메서드 안의 super(). get() 메서드를 타고 들어가면 RequestFactory의 get 메서드로 연결된다.
class RequestFactory:
...
def get(self, path, data=None, secure=False, **extra):
"""Construct a GET request."""
data = {} if data is None else data
return self.generic('GET', path, secure=secure, **{
'QUERY_STRING': urlencode(data, doseq=True),
**extra,
})
이 get 메서드는 다시 generic 메서드를 리턴한다.
class RequestFactory:
...
def generic(self, method, path, data='',='application/octet-stream', secure=False,
**extra):
"""Construct an arbitrary HTTP request."""
...
return self.request(**r)
generice 메서드는 다시 request 메서드를 리턴하는데 이 request 메서드는 WSGIRequest 객체를 리턴한다.
def request(self, **request):
"Construct a generic request object."
return WSGIRequest(self._base_environ(**request))
길었지만 정리하면 다음과 같다.
RequestFactory : get() -> generic() -> request(): WSGIReqeuest()
그래서 Django View를 호출할 수 있나?
결국 Django View에서 받아들이는 request의 타입과 RequestFactory에서 리턴하는 타입이 WSGIRequest로 같다는 것을 알 수 있었다. 이를 이용하면 Django View를 다음과 같이 호출할 수 있다.
def test_directory_view(rf):
rf = RequestFactory()
rf.method = "put"
view = greeting.NumberView()
response = view.put(rf)
assert response.status_code == 200
맺음말
개인적인 견해도 섞여서 작성된 글이라 이렇게 하는 게 옳다는 것이 아닌 단순히 이렇게도 할 수 있었다를 담았다. 코드를 여러 형태로 사용해 보고 자신에게 맞는 스타일을 만들어나가는 것 또한 코딩하는 즐거움이 아닐까 싶다.
'Language > Python' 카테고리의 다른 글
[SQLAlchemy] SQL Compilation Caching (0) | 2023.08.13 |
---|---|
[Python] Selenium Proxy를 이용한 Tor 사용하기 (0) | 2023.07.09 |
setuptools를 활용한 프로젝트 패키징 (1) | 2023.06.18 |
함수에 적용되어있는 decorator를 알아내는 방법 (0) | 2023.05.05 |
urllib을 이용한 학교 메일 가져오기 (0) | 2023.04.02 |