Django 09편 - TDD 2편
포스트
취소

Django 09편 - TDD 2편

들어가기 전…

위 프로젝트에서 다룬 앱을 이용해 TDD를 구성해보자.

뷰 테스트

앞에서 만든 설문조사 어플리케이션은 상당히 대충대충 만들어져 있다. 이 어플리케이션은 pub_date필드가 미래에있는 질문 까지도 포함하여 게시한다. 이것을 개선 해야한다. 미래로 pub_date를 설정한 것은 그 시기가 되면 질문이 게시되지만 그때까지는 보이지 않아야한다.

  • tests.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    
    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.
            """
            create_question(question_text="Past question.", days=-30)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                ['<Question: Past 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.
            """
            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: Past question.>']
            )
    
        def test_two_past_questions(self):
            """
            The questions index page may display multiple questions.
            """
            create_question(question_text="Past question 1.", days=-30)
            create_question(question_text="Past question 2.", days=-5)
            response = self.client.get(reverse('polls:index'))
            self.assertQuerysetEqual(
                response.context['latest_question_list'],
                ['<Question: Past question 2.>', '<Question: Past question 1.>']
            )
    
    • django.urls.reverse() 코드에서 url 템플릿 태그와 유사한 것을 사용해야하는 경우 사용한다.
    • SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix=’’, html=False) Response 인스턴스가 지정된 status*code를 생성하고 해당 텍스트가 응답의 내용에 표시되는지 확인합니다. count가 제공되면 텍스트가 응답에서 정확히 count 번 나타나야합니다. 텍스트를 HTML로 처리하려면 html을 True로 설정하십시오. 응답 내용과의 비교는 문자 별 동등성 대신 HTML 의미 체계를 기반으로합니다. 대부분의 경우 공백은 무시되며 속성 순서는 중요하지 않습니다. 자세한 내용은 _assertHTMLEqual()*을 참조하십시오.
    • TransactionTestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True, msg=None) queryset qs가 특정 값 목록 값을 반환하도록 지정합니다. qs의 내용과 값의 비교는 qs에 변환을 적용하여 수행됩니다. 기본적으로 이것은 qs에있는 각 값의 repr()와 비교됨을 의미합니다. 만약 repr()이 고유하거나 유용한 비교를 제공하지 않는 경우 다른 모든 콜러블을 사용할 수 있습니다. 기본적으로 비교도 순서에 따라 다릅니다. qs가 암시적으로 순서를 제공하지 않는 경우 ordered 매개 변수를 False로 설정하여 비교를 collections.Counter 비교로 전환 할 수 있습니다. 순서가 정의되지 않은 경우 (주어진 q가 순서가 지정되지 않고 둘 이상의 순서가 지정된 값과 비교되는 경우) ValueError가 발생합니다. 오류 발생시 출력은 msg 인수로 사용자 정의 할 수 있습니다.
  • 테스트 결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    FF......
    ======================================================================
    FAIL: test_future_question (apps.polls.tests.QuestionIndexViewTests)
    Questions with a pub_date in the future aren't displayed on
    
    Traceback (most recent call last):
    File "/Users/-/Desktop/github/django-demo/django_demo/apps/polls/tests.py", line 80, in test_future_question
        self.assertContains(response, "No polls are available.")
    File "/opt/miniconda3/envs/django_env/lib/python3.9/site-packages/django/test/testcases.py", line 470, in assertContains
        self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s in response" % text_repr)
    AssertionError: False is not true : Couldn't find 'No polls are available.' in response
    
    ======================================================================
    FAIL: test_future_question_and_past_question (apps.polls.tests.QuestionIndexViewTests)
    Even if both past and future questions exist, only past questions
    
    Traceback (most recent call last):
    File "/Users/-/Desktop/github/django-demo/django_demo/apps/polls/tests.py", line 91, in test_future_question_and_past_question
        self.assertQuerysetEqual(
    File "/opt/miniconda3/envs/django_env/lib/python3.9/site-packages/django/test/testcases.py", line 1052, in assertQuerysetEqual
        return self.assertEqual(list(items), values, msg=msg)
    AssertionError: Lists differ: ['<Question: Future question.>', '<Question: Past question.>'] != ['<Question: Past question.>']
    
    First differing element 0:
    '<Question: Future question.>'
    '<Question: Past question.>'
    
    First list contains 1 additional elements.
    First extra element 1:
    '<Question: Past question.>'
    
    - ['<Question: Future question.>', '<Question: Past question.>']
    + ['<Question: Past question.>']
    
    Ran 8 tests in 0.018s
    
    FAILED (failures=2)
    Destroying test database for alias 'default'...
    

    test_future_question, test_future_question_and_past_question 테스트가 실패했다.

뷰 코드 개선하기

  • views.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    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."""
            # return Question.objects.order_by('-pub_date')[:5]
            return Question.objects.filter(pub_date__lte=timezone.now()).order_by('-pub_date')[:5]
    

    IndexView의 get_queryset()을 위와 같이 수정해준다. Question.objects.filter(pub_date__lte = timezone.now())는 timezone.now()보다 pub_date가 작거나 같은 Question을 포함하는 queryset을 반환한다.

  • 재테스트 결과

    1
    2
    3
    4
    5
    6
    7
    8
    
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    ........
    ----------------------------------------------------------------------
    Ran 8 tests in 0.019s
    
    OK
    Destroying test database for alias 'default'...
    

DetailView 테스트하기

  • tests.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    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)
    
  • 테스트 결과

    ```text Creating test database for alias ‘default’… System check identified no issues (0 silenced). F……… ====================================================================== FAIL: test_future_question (apps.polls.tests.QuestionDetailViewTests) The detail view of a question with a pub_date in the future ———————————————————————- Traceback (most recent call last): File “/Users/-/Desktop/github/django-demo/django_demo/apps/polls/tests.py”, line 118, in test_future_question self.assertEqual(response.status_code, 404) AssertionError: 200 != 404


Ran 10 tests in 0.026s

FAILED (failures=1) Destroying test database for alias ‘default’…

1
2
3
4
5
6
7
8
9
10
11
12
  미래 날짜로 된 detail 페이지에 접근하면 404 not found를 띄워야하는 테스트다. 테스트 결과 의도한 바와 다르게 200 status code가 떴으니 views.py에서 DetailView의 코드를 수정해줘야한다.

- views.py

  ```python
  class DetailView(generic.DetailView):
      model = Question
      template_name = 'polls/detail.html'

      def get_queryset(self):
          return Question.objects.filter(pub_date__lte=timezone.now())

기존 DetailView 코드에서 get_queryset을 위와 같이 오버라이드 해준다.

  • 재테스트 결과

    1
    2
    3
    4
    5
    6
    7
    8
    
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    ..........
    ----------------------------------------------------------------------
    Ran 10 tests in 0.027s
    
    OK
    Destroying test database for alias 'default'...
    

    지금까지 만든 10개의 테스트가 모두 통과되었다. :)

더 많은 테스트를 위한 아이디어

우리는 비슷한 get_queryset 메소드를 ResultsView에 추가하고 그 뷰에 대한 새로운 테스트 클래스를 생성해야합니다. 그것은 우리가 방금 만든 것과 매우 유사합니다.

테스트를 추가하면서 다른 방법으로 애플리케이션을 개선 할 수도 있습니다. 예를 들어, 선택 사항이 없는 사이트에 설문을 게시 할 수 있다는 것은 바보같은 일입니다. 그래서 우리의 뷰는 이를 확인하고 그러한 질문을 배제 할 것입니다. 우리의 테스트는 선택사항이 없는 설문을 생성 한 다음, 실제로 게시되지 않는지 테스트하고, 선택사항이 있는 설문을 작성하고 게시 여부를 테스트합니다.

아마도 일반 사용자가 아닌 로그인 한 관리자는 게시되지 않은 설문을 볼 수 있어야합니다. 다시 말하면: 소프트웨어를 추가하기 위해 필요한 것은 무엇이든 테스트를 수반해야합니다, 먼저 테스트를 작성한 다음 코드가 테스트를 통과하게 만들 것인지, 아니면 먼저 코드에서 로직을 처리 한 다음 이를 증명할 테스트를 작성하십시오.

테스트 코드가 너무 비대해졌다! X(

테스트 할 때는, 많이 할 수록 좋습니다. 비대해지는것은 중요하지 않습니다. 대부분의 경우 테스트를 한 번 작성한 다음 신경을 끄게 됩니다. 그래도 이 테스트 코드의 유용한 기능들은 프로그램을 개발하는 동안 계속 해서 작동할것입니다.

아래와 같이, 테스트들이 현명하게 배열되어있는 한 관리가 어려워지지 않을 것입니다.

  • 각 모델이나 뷰에 대한 별도의 TestClass
  • 테스트하려는 각 조건 집합에 대해 분리된 테스트 방법
  • 기능를 설명하는 테스트 메소드 이름

더 알아보기

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.