Computer Science/BackEnd

장고 | 장고 공식 문서 내용 정리 #1-6 Getting started : writing your first Django app, part 5

토마토. 2022. 9. 3. 22:20

Writing your first Django app, part 5 | Django documentation | Django (djangoproject.com)

 

Writing your first Django app, part 5 | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

첫 장고 앱 만들기 5

 

자동화된 테스트

테스트는 코드가 잘 작동하는지 확인하는 루틴이다. 

작은 세부 사항을 확인하는 테스트도 있고, 전반적인 작동을 검사하는 테스트도 있다. 

테스트는 시스템이 실행한 다는 점에서 다르다.

 

만약 지금 만든 설문조사 페이지에서 만족할 것이 아니라면, 

지금이 테스트를 배울 좋은 시기이다. 

 

1. 테스트를 통해 시간을 절약할 수 있다. 

2. 테스트를 통해 문제를 확인하는 것 뿐만 아니라 문제를 예방할 수 있다. 

3. 테스트는 코드를 매력적으로 만든다. 

4. 테스트는 팀 작업에 도움이 된다. 

 

기본적인 테스트 방법

테스트에 접근하는 방식에는 여러 가지가 있다. 

  • 유닛 테스트 Unit test
    • 독립적인 class와 function 단위의 테스트
  • regression test 버그 수정 테스트
    • 발생한 버그에 대한 수정 테스트
  • Integration test 통합 테스트
    • 컴포넌트들이 결합하여 수행하는 동작을 검증

 

첫 테스트 만들기

 

  • step 1. 버그 발견하기
>>> 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가 나와서 버그다. 

 

  • step 2. 버그 노출하는 테스트 만들기

이미 존재하는 파일 tests.py에 테스트를 만들어준다. 

 

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

# Create your tests here.
class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

 

터미널에서 python manage.py test polls을 실행해주면, 미리 만들어둔 테스트케이스에 대한 테스트가 실행된다. 

$ 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 "/mnt/c/Users/enkee/OneDrive/바탕 화면/3-2/와플/이전세미나자료/seminar0-assignment/django_document/mysite/polls/tests.py", line 13, 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.016s

FAILED (failures=1)
Destroying test database for alias 'default'...

 

  • step 3. 버그 수정

Question 모델에서 발생한 버그이기 때문에 Question 모델의 내용을 수정해준다. 

# Create your models here.
class Question(models.Model):
    def __str__(self):
        return self.question_text
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

.

$ python manage.py test polls
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
---------------------------------------------------------------------- 
Ran 1 test in 0.003s

OK
Destroying test database for alias 'default'...

이제 정상적으로 작동한다는 걸 알 수 있다. 

 

  • 좀더 포괄적인 테스트
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

# Create your tests here.
class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
    def test_was_published_recently_with_old_question(self):
        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):
        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)

위 코드에서는 was_published_recently라는 매소드의 인자를 구분하여 보다 포괄적인 테스트를 진행하였다. 

 

view 테스트하기

웹 브라우저에서 사용자가 경험하는 동작을 확인하기

  • 장고 테스트 클라이언트
# 기존 데이터베이스에 실행
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

# 테스트 클라이언트 클래스
>>> from django.test import Client
>>> client = Client() # 클라이언트 객체 생성
>>> response = client.get('/') # 이건 정의를 안 해서 404 Not Found가 맞음
Not Found: /
>>> response.status_code
404
# reverse 모듈로 하드 코딩을 피함
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index')))
>>> response.status_code
200
>>> response.content
b'<!-- \xeb\xa7\x8c\xec\x95\xbd view \xed\x95\xa8\xec\x88\x98\xec\x97\x90 \xec\x9d\x98\xed\x95\xb4 latest_question_list \xea\xb0\x80 \xec\xa1\xb4\xec\x9e\xac\xed\x95\x9c\xeb\x8b\xa4\xeb\xa9\xb4 -->\n\n<ul>\n    \n        <li><a href="/polls/1/">what&#x27;s up?\n        </a></li>\n    \n</ul>\n<!-- latest_question_list\xea\xb0\x80 \xec\xa1\xb4\xec\x9e\xac\xed\x95\x98\xec\xa7\x80 \xec\x95\x8a\xec\x9d\x8c-->\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: what's up?>]>

[참고] Testing tools | Django documentation | Django (djangoproject.com)

 

  • view 개선하기
class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.filter(
            pub_date__lte=timezone.now()
        ).order_by('-pub_date')[:5]

 

  • 새로운 view 테스트하기
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
from django.urls import reverse

# Create your tests here.
# 질문 바로 만들기 메소드
def create_question(question_text, days):
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
	# 질문을 만들지는 않음. 설문조사 없음 메시지를 확인하고, response가 비어있는 걸 확인
    def test_no_questions(self):
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        # assertion method 1. assertContains
        self.assertContains(response, "No polls are available.")
        # assertion method 2. assertQuerysetEqual
        self.assertQuerysetEqual(response.context['latest_question_list'],[])
    def test_past_question(self):
        question = create_question(question_text="Past questoin.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question],
        )
    def test_future_question(self):
        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):
        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],
        )

 

  • 테스트 DetailView
class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'
    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

새로 만든 뷰에 맞는 테스트

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        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):
        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)

 

 

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

 

테스트는 치밀할 수록 좋다
  • 모든 모델, 뷰에 대한 독립된 테스트
  • 서로 다른 조건에 대한 독립적인 테스트
  • 테스트하는 내용을 명확히 표현하는 테스트 함수 이름
그 다음은?

ex. 코드 커버리지

테스트 케이스가 얼마나 충족되었는지를 나타내는 지표

개발자 관점의 단위 테스트

- 구문(statement)

- 조건(condition)

- 결정(decision)

을 테스트한다. 

 

코드 커버리지 적용(1) - 커버리지란? - 2WEEKS (tistory.com)