본문으로 바로가기
728x90
반응형

개요

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

 

 

맺음말

개인적인 견해도 섞여서 작성된 글이라 이렇게 하는 게 옳다는 것이 아닌 단순히 이렇게도 할 수 있었다를 담았다. 코드를 여러 형태로 사용해 보고 자신에게 맞는 스타일을 만들어나가는 것 또한 코딩하는 즐거움이 아닐까 싶다.

728x90
반응형