본문 바로가기
Django

첫 번째 Django 앱 만들기 (Part 5: Testing)

by AlbertIm 2024. 8. 21.

시작

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)

 

  • 테스트 클래스: QuestionModelTestsdjango.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'...

 

무슨 일이 발생했는가?

  1. manage.py test polls 명령어는 polls 앱의 테스트를 찾습니다.
  2. django.test.TestCase 클래스의 하위 클래스를 찾아서 실행합니다.
  3. 테스트 목적으로 특수 데이터베이스를 생성합니다:
    • Creating test database for alias 'default'...
  4. test로 시작하는 이름을 가진 테스트 메서드를 찾아 실행합니다.
  5. 각 메서드의 내용을 실행합니다.
  6. assertIs() 메서드를 사용하여 테스트 메서드가 예상한 값을 반환하는지 확인합니다.
  7. 테스트가 실패하면 어떤 테스트가 실패했는지 실패가 발생한 라인까지 알려줍니다:
    • 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.pyQuestionModelTests 클래스에서 두 개의 메서드를 추가하여 다양한 상황을 테스트합니다.

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.pyIndexView를 확인합니다.

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_datetimezone.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의 자동 테스트 기능을 작성하고 실행하는 방법을 알아보았습니다. 개인적으로 테스트는 매우 중요하다고 생각하기 때문에 나중에 테스트 관련 포스트를 더 상세히 작성하려고 합니다.

참고자료

댓글