이번 포스팅에서는 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'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과 같은 도구와 쉽게 통합할 수 있다.