본문 바로가기
Django

[Django] DRF - API Testing

by 혀넝 2022. 6. 1.

API Testing

test code를 작성할 때, app을 생성하면 자동으로 만들어지는 test.py를 사용해도 되지만, 프로젝트의 규모가 커지면 test 폴더를 따로 만들어서 관리할 수도 있다. 주의할 점은, 새로운 test 파일이나 폴더를 만들 때 이름의 시작이 test여야 한다는 점이다.

  • ex) test_create_account

Testcase를 실행하면 django는 일시적으로 새로운 db를 생성하고, 그 안에서 모든 testcase를 실행한다.

test code를 실행할 때, python manage.py test라고 하게 되는데, 이건 모든 test file을 호출하게 된다. 특정 app에 있는 test code를 실험하고 싶으면 python manage.py test user_app과 같이 사용하면 된다.

DRF는 django에 있는 test framework를 extend 한 class를 제공한다.

django에서 제공하는 test case와 다른 점은 Client 대신에 APIClient를 사용한다는 것이다. 그 이유로는 APITestCase class를 타고 들어가면 해당 class는 TestCase를 상속하고 있고, 또한 안에서 client_class로 APIClient를 지정하고 있기 때문이다.

class APITestCase(testcases.TestCase):
    client_class = APIClient

...

class APIClient(APIRequestFactory, DjangoClient):
    def __init__(self, enforce_csrf_checks=False, **defaults):
        super().__init__(**defaults)
        self.handler = ForceAuthClientHandler(enforce_csrf_checks)
        self._credentials = {}

    def credentials(self, **kwargs):
        ...

    def force_authenticate(self, user=None, token=None):
        ...

    def request(self, **kwargs):
        ...

    def get(self, path, data=None, follow=False, **extra):
        ...

    def post(self, path, data=None, format=None, content_type=None,
             follow=False, **extra):
       ...

    def put(self, path, data=None, format=None, content_type=None,
            follow=False, **extra):
        ...

    def patch(self, path, data=None, format=None, content_type=None,
              follow=False, **extra):
        ...

    def delete(self, path, data=None, format=None, content_type=None,
               follow=False, **extra):
        ...

    def options(self, path, data=None, format=None, content_type=None,
                follow=False, **extra):
       ...

    def logout(self):
        ...

따라서 APIClient class는 기본 Client class에서 사용하던 request를 제공하고, 거기에는 get, post, put 등이 있다.

 

Registraion TestCase

from django.contrib.auth.models import User
from django.urls                import reverse

from rest_framework                  import status
from rest_framework.test             import APITestCase
from rest_framework.authtoken.models import Token


