[Django] 5. 장고 튜토리얼 part.5

김학재·2021년 12월 23일
0

백엔드

목록 보기
6/8
post-thumbnail

자동화된 테스트

자동화된 테스트란?

코드의 동작을 확인하는 루틴

자동화된 테스트는 테스트 작업이 시스템에서 수행된다. 한번 테스트 세트를 작성한 이후에는 앱을 변경할 때 수동 테스트를 수행하지 않아도 원래 의도대로 코드가 작동하는지 확인할 수 있다.


테스트를 만들어야 하는 이유

  1. 시간 절약 가능
  2. 문제를 그저 식별하는 것이 아니라 예방 가능
  3. 코드를 더 매력적으로 만든다
  4. 팀이 함께 일하는 것을 돕는다

기초 테스팅 전략

TDD : Test Driven Development
어디서부터 테스트를 작성해야 할 지 종잡을 수 없다면 다음에 새로운 기능을 넣거나 버그를 수정하는 등, 코드를 변경할 일이 있을 때 첫 테스트를 작성할 수 있다.


첫번째 테스트 작성하기

버그 식별하기

>>> 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

30일 뒤에 즉, 미래에 만들어졌음에도 was_published_recnetly가 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
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)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hakjaekim/Desktop/VSC/mysite/polls/tests.py", line 14, 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.000s

FAILED (failures=1)
Destroying test database for alias 'default'...
  • manage.py test polls는 polls 애플리케이션에서 테스트를 찾음
  • django.test.Testcase 클래스의 서브 클래스 찾음
  • 테스트 목적으로 특별한 데이터베이스 만듦
  • 테스트 메소드 : 이름이 test로 시작하는 것 찾음
  • test_was_published_recently_with_future_question에서 pub_date 필드가 30일 미래인 Question 인스턴스를 생성
  • assertIs() 메소드를 사용해 우리는 False가 반환되기를 원함에도 was_published_recently() 가 True를 반환한다는 것을 확인

버그 수정

# polls/models.py
def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

보다 포괄적인 테스트

# 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)

뷰 테스트

미래로 pub_date필드를 설정하는 것은 그 시기가 되면 질문이 게시되지만 그때까지는 보이지 않는 것을 의미

이 테스트에서는 웹 브라우저를 통해 사용자가 경험하는대로 동작을 확인.

장고 테스트 클라이언트

Django는 뷰 레벨에서 코드와 상호작용하는 사용자를 시뮬레이트하기 위해 테스트 클라이언트 클래스 Client를 제공한다.

첫번째, shell에서 테스트 환경 구성하기

# response의 추가적인 속성을 사용할 수 있도록 템플릿 렌더러를 설치
from django.test.utils import setup_test_environment
setup_test_environment()

# 테스트 클라이언트 클래스 import
from django.test import Client
client = Client()

response = client.get('/')
response.status_code
>>> 404

from django.urls import reverse
response = client.get(reverse('polls:index'))
response.status_code
>>> 200
response.content
>>> b'<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset="utf-8">\n    <title>This is title</title>\n  </head>\n  <body>\n    \n        <ul>\n        \n        <li>\n          <a href="/polls/1/">\n            What&#39;s up?\n          </a>\n        </li>\n        \n        </ul>\n    \n  </body>\n</html>\n'
response.context['latest_question_list']
>>> <QuerySet [<Question: What's up?>]>

뷰를 개선시키기

# polls/views.py
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]

Question.objects.filter( pub_date__lte=timezone.now() )는 timezone.now보다 pub_date가 작거나 같은 Question을 포함하는 queryset을 반환

새로운 뷰 테스트

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],
        )

    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],
        )
  • create_question : 테스트 과정 중 설문 생성하는 부분에서 반복 사용
  • test_no_questions : 질문을 생성하지는 않지만 No polls are available라는 메시지 및 latest_question_list가 비어있음을 확인
  • test_past_questions : 질문을 생성하고 그 질문이 리스트에 나타나는지 확인
  • test_future_question : 미래의 pub_date로 질문을 만듦

DetailView 테스트하기

URL을 바꾸는 등의 동작을 막기 위해 제약 조건 추가

# polls/views.py
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())

하나는 미래의 pub_date, 다른 하나는 과거의 pub_date

# polls/tests.py
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)

비대해지는 것은 중요하지 않다. 대신 테스트 코드들이 늘어나게 하라.
때로는 테스트를 업데이트해야 하며, 테스팅에서 반복하는 것은 좋은 일이다.

profile
YOU ARE BREATHTAKING

0개의 댓글