[Django] 뷰(view) 테스트 자동화하기 - 투표 앱 만들기(Django tutorial)
목차
- IndexView 테스트 자동화
- DetailView 테스트 자동화
- 결론
이번 포스팅에서는 이전에 했던 테스트 자동화를 조금 더 해보겠습니다. 이전 포스팅에서는 모델에 대한 테스트를 해보았습니다. 이번에는 뷰(View)에 대한 테스트를 해보겠습니다. 실제로 많은 동작들은 View에서 동작하기 때문에 좀 더 도움이 될 것입니다. 오늘은 코드 작성량이 많습니다. 평소에는 복사-붙여 넣기보다 손으로 타이핑하라고 많이 말씀드리지만 오늘은... 복사-붙여 넣기로 해도 괜찮습니다.
1. IndexView 테스트 자동화
IndexView 코드 수정
시작하기 전에 이전 코드를 살짝 살펴보겠습니다. polls/views.py
파일을 보겠습니다.
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by("-pub_date")[:5]
위 코드는 우리가 구현해 놓은 IndexView로 이전 실습까지 작성된 상태입니다. 제네릭 뷰를 이용하여 IndexView 클래스를 작성한 상태입니다. 여기서 get_query() 함수를 아래와 같이 변경하여 보겠습니다.
from django.utils import timezone
...
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5]
timezome이라는 Django 모듈을 임포트 하였습니다. 그리고 timezone 모듈을 활용하여 발행날짜가 현재와 같거나 이른 객체들을 필터링하여 가져옵니다. filter 함수의 pub_date__lte가 바로 less than or equal to, 즉, '현재와 같거나 이른'이라는 의미입니다. [:5]의 의미는 가져온 객체 중 상위 5개 까지만 반환한다는 의미입니다.
IndexView 테스트 케이스 작성
이제, 수정한 IndexView에 대한 테스트 코드를 작성해 보겠습니다. 총 5가지의 테스트 케이스를 작성하겠습니다. 먼저 polls/tests.py
에 아래 모듈을 임포트합니다.
from django.urls import reverse
from django.urls import reverse
는 Django에서 URL을 하드코딩하지 않고, URL의 이름(name)을 기반으로 동적으로 URL을 생성할 수 있도록 돕는 함수입니다. URL 패턴이 변경되거나 재구성될 때, URL 이름을 유지하기만 하면 코드 수정 없이 작동합니다.
polls/tests.py
에 아래 코드를 추가하여 테스트 케이스를 추가합니다.
def create_question(question_text, days):
"""
Create a question with the given `question_text` and published the
given number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is displayed.
"""
response = self.client.get(reverse("polls:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertContains(response, "No polls are available.")
self.assertQuerySetEqual(response.context["latest_question_list"], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse("polls:index"))
self.assertQuerySetEqual(
response.context["latest_question_list"],
[question2, question1],
)
IndexView 테스트 케이스 시나리오 설명
각각의 테스트 시나리오에 대해 설명하겠습니다. 좀 길지만, 목적만 이해하신다면 크게 상관없겠습니다. 나중에 여러분의 코드를 작성하실 때 목적에 맞게 로직을 작성하시면 됩니다.
1) test_no_questions
- 목적: 데이터베이스에 질문이 없는 경우를 테스트합니다.
- 로직:
- reverse("polls:index")를 통해 index view에 GET 요청을 보냅니다.
- HTTP 상태 코드가 200인지 확인합니다.
- 페이지에 "No polls are available."라는 메시지가 표시되는지 확인합니다.
- 콘텍스트 변수 latest_question_list가 빈 리스트인지 확인합니다.
2) test_past_question
- 목적: 과거의 질문이 목록에 표시되는지 테스트합니다.
- 로직:
- create_question 함수(정의는 주어지지 않았지만, 테스트용 질문을 생성하는 유틸리티 함수로 보임)를 호출하여 과거의 질문을 생성합니다.
- index view에 GET 요청을 보냅니다.
- 응답의 컨텍스트 변수 latest_question_list에 과거의 질문이 포함되어 있는지 확인합니다.
3) test_future_question
- 목적: 미래의 질문이 목록에 표시되지 않는지 테스트합니다.
- 로직:
- 미래 날짜의 질문을 생성합니다.
- index view에 GET 요청을 보냅니다.
- 페이지에 "No polls are available." 메시지가 표시되는지 확인합니다.
- latest_question_list가 빈 리스트인지 확인합니다.
4) test_future_question_and_past_question
- 목적: 과거와 미래의 질문이 모두 있을 때, 과거의 질문만 표시되는지 테스트합니다.
- 로직:
- 과거 질문과 미래 질문을 생성합니다.
- index view에 GET 요청을 보냅니다.
- 응답의 latest_question_list에 과거 질문만 포함되어 있는지 확인합니다.
5) test_two_past_questions
- 목적: 여러 과거 질문이 있을 때, 올바른 순서(최신 질문 먼저)로 표시되는지 테스트합니다.
- 로직:
- 두 개의 과거 질문을 생성합니다. 하나는 30일 전, 다른 하나는 5일 전입니다.
- index view에 GET 요청을 보냅니다.
- latest_question_list가 최신 질문(5일 전)이 먼저 오도록 올바른 순서를 유지하는지 확인합니다.
2. DetailView 테스트 자동화
DetailView 코드 수정
DetailView도 테스트를 해보겠습니다. 테스트 전에 polls/views.py
를 수정하겠습니다. 뷰에서 DetailView를 아래 코드와 같이 수정합니다.
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
DetailView 테스트 케이스 작성
수정하고 polls/tests.py
에 아래 코드를 추가합니다. DetailView에 대한 2 가지 테스트 케이스입니다. DetailView에서는 IndexView와 달리 실제 Question 모델의 세부적인 내용을 표시하게 됩니다. 따라서 미래의 Question에 대한 객체는 보여줄 수 없기 때문에 404로 처리하도록 하고 테스트를 진행합니다.
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
The detail view of a question with a pub_date in the future
returns a 404 not found.
"""
future_question = create_question(question_text="Future question.", days=5)
url = reverse("polls:detail", args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
The detail view of a question with a pub_date in the past
displays the question's text.
"""
past_question = create_question(question_text="Past Question.", days=-5)
url = reverse("polls:detail", args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
DetailView 테스트 케이스 시나리오 설명
각각의 테스트 시나리오에 대해 설명하겠습니다. 앞서 IndexView의 테스트와 마찬가지로 목적 위주로 보시면 됩니다.
1) test_future_question
- 목적: pub_date가 미래인 질문에 대한 상세 보기가 404 Not Found를 반환하는지 확인.
- 로직:
- create_question 함수를 사용하여 미래의 질문(days=5)을 생성.
- 생성된 질문의 ID를 사용해 URL을 생성.
- HTTP GET 요청으로 URL 접근.
- HTTP 상태 코드가 404인지 확인.
2) test_past_question
- 목적: pub_date가 과거인 질문의 텍스트가 상세 보기 페이지에 제대로 표시되는지 확인.
- 로직:
- create_question 함수를 사용하여 과거의 질문(days=-5)을 생성.
- 생성된 질문의 ID를 사용해 URL을 생성.
- HTTP GET 요청으로 URL 접근.
- 응답 본문에 질문 텍스트가 포함되어 있는지 확인.
결론
이번 포스팅에서는 Django 테스트를 통해 IndexView와 DetailView의 동작을 자동화하여 검증하는 과정을 다루었습니다.
IndexView 테스트
- get_queryset을 수정하여 과거(pub_date <= timezone.now())의 질문만 표시하도록 구현하였습니다.
- 5가지 테스트 케이스를 통해 질문이 없는 경우, 과거 질문만 표시되는 경우, 여러 과거 질문이 올바른 순서로 표시되는 경우 등을 확인했습니다.
DetailView 테스트
- get_queryset을 수정하여 미래(pub_date > timezone.now())의 질문에 접근할 경우 404 Not Found를 반환하도록 구현하였습니다.
- 2가지 테스트 케이스를 통해 과거 질문이 제대로 표시되고, 미래 질문에 접근이 차단되는지 검증했습니다.
- 이처럼 테스트 자동화를 통해 코드가 예상대로 동작하는지 지속적으로 확인할 수 있습니다. 특히, 뷰에서 발생할 수 있는 데이터 처리와 출력 문제를 조기에 발견하여 유지보수성을 높이는 데 매우 유용합니다.
오늘 테스트와 관련된 실습을 하시면서 아마 많은 코드양에 좀 어려움이 있을 수 있었을 것입니다. 평소 제가 늘 강조드리는 것처럼 몸에 힘을 좀 빼고 긴장을 내려놓고 편하게 보시면 되겠습니다. Django 가 여러분께 좀 더 가까이 왔을 뿐입니다.
도움이 되셨다면 공감 부탁드리겠습니다. 여러분의 공감이 정말 큰 힘이 됩니다.
감사합니다!