class RegisterTestCase(APITestCase):
    def test_register(self):
        data = {
            "username":"testcase",
            "email":"testcase@example.com",
            "password":"Password123@",
            "password2":"Password123@"
        }
        
        response = self.client.post(reverse('register'), data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
  • from django.urls import reverse
    • test 파일에서 url을 사용할 때, 해당 파일이 있는 app의 url을 활용하게 된다.
  • response = self.client.post(reverse(’register’), data)
    • 이 안에는 url과 db에 들어갈 데이터가 필요.
    • url은 urls.py에 있는 걸 사용해도 되고, reverse를 사용할 수도 있다. reverse 안에는 url에서 지정한 name의 값을 넣어주면 된다.
    • 따라서 위 내용은, 해당 url로 post 요청을 보내는데, data에 담긴 내용을 가지고 요청을 한다는 것을 의미한다.
  • self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    • post 해서 얻어지는 내용은 response에 담고, 거기에 있는 status code를 활용한다.
    • post가 제대로 되면 201을 얻는데, 그 값과 status.HTTP_201_CREATED와 비교한다.

 

Login Logout TestCase

class LoginLogoutTestCase(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username="example", password="Password123@")

    def test_login(self):
        data = {
            "username":"example",
            "password":"Password123@"
        }
        response = self.client.post(reverse('login'), data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_logout(self):
        self.token = Token.objects.get(user__username="example")
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

        response = self.client.post(reverse('logout'))
        self.assertEqual(response.status_code, status.HTTP_200_OK)
  • login logout을 진행하기 위해서는 우선 user가 필요한데, register test case에서 만든 user는 test 후 destroy 되기 때문에 사용할 수 없다.  따라서 먼저 user를 생성해야 하고 이때 setUp을 사용하면 된다. setup 안에서 create 하고 싶은 내용을 적으면 된다.
  • logout을 하려면 token이 필요하다. user table에 있는 username field에 접근해, 위에서 만든 이름을 가지고 token을 가져온다.
  • # token을 가져온 후에는 credentials method를 사용한다. credentials는 request를 보낼 때 header에 담기는 내용을 설정한다. credentials까지 설정하면 login 과정을 완료한 게 된다.
  • logout을 할 땐 이미 로그인을 해서 data가 들어가 있는거와 마찬가지이기 때문에 따로 data를 설정해서 pass 할 필요가 없다.
  • 중요한 건, self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) 작성 시, ‘Token ‘에 띄어쓰기가 필요하다는 것이다. 띄어쓰기가 들어가지 않으면 에러가 발생한다.
    • postman에서도 Token <token>과 같이 띄어쓰기를 사용해서 type과 credential의 구간을 나눠주기 때문에 똑같이 작성해야 한다.

 

StreamPlatform TestCase

  • streamplatform은 permission class가 IsAdminOrReadOnly로 되어 있어, user를 따로 만들지 않고 post를 진행하면 unauthorized 401 error가 발생한다.
  • logout 부분에서 했던 것처럼 user를 만드는데, 만약 아무런 권한을 부여하지 않은 user를 만들어 test code를 실행시키면, forbidden 403 error가 발생한다.
    • 따라서 일반 user로 test를 하고 ok를 받으려면 status를 403으로 진행하면 된다.
class StreamPlatformTestCase(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='example', password='Password123!')
        self.token = Token.objects.get(user__username=self.user)
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

    def test_streamplatform_create(self):
        data = {
            "name":"test",
            "about":"test 1 platform",
            "website":"http://www.example.com"
        }
        
        response = self.client.post(reverse('streamplatform-list'), data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
		
		def test_streamplatform_list(self):
        response = self.client.get(reverse('streamplatform-list'))
        self.assertEqual(response.status_code, status.HTTP_200_OK)
  • streamplatform의 경우, router를 사용하고 있기 때문에 router에서 설정한 basename을 가져와 reverse에 사용하면 된다.
  • 중요한 건, basename의 경우 streamplatform으로만 지정되어 있는데 여기서는 streamplatform-list로 작성해야 한다는 점이다.
    • 공식문서를 확인해보면, HTTP Method가 post 또는 get인 경우 url name이 {basename}-list 가 되어야 한다고 한다.
  • 또한 이대로 get 요청도 테스트 해보면 통과는 하지만, test_streamplatform_create에서 normal user로 실험했기 때문에 data는 post 되지 않은 상태로, get 요청을 보내도 streamplatform은 현재 비어 있는 상태이다. 그러므로 한 개의 element에 접근할 수 없다.
  • 따라서 setUp() 부분에 element를 한 개를 만들어 detail에 접근할 수 있게 한다.

<수정>

class StreamPlatformTestCase(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='example', password='Password123!')
        self.token = Token.objects.get(user__username=self.user)
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

        self.stream = models.StreamPlatform.objects.create(name="test", about="about test", website="https://www.example.com")


    def test_streamplatform_create(self):
        data = {
            "name":"test",
            "about":"test 1 platform",
            "website":"https://www.example.com"
        }

        response = self.client.post(reverse('streamplatform-list'), data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    
    def test_streamplatform_list(self):
        response = self.client.get(reverse('streamplatform-list'))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_streamplatform_ind(self):
        response = self.client.get(reverse('streamplatform-detail', args=(self.stream.id, )))
        self.assertEqual(response.status_code, status.HTTP_200_OK)
  • 마지막 test_streamplatform_ind를 보면 get을 하기 때문에 data는 필요 없지만, detail page에 접근할 땐 url이 /watch/stream/10/과 같은 형태를 취하기 때문에 위에서 만든 object를 id로 확인해야 한다. 그때 args을 사용하여 id를 가져오면 되고, args는 reverse안에서 보내져야 한다.

 

WatchList TestCase

class WatchListTestCase(APITestCase):
    def setUp(self):
        self.user  = User.objects.create_user(username='example', password='Password123!')
        self.token = Token.objects.get(user__username=self.user)
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

        self.stream = models.StreamPlatform.objects.create(name="test", about="about test", website="https://www.example.com")

        self.watchlist = models.WatchList.objects.create(platform=self.stream, title='example title', storyline='example storyline', active=True)

    def test_watchlist_create(self):
        data = {
            "platform": self.stream,
            "title":"test title",
            "storyline":"test storyline",
            "active":True
        }
        response = self.client.post(reverse('movie-list'), data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    
    def test_watchlist_list(self):
        response = self.client.get(reverse('movie-list'))
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_watchlist_ind(self):
        response = self.client.get(reverse('movie-detail', args=(self.watchlist.id, )))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

		def test_watchlist_update(self):
        data = {
            "platform": self.stream,
            "title":"test title - updated",
            "storyline":"test storyline - updated",
            "active":True
        }
        response = self.client.put(reverse('movie-detail', args=(self.watchlist.id, )))
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
  • 지금까지는 status code를 비교했는데, object를 비교하는 방법도 있다.
    • self.assertEqual(models.WatchList.objects.get().title, 'example title')

 

Review TestCase

  • force_authenticate(): helps to login as any other user, create any other user, and if setting user=None, just login as none
    • 즉 none이면 login 하지 않았음을 의미.
  • 위 코드처럼 self.watchlist를 활용해 test_review_create를 진행하는데, 만약 아래의 코드처럼 setUp 내부에 self.review와 같이 review를 미리 만들고 다시 review create를 하면 testsms fail 한다. (한 user 당 한 watchlist에 한 review만 작성 가능해서 그렇다)
  • 따라서 self.watchlist2와 같이 watchlis를 하나 더 만들어, setUp에서 만드는 review와 test_review_create에서 만드는 review가 바라보는 watchlist를 다르게 하면 된다.
class ReviewTestCase(APITestCase):
    def setUp(self):
        self.user  = User.objects.create_user(username='example', password='Password123!')
        self.token = Token.objects.get(user__username=self.user)
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

        self.stream = models.StreamPlatform.objects.create(name="test", about="about test", website="https://www.example.com")

        self.watchlist = models.WatchList.objects.create(platform=self.stream, title='example title', storyline='example storyline', active=True)

        self.watchlist2 = models.WatchList.objects.create(platform=self.stream, title='example title', storyline='example storyline', active=True)

        self.review = models.Review.objects.create(review_user=self.user, rating=5, description='test desc', watchlist=self.watchlist2, active=True)

    def test_review_create(self):
        data = {
            "review_user":self.user,
            "rating":5,
            "description":"test desc",
            "watchlist":self.watchlist,
            "active":True
        }
        response = self.client.post(reverse('review-create', args=(self.watchlist.id, )), data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    
    def test_review_create_unauthenticated(self):
        data = {
            "review_user":self.user,
            "rating":5,
            "description":"test desc",
            "watchlist":self.watchlist,
            "active":True
        }
        self.client.force_authenticate(user=None)
        response = self.client.post(reverse('review-create', args=(self.watchlist.id, )), data)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    def test_review_update(self):
        data = {
            "review_user":self.user,
            "rating":4,
            "description":"test desc - updated",
            "watchlist":self.watchlist,
            "active":False
        }
        response = self.client.put(reverse('review-detail', args=(self.review.id, )), data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_review_list(self):
        response = self.client.get(reverse('review-list', args=(self.watchlist.id, )))
        self.assertEqual(response.status_code, status.HTTP_200_OK)
    
    def test_review_ind(self):
        response = self.client.get(reverse('review-detail', args=(self.review.id, )))
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_review_user(self):
        response = self.client.get('/watch/reviews/?username' + self.user.username)
        self.assertEqual(response.status_code, status.HTTP_200_OK)