Django tutorial 05

.·2020년 5월 5일
0

Coding

목록 보기
10/33

장고 튜토리얼 파트 5를 시작하지..

1. 테스트를 학습해 보자.

작성한 코드들에 대해서 테스트를 할 수 있는 코드를 작성하는거다.

하나의 기능을 제공하는 코드를 작성하면 결과값이 원하는 값이 나오는지 에 대한 테스트 코드를 작성하는 것이다.

포괄적으로 두가지 의미가 있다.
앞으로 작성할 우리 코드는 수정과 버그가 나오기에 테스트가 필요하다.
많은 사람들과 협업을 하기 때문이다.
문서에 그 이유가 네 가지 경우로 설명이 되어 있다.

  1. 테스트를 통해 시간을 절약 할 수 있습니다.
    : 시간절약, 기능을 명확히하여 앞으로 생길 문제들을 예방하는 거지.

  2. 테스트는 문제를 그저 식별하는 것이 아니라 예방합니다.
    : 코드를 더 매력적으로 만든다라.. 테스트 없는 코드는 설계상 망가진거나 마찬가지다.
    협업.. 내가 작성한 코드를 다른 사람도 수정을 하기 에 더욱이 그렇다.

  3. 테스트가 코드를 더 매력적으로 만듭니다.

  4. 테스트는 팀이 함께 일하는것을 돕습니다.

자 테스트 코드를 작성해 보자.
모델 코드중에 데이터가 최근 데이터이냐 아니냐를 반환하는 함수가 있는데,
생성 날짜가 미래의 생성된 데이터는 최근으로 간주하지 않는다.
미래 날짜는 거짓으로 나와야 하는데.. models.py 를 봐도 미래에 대한 코드 처리는 없는 듯 싶다. shell 에 가서 결과값이 참인지 거짓인지 테스트 해보자.

테스트 해보니 결과 값이 참이 나왔고 테스트 내용을 보게 되면 관련 패키지들을 임포트 하고 현재 시간에 30일을 더한 미래의 생성 날짜를 가진 질문을 하나 생성하게 된다.
모델에 만들어 놨던 최근의 식별함수를 호출 하면 미래 생성 날짜 임에도 불구하고 결과값이 참이 나온 것을 볼 수가 있다. 테스트를 이렇게 해보고 원하는 결과값이 안나올 경우에 대해서 최근 발행일 날짜 함수를 이렇게 다시 수정을 해주는데 우리의 목표는 방금 진행했던 테스트를 shell 에서 수동으로 해보는 것이 아닌 명령어 하나로 확인할 수 있는 테스트 코드를 작성하는 것이다.

코드를 작성하고 내용을 살펴보도록 하자.

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)

테스트 작성시 파일의 이름은 test로 시작이 되어야 하고 tests.py 의 파일 위치는 앱 내부에 파일이 존재해야 하며 코드 작성시 함수 이름의 prefix, 앞머리도 test 로 시작해 주면 된다.
그러면 명령어 실행시 test code 를 찾아서 수행해 준다. 코드 내용을 보면 방금 전 쉘에서 작업했던 내용과 똑같다.
필요한 패키지들을 import 하고 추가적으로 test case 하나를 더 임포트 했다.
테스트 케이스를 작성할 때 방금 임포트 한 테스트 케이스를 우리가 작성할 테스트 클래스에 상속해주면 된다.

time = timezone.now() + datetime.timedelta(days=30)
시간은 현재 시간에 30일을 더 했고 이 30일을 더 한 질문을 생성하였다.
미래의 생성한 질문에 대한 생성일이 최근이냐 라고 호출했을 때 함수 호출값은 False 가 나오기를 기대 되어진다.
결과값이 거짓이 나오는지 확인을 하는 것이다.

자 이제 명령어를 수행해 보자
어떻게?
python manage.py test polls

실행해보니 테스트 실패 했다고 뜬다. 예상했던 결과 값이 거짓이 아닌 함수 호출로 결과값이 참이 나왔기 때문이다.
AssertionError: True is not False
텍스트 코드는 상황을 코드로 만들어주는 것이다. 그리고 원하는 결과값이 나오는지 확인하는 것이다. 테스트를 통과를 하면 제대로 수정이 된 것이겠지?
테스트가 통과될 수 있도록 코드를 수정하도록 하자.

