จากบทที่แล้วเราไม่สามารถบันทึกค่า input ของผู้ใช้ได้
ที่ lists/templates/home.html ให้เพิ่ม form เข้าไปครอบ input
<h1>Your To-Do list</h1>
<form method="POST">
<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
</form>
<table id="id_list_table">
เมื่อการTestล้มแหลวแบบนึกไม่ถึง มีหลายวิธ๊ที่จะแก้ไขดังนี้
- ให้เพิ่ม print statements เพื่อดูข้อความerrorที่โปรแกรมบอก
- ปรับปรุงข้อผิดพลาดนั้น
- ดูเว็บไซต์ ด้วยตัวเอง
- ใช้คำสั่ง time.sleep เพื่อหยุดการทดสอบระหว่างการทำงาน
functional_tests.py เพิ่ม
[...]
inputbox.send_keys(Keys.ENTER)
import time
time.sleep(10)
table = self.browser.find_element_by_id('id_list_table')
[...]
เกิด Error เนื่องจากการตรวจสอบ CSRF ล้มเหลว
lists/templates/home.html
<form method="POST">
<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
{% csrf_token %}
</form>
การใส่ {% csrf_token %} ลงไปจะเปรียบเสมือนเรากำหนดค่า<input type="hidden"> ทำให้เราสามารถ render หน้าเว็บได้
เนื่องจากเรายังมีคำสั่ง time.sleep อยู่เราจะเห็นว่า web page ที่เราเรียกนั้นไม่มีปัญหาแล้ว
จากนั้นให้ลบคำสั่ง time.sleep ออกได้เลย
Processing a POST Request on the Server
ทดสอบดูว่า home.html จะสามารถบันทึกค่า POST Requestได้หรือไม่ lists/tests.py เพิ่ม code ข้างล่างเข้าไป
from django.template.loader import render_to_string
from django.core.urlresolvers import resolve
from django.test import TestCase
from django.http import HttpRequest
from lists.views import home_page
class HomePageTest(TestCase):
def test_root_url_resolves_to_home_page_view(self):
found = resolve('/')
self.assertEqual(found.func, home_page)
def test_home_page_returns_correct_html(self):
request = HttpRequest()
response = home_page(request)
expected_html = render_to_string('home.html')
self.assertEqual(response.content.decode(), expected_html)
def test_home_page_can_save_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertIn('A new list item', response.content.decode())
ที่ lists/views.py
from django.http import HttpResponse
from django.shortcuts import render
def home_page(request):
if request.method == 'POST':
return HttpResponse(request.POST['item_text'])
return render(request, 'home.html')
Passing Python Variables to Be Rendered in the Template
การแสดงชื่อตัวแปรใน template จะใช้สัญลักษณ์ {{ ... }}
lists/templates/home.html
[...]
<body>
<h1>Your To-Do list</h1>
<form method="POST">
<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
{% csrf_token %}
</form>
<table id="id_list_table">
<tr><td>{{ new_item_text }}</td></tr>
</table>
</body>
[...]
จากนั้นจะใช้ Function render_to_string โดย parameter ตัวแรกคือ template ส่วนตัวที่สองเป็นการ map กันระหว่างชื่อตัวแปรกับค่าของตัวแปร
lists/tests.py เพิ่ม code ส่วนล่างเข้าไป
[...]
self.assertIn('A new list item', response.content.decode())
expected_html = render_to_string(
'home.html',
{'new_item_text': 'A new list item'}
)
self.assertEqual(response.content.decode(), expected_html)
จากนั้นไปแก้ lists/views.py
def home_page(request):
return render(request, 'home.html', {
'new_item_text': request.POST['item_text'],
})
เกิด KeyError ขึ้น แก้ไขโดยใช้คำสั่ง .get() ไปแก้ที่ lists/views.py
def home_page(request):
return render(request, 'home.html', {
'new_item_text': request.POST.get('item_text', ''),
})
ยังไม่มี item ใน table
เราต้องการให้ error แสดงรายละเอียดมากกว่านี้ โดยใช้เทคนิค FT debugging โดยแสดงค่าที่มีอยู่ใน table functional_tests.py
[...]
self.assertTrue(
any(row.text == '1: Buy peacock feathers' for row in rows),
"New to-do item did not appear in table -- its text was:\n%s" % (
table.text,
)
)
[...]
ที่ functional_tests.py ใส่ code ด้านล่างแทน self.assertTrue อันเก่าที่มี 6 บรรทัด
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
เนื่องจากใน functional_tests นั้นเราใส่เลข 1 แต่ใน template ของเรานั้นไม่มีเลข 1
lists/templates/home.html เพิ่มเลข 1: เข้าไป เพราะ FT ต้องการให้เราระบุเลข 1 ที่จุดเริ่มต้นของรายการ
<tr><td>1: {{ new_item_text }}</td></tr>
เมื่อลองรันดูก็จะผ่าน
ที่นี้ลองเพิ่มการส่งค่าให้ template โดยใช้การ copy and paste
functional_tests.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
def test_can_start_a_list_and_retrieve_it_later(self):
# Edith has heard about a cool new online to-do app. She goes
# to check out its homepage
self.browser.get('http://localhost:8000')
# She notices the page title and header mention to-do lists
self.assertIn('To-Do', self.browser.title)
header_text = self.browser.find_element_by_tag_name('h1').text
self.assertIn('To-Do', header_text)
# She is invited to enter a to-do item straight away
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertEqual(
inputbox.get_attribute('placeholder'),
'Enter a to-do item'
)
# She types "Buy peacock feathers" into a text box (Edith's hobby
# is tying fly-fishing lures)
inputbox.send_keys('Buy peacock feathers')
# When she hits enter, the page updates, and now the page lists
# "1: Buy peacock feathers" as an item in a to-do list table
inputbox.send_keys(Keys.ENTER)
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
# There is still a text box inviting her to add another item. She
# enters "Use peacock feathers to make a fly" (Edith is very
# methodical)
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys('Use peacock feathers to make a fly')
inputbox.send_keys(Keys.ENTER)
# The page updates again, and now shows both items on her list
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
self.assertIn(
'2: Use peacock feathers to make a fly' ,
[row.text for row in rows]
)
# Edith wonders whether the site will remember her list. Then she sees
# that the site has generated a unique URL for her -- there is some
# explanatory text to that effect.
self.fail('Finish the test!')
# She visits that URL - her to-do list is still there.
if __name__ == '__main__':
unittest.main(warnings='ignore')
เนื่องจาก temaplate ยังไม่รองรับเลข 2
Three Strikes and Refactor
ไม่ควรใช้วิธีแก้ copy and paste แบบใน FT ควรใช้ function หากอยู่ในไฟล์เดียวกัน หรือใช้ import ในกรณีที่อยู่คนละไฟล์
ทำการ refactor functional_tests.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import unittest
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
def check_for_row_in_list_table(self, row_text):
table = self.browser.find_element_by_id('id_list_table')
rows = table.find_elements_by_tag_name('tr')
self.assertIn(row_text, [row.text for row in rows])
def test_can_start_a_list_and_retrieve_it_later(self):
# Edith has heard about a cool new online to-do app. She goes
# to check out its homepage
self.browser.get('http://localhost:8000')
# She notices the page title and header mention to-do lists
self.assertIn('To-Do', self.browser.title)
header_text = self.browser.find_element_by_tag_name('h1').text
self.assertIn('To-Do', header_text)
# She is invited to enter a to-do item straight away
inputbox = self.browser.find_element_by_id('id_new_item')
self.assertEqual(
inputbox.get_attribute('placeholder'),
'Enter a to-do item'
)
# She types "Buy peacock feathers" into a text box (Edith's hobby
# is tying fly-fishing lures)
inputbox.send_keys('Buy peacock feathers')
# When she hits enter, the page updates, and now the page lists
# "1: Buy peacock feathers" as an item in a to-do list table
inputbox.send_keys(Keys.ENTER)
self.check_for_row_in_list_table('1: Buy peacock feathers')
# There is still a text box inviting her to add another item. She
# enters "Use peacock feathers to make a fly" (Edith is very
# methodical)
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys('Use peacock feathers to make a fly')
inputbox.send_keys(Keys.ENTER)
# The page updates again, and now shows both items on her list
self.check_for_row_in_list_table('1: Buy peacock feathers')
self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
# Edith wonders whether the site will remember her list. Then she sees
# that the site has generated a unique URL for her -- there is some
# explanatory text to that effect.
self.fail('Finish the test!')
# She visits that URL - her to-do list is still there.
if __name__ == '__main__':
unittest.main(warnings='ignore')
error ยังคงเหมือนเดิมคือ template ยังไม่รองรับเลข 2
The Django ORM and Our First Model
สร้าง database ด้วย Django ORM
lists/tests.py
from lists.models import Item
[...]
class ItemModelTest(TestCase):
def test_saving_and_retrieving_items(self):
first_item = Item()
first_item.text = 'The first (ever) list item'
first_item.save()
second_item = Item()
second_item.text = 'Item the second'
second_item.save()
saved_items = Item.objects.all()
self.assertEqual(saved_items.count(), 2)
first_saved_item = saved_items[0]
second_saved_item = saved_items[1]
self.assertEqual(first_saved_item.text, 'The first (ever) list item')
self.assertEqual(second_saved_item.text, 'Item the second')
เนื่องจากเรายังไม่มี item จึงต้องไปสร้าง item ที่เป็น model
lists/models.py
from django.db import models
class Item(object):
pass
ไม่มี attribute save เราจึงสืบทอดคลาสมาจาก Model
lists/models.py
from django.db import models
class Item(models.Model):
pass
Our First Database Migration
เมื่อลองรัน test ดู
เนื่องจากการสร้าง ORM เป็นแค่การ model Database ยังไม่เป็น Database จริงๆ
![]() |
โดยคำสั่ง python3 manage.py makemigrations จะสร้างไฟล์ 0001_initial.py ขึ้นมา เป็น Migration เริ่มต้น
The Test Gets Surprisingly Far
ลองทดสอบดู
เนื่องจาก Django ไม่รู้ว่าเรามี table ที่เก็บ text ของเราไว้เราจึงต้องสร้าง text ใหม่ของเราขึ้นมาเอง
lists/models.py
class Item(models.Model):
text = models.TextField()
A New Field Means a New Migration
เมื่อทดสอบรันดูก็จะเกิด Error ขึ้นว่า
django.db.utils.OperationalError: no such column: lists_item.text
เพราะว่าเราได้ไปเพิ่ม field ใหม่ใน database ของเราซึ่งหมายความว่าเราต้องสร้าง Migration ใหม่สำหรับ field ใหม่ด้วย
ให้เลือก 2 เพื่อเพิ่มค่าเริ่มต้นของ field ไปใน models.py
lists/models.py
class Item(models.Model):
text = models.TextField(default='')

