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)
'Django' 카테고리의 다른 글
[Django] DRF - 1차 프로젝트 리팩토링 (2) Category detail & list GET (0) | 2022.06.07 |
---|---|
[Django] DRF - 1차 프로젝트 리팩토링 (1) Product detail & list GET (0) | 2022.06.07 |
[Django] DRF 정리 - instance, validated_data, @api_view, reuiqred, ModelSerializer, Nested Serializer (4) | 2022.05.29 |
[Django] DRF - Token Authentication (0) | 2022.05.29 |
[Django] DRF - Permission (0) | 2022.05.28 |