본문 바로가기

Django 맛보기

Django 맛보기7 - 테스트

안녕하세요. 상당히 오랜만에 돌아온 개발자 정씨입니다.

그럼 오늘도 여김없이 바로 시작해보도록 하죠

 


1. 테스트

오류검증은 어느 프로그래밍에서든 중요한 부분이라고 생각합니다.

Django에서는 어떤방식으로 오류를 검증할 수 있는지 확인해보도록 하겠습니다.

 

# polls/models.py

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

polls/model.py를 보면 was_published_recently()라는 함수가 존재합니다.

해당 함수는 어제, 오늘 게시된 Question을 확인하는 것을 목적으로 작성된 함수임을 알 수 있습니다.

하지만, 어제 오늘이 아닌 미래의 날짜에 발행되었을 경우에도 True를 반환하게 됩니다. 이는 목적과 부합하지 않습니다.

 

> python manage.py shell

> import datetime
> from django.utils import timezone
> from polls.models import Question
> # create a Question instance with pub_date 30 days in the future
> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
> # was it published recently?
> future_question.was_published_recently()
True

실제로 shell을 이용하여 미래에 Question을 발행하였을때, was_published_recently()는 True를 반환합니다.

 

# polls/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() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

위의 shell 내용을 코드 내재화 한다면, 위와 같이 작성이 가능합니다.

 

> python manage.py test polls

위와 같은 명령어를 통해 테스트를 진행할 수 있습니다.

아마 결과 내용으로는 Error가 반환 될 것 입니다.

 

# polls/models.py

def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

위와 같이 어제와 오늘에만 True값을 반환하도록 was_published_recently() 수정하고,

테스트를 진행하게 되면 이번에는 OK가 반환되는 것을 확인 할 수 있습니다.

 

# polls/tests.py

def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() returns False for questions whose pub_date
    is older than 1 day.
    """
    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() returns True for questions whose pub_date
    is within the last day.
    """
    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)

polls/tests.py를 위와같이 수정하여 보다 정밀한 테스트 또한 가능합니다. 

 


2. 테스트 클라이언트

Django에서는 tests.py를 작성하지 않아도 shell을 통해 테스트 할 수 있는 클라이언트를 제공합니다.

 

> python manage.py shell

> from django.test.utils import setup_test_environment
> setup_test_environment()

> from django.test import Client
> # create an instance of the client for our use
> client = Client()

위와 입력하여 테스트 클라이언트를 실행합니다.

 

> # get a response from '/'
> response = client.get('/')
Not Found: /
> # we should expect a 404 from that address; if you instead see an
> # "Invalid HTTP_HOST header" error and a 400 response, you probably
> # omitted the setup_test_environment() call described earlier.
> response.status_code
404
> # on the other hand we should expect to find something at '/polls/'
> # we'll use 'reverse()' rather than a hardcoded URL
> from django.urls import reverse
> response = client.get(reverse('polls:index'))
> response.status_code
200
> response.content
b'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#x27;s up?</a></li>\n    \n    </ul>\n\n'
> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>

위와 같은 과정들을 통해 테스트를 진행할 수 있습니다.

 


3. 뷰(View) 테스트

이번에는 View에서 미래에 발행된 질문들이 보이지 않게끔 수정해보고 이를 테스트 해보도록 합시다.

 

# polls/views.py

from django.utils import timezone

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 (not including those set to be
        published in the future).
        """
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

polls/views.py를 위와 같이 수정하여 오늘날짜 이전의 Question들만 리스팅 할 수 있도록 수정합니다.

 

# polls/tests.py

from django.urls import reverse

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],
            transform=lambda x: x
        )

    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],
            transform=lambda x: x
        )

    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],
            transform=lambda x: x
        )

* Document와 다르게 "transform=lambda x: x" 코드가 들어간 함수가 있습니다

* "AssertionError: Lists differ: ['<Question: Past question.>'] != [<Question: Past question.>]" 오류가 나는경우 참조하세요 (작은따옴표 차이)

 

그리고 위와 같이 polls/tests.py를 수정하여 다양한 Case를 테스트 해볼 수 있도록 합니다.

해당 함수들은 각각 아래와 같은 경우를 Test하고 있음을 확인 할 수 있습니다. 

  • test_no_questions(): Question이 존재하지 않는 경우
  • test_past_question(): Question이 과거(30일 전)에 발행된 경우 
  • test_future_question(): Question이 미래(30일 후)에 발행된 경우
  • test_future_question_and_past_question(): Question이 미래(30일 후)에 하나, 과거(30일 전)에 하나 발행된 경우
  • test_two_past_questions(): Question이 과거(각각 30일 전, 5일 전)에 2개 발행된 경우

 

Test 기능을 사용하면 보다 면밀하게 프로그래밍을 할 수 있을 것 같다는 생각이듭니다. 참 좋은 기능이네요

조만간 또 돌아오겠습니다. 항상 감사드립니다!