시작
Part4를 이어서 간단한 설문조사(Polls) 앱을 만드는 과정을 통해 Django의 testing을 알아보려고 합니다. 본 포스트에서는 macOS와 IntelliJ IDEA Ultimate을 사용합니다.
본문
첫 번째 테스트 작성
bug
models.py
파일에서 Question
모델을 확인해 봅니다.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def __str__(self):
return self.question_text
shell을 사용하여 다음과 같이 was_published_recently
메서드를 확인합니다.
python manage.py shell
shell에서 다음 코드를 입력합니다.
import datetime
from django.utils import timezone
from polls.models import Question
future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
future_question.was_published_recently()
# True
pub_date
가 30일 후로 설정되었는데도 was_published_recently
메서드는 True
를 반환합니다. 이는 잘못된 결과입니다.
자동화 테스트 작성
tests.py
파일을 아래와 같이 수정하여 버그를 확인합니다.
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently()은 pub_date가 미래일 경우 False를 반환합니다.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
- 테스트 클래스:
QuestionModelTests
는django.test.TestCase
를 상속받아 작성되어야 합니다. - 테스트 메서드: 테스트 메서드는 반드시
test
로 시작해야 합니다. assertIs
사용:assertIs
메서드는 두 객체가 동일한지 확인할 때 사용됩니다.
테스트를 실행합니다
터미널에서 다음 명령어를 실행합니다.
python manage.py test polls
테스트 실행 후 다음과 같은 내용이 표시됩니다:
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests.test_was_published_recently_with_future_question)
was_published_recently()은 pub_date가 미래일 경우 False를 반환합니다.
----------------------------------------------------------------------
Traceback (most recent call last):
File ".../polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
무슨 일이 발생했는가?
manage.py test polls
명령어는polls
앱의 테스트를 찾습니다.django.test.TestCase
클래스의 하위 클래스를 찾아서 실행합니다.- 테스트 목적으로 특수 데이터베이스를 생성합니다:
Creating test database for alias 'default'...
test
로 시작하는 이름을 가진 테스트 메서드를 찾아 실행합니다.- 각 메서드의 내용을 실행합니다.
assertIs()
메서드를 사용하여 테스트 메서드가 예상한 값을 반환하는지 확인합니다.- 테스트가 실패하면 어떤 테스트가 실패했는지 실패가 발생한 라인까지 알려줍니다:
File "/Users/Albert/Documents/study/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
AssertionError: True is not False
Bug를 해결합니다
models.py
에서 Question
모델의 was_published_recently()
메서드를 수정하여 미래 날짜나 과거 날짜에 대해 올바른 값을 반환하도록 합니다.
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
메서드를 수정한 후 다시 테스트를 실행하면 성공을 확인할 수 있습니다.
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Destroying test database for alias 'default'...
보다 포괄적인 테스트
tests.py
의 QuestionModelTests
클래스에서 두 개의 메서드를 추가하여 다양한 상황을 테스트합니다.
def test_was_published_recently_with_old_question(self):
"""
was_published_recently()은 pub_date가 하루 이상 지난 경우 False를 반환합니다.
""" time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently()은 pub_date가 하루 이내일 경우 True를 반환합니다.
""" time = timezone.now() - datetime.timedelta(hours=23, minutes=59,
seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
이렇게 Question.was_published_recently()
메서드가 과거, 최근 및 미래 질문에 대해 올바른 값을 반환하는지 확인하는 세 가지 테스트를 작성합니다. polls
앱이 얼마나 복잡해지든 상호작용하는 다른 코드가 무엇이든 간에 Question.was_published_recently()
가 예상한 방식으로 작동할 것이라고 어느 정도 보장할 수 있습니다.
뷰 테스트
뷰를 개선한다
polls/views.py
의 IndexView
를 확인합니다.
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""최근 생성된 질문 5개를 반환합니다."""
return Question.objects.order_by("-pub_date")[:5]
get_queryset()
메서드를 수정하여 timezone.now()
와 비교하여 날짜도 확인하도록 변경합니다.
Question.objects.filter(pub_date__lte=timezone.now())
는pub_date
가timezone.now
보다 작거나 같은(즉 이전이거나 같은) 질문을 필터링합니다.
from django.utils import timezone
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""
최근 생성된 질문 5개를 반환합니다.(미래의 질문은 포함하지 않음)
"""
return Question.objects.filter(pub_date__lte=timezone.now()
).order_by("-pub_date")[:5]
뷰를 테스트한다
polls/tests.py
에 테스트를 추가합니다.
def create_question(question_text, days):
"""
question_text와 days에 주어진 값을 사용해 질문을 생성하고 해당 질문을 반환합니다.
days가 음수일 경우 과거의 질문을 생성합니다.
days가 양수일 경우 미래의 질문을 생성합니다.
""" 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):
"""
만약에 질문이 없다면 적절한 메시지가 표시됩니다.
""" 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):
"""
과거 질문은 index 페이지에 표시됩니다.
""" 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):
"""
미래 질문은 index 페이지에 표시되지 않습니다.
""" 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):
"""
과거 질문과 미래 질문이 모두 존재할 경우에도 과거 질문만 표시됩니다.
""" 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):
"""
index 페이지에 여러 질문이 표시됩니다.
""" 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],
)
마무리
이번 Part 5에서는 간단히 Django의 자동 테스트 기능을 작성하고 실행하는 방법을 알아보았습니다. 개인적으로 테스트는 매우 중요하다고 생각하기 때문에 나중에 테스트 관련 포스트를 더 상세히 작성하려고 합니다.
참고자료
'Django' 카테고리의 다른 글
첫 번째 Django 앱 만들기 (Part 7: Customizing the admin site) (0) | 2024.08.23 |
---|---|
첫 번째 Django 앱 만들기 (Part 6: Static files) (0) | 2024.08.22 |
첫 번째 Django 앱 만들기 (Part 4: Forms and generic) (0) | 2024.08.19 |
첫 번째 Django 앱 만들기 (Part 3: Views and templates) (0) | 2024.08.14 |
첫 번째 Django 앱 만들기 (Part 2: Models and the admin site) (0) | 2024.08.10 |
댓글