본문으로 바로가기

Django tutorial 끝내기 with mysql - 3

category Project/gist 2019. 6. 26. 15:48

이번 포스팅에서는 form 태그를 사용하여 클라이언트와 소통하는 방법과 

generic.view를 사용하여 적은 코드로도 같은 기능을 작성할 수 있도록 하는 방법에 대해 정리하였다. (part4)

그 다음으로는 자동화된 테스트를 할 수 있는 환경을 만드는 방법에 대해서 정리하였다. (part5)


Form

Django에서 html의 form 태그는 다음과 같이 작성할 수 있다. 기억해야 할건 view.py에서 form 태그의 결과 값으로 전달되는 request의 인자값은 form 태그의 name 값을 Key로 전달된다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--templates/polls/detail.html-->
 
<h1>{{ question.question_text }}</h1>
 
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
 
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>
cs

다음은 form 태그에서 선택한 값이 전달되어지는 polls/vote/question.id의 값이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# polls/views.py의 vote에 대한 처리 부분
 
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        # choice is name tag
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question' : question,
            'error_message' : "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # user hits the back button
        return HttpResponseRedirect(reverse('polls:results', args=(question_id,)))
cs

generic.view

View는 URL에서 전달 된 매개 변수에 따라 데이터베이스에서 데이터를 가져오고, 템플릿을 불러와 값을 렌더링하여 반환하는 기본적인 역할을 한다. Django는 웹에서 많이 쓰는 View의 패턴을 추상화하여  'generic.view' 패키지를 구성해 두었다. 덕분에 개발자는 View를 쉽게 작성할 수 있다.

generic.view를 사용하지 않은 View의 코드이다.

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
# polls/views.py - 변경 전
 
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list' : latest_question_list
    }
    return HttpResponse(template.render(context, request))
 
 
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
 
 
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question':question})
 
 
# polls/urls.py - urlpatterns 변경 전
urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex : /polls/5/results/
    path('<int:question_id>/results', views.results, name='results'),
    # ex : /polls/5/vite/
    path('<int:question_id>/vote/', views.vote, name="vote"),
]
 
 
 
 
cs

아래는 gerneric.view를 사용하여 코드를 작성했을 때의 코드이다.

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
 
# polls/views.py - 변경 
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]
 
 
class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'
 
 
class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'
 
 
# polls/urls.py - urlpatterns 변경 후
urlpatterns = [
    # ex: /polls/
    path('', views.IndexView.as_view(), name='index'),
    # ex: /polls/5/
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    # ex : /polls/5/results/
    path('<int:pk>/results', views.ResultsView.as_view(), name='results'),
    # ex : /polls/5/vite/
    path('<int:question_id>/vote/', views.vote, name="vote"),
    ]
 
 
 
cs

generic.view를 잘 사용하려면 어떤 모델이 적용될 것인지를 알아야 한다.  DetailView는 URL에서 인식하는 값을 primary key 값이라고 생각하고 이를 'pk'라고 부르기 때문에 urls.py에서 question_id를 pk로 변경했다. 

Automated testing

이 튜토리얼에서는 기본적인 자동화 테스트를 볼 수 있다. 쉘을 사용하여 메서드의 동작을 검사하거나 응용 프로그램을 실행하고 데이터를 입력하여 동작 방법을 확인한다. 자동화된 테스트에서 다른 점은 테스트 작업이 시스템에서 수행된다는 점이다. 테스트 세트를 한 번 작성한 다음부터는, 앱이 변경될 때마다 수동으로 테스트를 진행하지 않아도 원래 의도대로 코드가 잘 작동하는지 자동으로 테스트가 가능하다.

테스트를 작성하는 방법은 여러가지 방법이 있으며, 그 가운데 '테스트 중심 개발test-driven development'란 실제로 코드를 작성하기 전에 테스트 코드를 먼저 작성하는 방법이다. 문제를 설명하고 문제를 해결하기 위해서 코드를 작성하는 것이다. 

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
# polls/tests.py
# Question의 was_published_recently()에 대한 테스트 코드
 
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)
 
    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() return 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)
cs

위와 같은 테스트 코드를 작성하고 실행해보자. polls 애플리케이션에서 tests를 찾아서 실행시킨다.

1
manage.py test polls
cs

Django test client

장고에서는 뷰 단계에서 코드와 상호 작용하는 사용자를 시뮬레이트하게 위해 테스트 클라이언트 클래스 Client를 제공한다. 이 test client를 tests.py나 shell에서 모두 사용할 수 있다. 

shell에서 테스트하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Django python shell 불러오기
python manage.py shell
 
# 이하 코드는 shell이 실행된 상태에서 작성한다
from django.test.utils import setup_test_environment
 
# 템플릿 렌더러 설치 (현재 사용중인 DB 위에서 실행된다)
setup_test_environment()
 
# 테스트 클라이언트 클래스를 import한다
from django.test import Client
client = Client()
 
# 클라이언트를 사용해 테스트
response = client.get('/')
response.status_code  # 404
 
# hardcoded URL을 대신해주는 reverse 메소드를 사용하여 응답 체크
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&#39;s up?</a></li>\n\n</ul>\n\n' 
response.context['latest_question_list'# <QuerySet [<Question: What's up?>]>
 
cs

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# polls/tests.py
import datetime
 
from django.test import TestCase
from django.urls import reverse
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)
 
    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() return 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)
 
 
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 id 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):
        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):
        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.>']
        )
 
 
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)
 
cs

Further testing

이 외에도 다양한 방식의 추가 테스팅을 할 수 있다. selenium같은 브라우저 내 Framework를 사용하여 HTML이 브라우저에서 시제로 렌더링되는 방식을 테스트 할 수 있다. 이럴 경우에는 Django 내의 코드 로직 뿐만 아니라, JS도 확인할 수 있다. Django에는 LiveServerTestCase가 포함되어 있어서 Selenium과 같은 도구와 쉽게 통합할 수 있다.