우리는 이미 문제가 무엇인지 알고 있습니다: Question.was_published_recently()는 pub_date가 미래에 있다면 False를 반환해야 합니다. models.py에서 날짜가 과거에 있을 때에만 True를 반환하도록 메소드를 수정하십시오.

polls/models.py

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

question 생성 날짜가 미래로 넘어가지 않도록 현재 날짜를 두고 최근 기준을 하루로 주었다.
다시 테스트를 돌려보자.

코드가 테스트를 통과 했다.

테스트 코드를 작성해 놓으면 테스트 코드를 작성해 놓은 부분에 대해서는 앞으로 문제가 생기지 않을 것이다.
다른 이가 이 부분을 수정 후 값이 잘못 되어도 자동화된 테스트로 확인을 하면 된다.

2. 보다 포괄적인 테스트 파트

우리가 여기있는 동안, 우리는 was_published_recently()메소드를 고정하는것 이상을 할수 있습니다; 사실 하나의 버그를 고치면서 다른 새로운 버그를 만들어 낸다면 분명 곤란할것입니다. 메소드의 동작을 보다 포괄적으로 테스트하기 위해 동일한 클래스에 두 가지 테스트 메소드를 추가하십시오:

문서로 넘어가 보게 되면 하나는 1일이 넘어갈 때에 대한 결과값이 false 가 나와야 되고 하나는 1일이 넘어가지 않았을 때에 대한 true 가 나오도록 검증을 하는 테스트 코드를 작성해 놓았다.

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)

발생할 수 있는 경우에 대하여 최대한 테스트 코드를 작성하는 것이다.
앞서 설명드렸던 테스트 코드를 왜 작성을 해야 되나 두 번째 이유가 이제는 기능을 명확하게 한다는 의미가 와닿겠지..

테스트 코드는 충분할 수록 좋다 코드의 양과 상관없이 최대한 작성을 해주어야 된다.

다음으로는 view 에 대한 테스트이다.
함수에 기능적인 테스트를 진행했다면 view 가 동작을 잘 하는 지에 대한 view test 를 진행해 보도록 하자.
view 파트에서 공부했듯이 view 의 핵심은 request 를 받고 response 를 해주는 것이다.

view test 코드는 사용자가 된 것 처럼 request 즉 요청을 하고 response 를 받아서 결과값을 확인하는 것이다. 사용자가 요청 후 원하는 결과값을 받았는지에 대해서 말이다.

자 다시 shell 에 접속을 해서 현재 사용중인 db 데이터를 이용해서 테스트하기 위해서 setup_test_environment() 라는 명령어를 입력해주고 그전에 임포트도 하고 이 테스트 클라이언트가 테스트 코드에서 사용자 역할을 client = Client() 한다. 우리가 url 을 입력하여 접속하듯이 이 테스트 클라이언트가 요청을 하는 것이다. 클라이언트가 웹 페이지를 호출하는 작업이다.
GET 은 조회를 호출하는 방식이다. POST 는 데이터 생성 또는 수정을 위해서 사용하면 되고, 포스트 호출시 이렇게 GET 대신 POST 를 입력하면 된다.

호출햇더니 NOT found 이렇게 뜨고, 상태코드는 404 로 뜬다.
상태 코드는 status_code 는 서버에서 응답에 대한 상태를 코드로 전달해준다. 상태 코드는 규약 이다. 성공하면 아래와 같이 200을 받는다.
우리가 서버를 구동해서 직접 해당 url 을 들어가보도록 하자.

127.0.0.1:8000 page not found 라고 뜬다.
404 상태 코드를 받았고, 튜토리얼에 나온 코드처럼 동일하게 404가 나온것을 확인할 수가 있다. 방금은 직접 사이트에 들어가서 확인했는데 shell 에서는 테스트 클라이언트가 우리 대신 호출을 한 것을 알 수 있다.
not found /