ที่นี้เราก็จะได้ไฟล์ 0002_item_text.py ขึ้นมา
$ git status
$ git diff
$ git add lists
$ git commit -m "Model for list Items and associated migration"
Saving the POST to the Database
ก่อนอื่นเรามาปรับปรุง code ของเราโดยการเพิ่ม 3 บรรทัดใหม่เข้าไป
lists/tests.py
def test_home_page_can_save_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertEqual(Item.objects.count(), 1) #1
new_item = Item.objects.first() #2
self.assertEqual(new_item.text, 'A new list item') #3
self.assertIn('A new list item', response.content.decode())
expected_html = render_to_string(
'home.html',
{'new_item_text': 'A new list item'}
)
self.assertEqual(response.content.decode(), expected_html)
1 เช็คว่า item ถูกบันทึกลง database.objects.count()ซึ่งเป็นตัวย่อของ objects.all().count()
2 objects.first() เป็นเหมือน objects.all()[0].
3 เช็คว่าค่าที่รับมานั้นถูกต้องหรือไม่
เมื่อลองรันดู
lists/views.py.
from django.shortcuts import render
from lists.models import Item
def home_page(request):
item = Item()
item.text = request.POST.get('item_text', '')
item.save()
return render(request, 'home.html', {
'new_item_text': request.POST.get('item_text', ''),
})
หลังจากนั้นทำการปรับปรุงแก้ไข code
lists/views.py.
return render(request, 'home.html', {
'new_item_text': item.text
})
Let’s have a little look at our scratchpad. I’ve added a couple of the other things that are on our mind:
Let’s start with the first one. We could tack on an assertion to an existing test, but it’s best to keep unit tests to testing one thing at a time, so let’s add a new one:
lists/tests.py
class HomePageTest(TestCase):
[...]
def test_home_page_only_saves_items_when_necessary(self):
request = HttpRequest()
home_page(request)
self.assertEqual(Item.objects.count(), 0)
lists/views.py.
def home_page(request):
if request.method == 'POST':
new_item_text = request.POST['item_text'] #1
Item.objects.create(text=new_item_text) #2
else:
new_item_text = '' #3
return render(request, 'home.html', {
'new_item_text': new_item_text, #4
})
1 3 4 ใช้ตัวแปร new_item_text เพื่อเก็บค่า POST หรือ ให้เป็นคำว่าง
2 .objects.create ใช้สร้างรายการใหม่โดยไม่ต้องเรียกใช้ .save()
Redirect After a POST
หลังจากเรารับค่า POST มาแล้ว มันควรจะส่งค่ากลับไปยัง home page แทนที่จะ render ออกมา
lists/tests.py
def test_home_page_can_save_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.first()
self.assertEqual(new_item.text, 'A new list item')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/')
การเปลี่ยนเส้นทางควรมี code 302 และชี้ browser ไปยังที่อยู่ใหม่
lists/views.py
from django.shortcuts import redirect, render
from lists.models import Item
def home_page(request):
if request.method == 'POST':
Item.objects.create(text=request.POST['item_text'])
return redirect('/')
return render(request, 'home.html')
ลองทดสอบรันอีกครั้ง
Better Unit Testing Practice: Each Test Should Test One Thing
จากที่หนังสือได้บอกว่า Unit Test ควรสั้นและแยกเป็นส่วนๆ เพื่อให้ง่ายต่อการอ่านและการแก้ไข bug
lists/tests.py
def test_home_page_can_save_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.first()
self.assertEqual(new_item.text, 'A new list item')
def test_home_page_redirects_after_POST(self):
request = HttpRequest()
request.method = 'POST'
request.POST['item_text'] = 'A new list item'
response = home_page(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/')
เมื่อทดลองรันดู
เราจะทำการ check ว่า template สามารถแสดงรายชื่อ item ได้หรือไม่
lists/tests.py
class HomePageTest(TestCase):
[...]
def test_home_page_displays_all_list_items(self):
Item.objects.create(text='itemey 1')
Item.objects.create(text='itemey 2')
request = HttpRequest()
response = home_page(request)
self.assertIn('itemey 1', response.content.decode())
self.assertIn('itemey 2', response.content.decode())
แก้ template ให้แสดงผลได้หลายๆแถว โดยใช้ คำสั่ง {% for .. in .. %}
lists/templates/home.html
<table id="id_list_table">
{% for item in items %}
<tr><td>1: {{ item.text }}</td></tr>
{% endfor %}
</table>
หลังจากนั้นไปแก้ที่ lists/views.py
def home_page(request):
if request.method == 'POST':
Item.objects.create(text=request.POST['item_text'])
return redirect('/')
items = Item.objects.all()
return render(request, 'home.html', {'items': items})
เมื่อรัน Unit test ก็จะผ่าน แต่รัน FT ไม่ผ่าน
Creating Our Production Database with migrate
เราต้องทำการสร้าง database ของจริงขึ้นมาเพราะ database ที่เราใช้นั้นเป็น database พิเศษ สำหรับใช้ test
superlists/settings.py
[...]
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
We’ve told Django everything it needs to create the database, first via models.py and then when we created the migrations file. To actually apply it to creating a real database, we use another Django Swiss Army knife manage.py command, migrate:
Now we can refresh the page on localhost, see that our error is gone, and try running the functional tests again:[6]
ใกล้เคียงความถูกต้องแล้ว เหลือแค่กำหนดตัวเลขให้ถูกต้อง
lists/templates/home.html
{% for item in items %}
<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
{% endfor %}
ลองทดสอบดูอีกครั้ง
ถ้าต้องการลบและสร้าง database ใหม่ให้ใช้คำสั่ง
$ rm db.sqlite3
$ python3 manage.py migrate --noinput
$ git add lists
$ git commit -m "Redirect after POST, and show all items in template"
































ไม่มีความคิดเห็น:
แสดงความคิดเห็น