테스트 클라이언트가 투표 앱에 인덱스를 호출하게 되면 GET 방식으로 호출했고 이제 결과값을 받는다. 리스폰스 내 상태 코드 콘텐츠 컨텍스트 값을 전달받는 것을 볼 수가 있다. 테스트 코드를 통해서 리스폰스 값이 원하는 값이 나오는지 확인하는 것이다. 직접 shell 에서 코드를 작성하고 테스트 클라이언트를 통해서 request 하고 response 를 이제 받아보도록 하자.

shell에 진입해서 입력해 봤더니
튜토리얼에 나온 값대로 동일하게 나오는 것을 확인할 수 있었다.

>>> # 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?>]>

http 상태 코드 확인해 보기 : https://developer.mozilla.org/ko/docs/Web/HTTP/Status

방금 shell 에서 코드를 작성하여 원하는 값이 나오는지 확인을 하는 것이 view 에 대한 테스트 코드 작성이다. 테스트 코드를 작성하기 전에 리스트뷰 수정이 있다.
미래의 날짜가 안 나오도록 필터를 넣어서 조건을 입력한 것이다.

polls/views.py

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]

현재 시간보다 작거나 같은 데이터를 갖고 오도록 하자 라는 의미의,
__lte 는 less than equal 이라고 해서 장고에서 제공되는 필터 조건이다.

큰 값은 디티를 사용하면 되고 인 또는 컨테이너 같은 많은 키워드가 존재한다고 한다.

https://docs.djangoproject.com/en/2.2/ref/models/querysets/#field-lookups

미래의 데이터가 안 나오도록 수정을 하고 view test 코드도 추가하여 작성하자.

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.
        """
        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.>']
        )

작성 완료후 python manage.py test polls 로 확인해보니 테스트를 통과하였음을 확인하였다.

코드를 살펴보면 url 을 하드코딩 하지 않도록 reverse 를 import 하였고 테스트 데이터를 만들기 위해서 함수 하나를 만들었다. def create_question() 함수를 호출하게 되면 데이터 하나가 만들어진다.
데이터는 각 테스트 양식마다 재설정 되므로 필요할 때마다 그때그때 함수를 만들어서 호출해야 한다.

테스트를 위해서 함수를 여러 개 작성했는데 상황만 다르고 패턴은 같다. 상황을 만들어서 테스트 클라이언트가 요청을 하고 리스폰스를 받아서 원하는 값이 나오는지에 대해서 확인을 하는 것이다.

첫번째 테스트 케이스는 def test_no_questions(self):

데이터가 없는 경우인 테스트 이다. 데이터가 없는 경우를 호출하여 상태코드 응담에 포함되어 있는 내용 컨텍스트가 비어 있는 걸 확인하는 것이다. 같은 값을 비교하는 경우에는 equal 을 쓰고 포함되어 있는지는 contains 쿼리셋인 경우에는 querysetequal 이러한 테스트 메소드를 사용해서 테스트를 하면 되고 이러한 패턴들을 익혀 주면 된다.

response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")

다음으로 데이터가 과거인 경우를 확인한다. def test_past_question(self):
과거 데이터를 하나 만들고 호출한 다음에 데이터가 나오는지 확인을 하는 모습이다. 데이터가 나오지 않으면 문제가 있는 것이다.

다음으로 미래 데이터를 만들고 def test_future_question(self):
데이터가 안나오는 경우를 확인하는 거다.
이 경우는 데이터가 나오면 문제가 있는 경우이다.

다음으로 과거데이터 하나, 미래 데이터 하나 각각 입력을 한 뒤에 호출을 하면 과거 데이터만 나온 모습을 기대하는 것.
def test_future_question_and_past_question(self):

과거 데이터 2개를 입력했을 때에 대해서는 두 개의 데이터가 기대 되어 진다.
def test_two_past_questions(self):

아래 하단 detailview 테스트하기도 동일하다.
미래 데이터가 안나오도록 필터를 추가해주고, 방금 했던 인덱스뷰와 동일한 포맷으로 아래쪽에 테스트 케이스를 작성해 주면 된다.

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

질문을 만들고 테스트 클라이언트가 호출하고 결과를 받아서 검증하면 되는 구조.

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
.

0개의 댓글