link ไปยังงาน Test-3
https://bitbucket.org/tengbs/test3
Building 2
วันศุกร์ที่ 15 พฤษภาคม พ.ศ. 2558
วันพุธที่ 8 เมษายน พ.ศ. 2558
วันพุธที่ 1 เมษายน พ.ศ. 2558
วันอาทิตย์ที่ 22 กุมภาพันธ์ พ.ศ. 2558
Chapter 6. Getting to the Minimum Viable Site
Ensuring Test Isolation in Functional Tests
$ mkdir functional_tests
$ touch functional_tests/__init__.py
สร้าง Folder functional_tests และข้างในมีไฟล์ __init__.py
$ git mv functional_tests.py functional_tests/tests.py
ย้าย functional_tests.py ไปอยู่ใน functional_tests และเปลี่ยนชื่อเป็น test.py
├── db.sqlite3
├── functional_tests
│ ├── __init__.py
│ └── tests.py
├── lists
│ ├── admin.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_item_text.py
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── models.py
│ ├── __pycache__
│ ├── templates
│ │ └── home.html
│ ├── tests.py
│ └── views.py
├── manage.py
└── superlists
├── __init__.py
├── __pycache__
├── settings.py
├── urls.py
└── wsgi.py
functional_tests/tests.py
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
class NewVisitorTest(LiveServerTestCase):
def setUp(self):
[...]
แก้ตรง
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(self.live_server_url)
git status
git add functional_tests
git diff --staged -M
git commit
Running Just the Unit Tests
ตอนนี้ถ้าเรา run manage.py test Django จะ run ทั้ง functional และ unit tests ทั้งคู่เลย
แต่ถ้าอยากรันแค่ unit tests ให้ใช้คำสั่ง python3 manage.py test lists
คำสั่งในการรัน FT จะเปลี่ยนเป็น
python3 manage.py test functional_tests
คำสั่งในการรัน Unit Test จะเปลี่ยนเป็น
python3 manage.py test lists
Small Design When Necessary
เราต้องการสร้าง list ใหม่ เพื่อใส่itemเข้าไป1อย่าง และใช้ url สำหรับ list นี้
functional_tests/tests.py
inputbox.send_keys('Buy peacock feathers')
# When she hits enter, she is taken to a new URL, # and now the page lists "1: Buy peacock feathers" as an item in a # to-do list table inputbox.send_keys(Keys.ENTER) edith_list_url = self.browser.current_url self.assertRegex(edith_list_url, '/lists/.+') #1 self.check_for_row_in_list_table('1: Buy peacock feathers') # There is still a text box inviting her to add another item. She [...]
เมื่อมีผู้ใช้รายใหม่เข้ามาใช้เราต้องตรวจสอบว่า พวกเขาจะไม่เห็นรายการใดๆของ Edith’s items เมื่อเข้ามาแล้วต้องได้รับ url ที่เป็นเอกลักษณ์ของตนเอง
[...]
# The page updates again, and now shows both items on her list
self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
self.check_for_row_in_list_table('1: Buy peacock feathers')
# Now a new user, Francis, comes along to the site.
## We use a new browser session to make sure that no information
## of Edith's is coming through from cookies etc #1
self.browser.quit()
self.browser = webdriver.Firefox()
# Francis visits the home page. There is no sign of Edith's
# list
self.browser.get(self.live_server_url)
page_text = self.browser.find_element_by_tag_name('body').text
self.assertNotIn('Buy peacock feathers', page_text)
self.assertNotIn('make a fly', page_text)
# Francis starts a new list by entering a new item. He
# is less interesting than Edith...
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys('Buy milk')
inputbox.send_keys(Keys.ENTER)
# Francis gets his own unique URL
francis_list_url = self.browser.current_url
self.assertRegex(francis_list_url, '/lists/.+')
self.assertNotEqual(francis_list_url, edith_list_url)
# Again, there is no trace of Edith's list
page_text = self.browser.find_element_by_tag_name('body').text
self.assertNotIn('Buy peacock feathers', page_text)
self.assertIn('Buy milk', page_text)
# Satisfied, they both go back to sleep
# The page updates again, and now shows both items on her list
self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
self.check_for_row_in_list_table('1: Buy peacock feathers')
# Now a new user, Francis, comes along to the site.
## We use a new browser session to make sure that no information
## of Edith's is coming through from cookies etc #1
self.browser.quit()
self.browser = webdriver.Firefox()
# Francis visits the home page. There is no sign of Edith's
# list
self.browser.get(self.live_server_url)
page_text = self.browser.find_element_by_tag_name('body').text
self.assertNotIn('Buy peacock feathers', page_text)
self.assertNotIn('make a fly', page_text)
# Francis starts a new list by entering a new item. He
# is less interesting than Edith...
inputbox = self.browser.find_element_by_id('id_new_item')
inputbox.send_keys('Buy milk')
inputbox.send_keys(Keys.ENTER)
# Francis gets his own unique URL
francis_list_url = self.browser.current_url
self.assertRegex(francis_list_url, '/lists/.+')
self.assertNotEqual(francis_list_url, edith_list_url)
# Again, there is no trace of Edith's list
page_text = self.browser.find_element_by_tag_name('body').text
self.assertNotIn('Buy peacock feathers', page_text)
self.assertIn('Buy milk', page_text)
# Satisfied, they both go back to sleep
Iterating Towards the New Design
สร้าง test ให้ redirect location หลังจากได้รับ POST
lists/tests.py
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
และแก้ใน lists/views.py
def home_page(request):
if request.method == 'POST':
Item.objects.create(text=request.POST['item_text'])
return redirect('/lists/the-only-list-in-the-world/')
items = Item.objects.all()
return render(request, 'home.html', {'items': items})
จะ error ว่า เนื่องจากยังไม่มี list item ชื่อว่า the-only-list-in-the-world
Testing Views, Templates, and URLs Together with the Django Test Client
ในบทนี้เราจะใช้ unit tests เช็คความละเอียดของ URL และเช็ค views render templates ถูกต้องหรือไม่
A New Test Class
ไปที่ lists/tests.py เพิ่มคลาสใหม่ ListViewTest หลังจากนั้น copy method ที่ชื่อว่า test_home_page_displays_all_ list_items จาก HomePageTest มาใส่ไว้ในคลาสใหม่แทนlists/tests.py
class ListViewTest(TestCase):
def test_displays_all_items(self):
Item.objects.create(text='itemey 1')
Item.objects.create(text='itemey 2')
response = self.client.get('/lists/the-only-list-in-the-world/') #1
self.assertContains(response, 'itemey 1') #2
self.assertContains(response, 'itemey 2') #3
A New URL
URL ของเรายังไม่ได้สร้างจึงไปแก้ไขใน superlists/urls.py
superlists/urls.py
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/the-only-list-in-the-world/$', 'lists.views.view_list',
name='view_list'
),
# url(r'^admin/', include(admin.site.urls)),
)
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/the-only-list-in-the-world/$', 'lists.views.view_list',
name='view_list'
),
# url(r'^admin/', include(admin.site.urls)),
)
A New View Function
lists/views.py
def view_list(request):
pass
def view_list(request):
pass
lists/views.py
def view_list(request):
items = Item.objects.all()
return render(request, 'home.html', {'items': items})
def view_list(request):
items = Item.objects.all()
return render(request, 'home.html', {'items': items})
Green? Refactor
เราสามารถลบ test_home_page_displays_all_list_items method เพราะมันไม่จำเป็น
test_home_page_displays_all_list_items method เพราะมันไม่จำเป็นA Separate Template for Viewing Lists
lists/tests.py
class ListViewTest(TestCase):def test_uses_list_template(self):
response = self.client.get('/lists/the-only-list-in-the-world/')
self.assertTemplateUsed(response, 'list.html')
def test_displays_all_items(self):
[...]
lists/views.py
def view_list(request):
items = Item.objects.all()
return render(request, 'list.html', {'items': items})
สร้างไฟล์ใหม่ที่ lists/templates/list.html ใช้คำสั่ง $ touch lists/templates/list.html
$ cp lists/templates/home.html lists/templates/list.html
home pageไม่จำเป็นต้องแสดงlist items มันเพียงต้องการ list inputใหม่ เพื่อให้เราสามารถลบบรรทัดจาก lists/templates/home.html และอาจปรับแต่งh1 ที่จะบอกว่า "Start a new To-Do list"
lists/templates/home.html
<body>
<h1>Start a new To-Do list</h1>
<form method="POST">
<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
{% csrf_token %}
</form>
</body>
lists/views.py
def home_page(request):
if request.method == 'POST':
Item.objects.create(text=request.POST['item_text'])
return redirect('/lists/the-only-list-in-the-world/')
return render(request, 'home.html')
การรับ input item ตัวที่สองยังผิดอยู่ เพราะว่ารูปแบบใหม่ action= attribute หายไป เลยเป็นค่า default หมายความว่ามันจะส่งไปยัง url เดียวกัน
lists/templates/list.html
<form method="POST" action="/">
$ git status
$ git add lists/templates/list.html
$ git diff
$ git commit -a
Another URL and View for Adding List Items
เปิดไฟล์ lists/tests.py ให้นำ methods test_home_page_can_save_a_POST_request และ test_home_page_redirects_after_POST ไปใส่ในคลาสใหม่และเปลี่ยนชื่อตามข้างล่างA Test Class for New List Creation
lists/tests.py
class NewListTest(TestCase):
def test_saving_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
[...]
def test_redirects_after_POST(self):
[...]
class NewListTest(TestCase):
def test_saving_a_POST_request(self):
request = HttpRequest()
request.method = 'POST'
[...]
def test_redirects_after_POST(self):
[...]
แล้วเปลี่ยนcode ตามหนังสือ
class NewListTest(TestCase):
def test_saving_a_POST_request(self):
self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.first()
self.assertEqual(new_item.text, 'A new list item')
def test_redirects_after_POST(self):
response = self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
A URL and View for New List Creation
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/the-only-list-in-the-world/$', 'lists.views.view_list',
name='view_list'
),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/the-only-list-in-the-world/$', 'lists.views.view_list',
name='view_list'
),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
lists/views.py
def new_list(request):
pass
Then we get "The view lists.views.new_list didn’t return an HttpResponse object". (This is getting rather familiar!) We could return a raw HttpResponse, but since we know we’ll need a redirect, let’s borrow a line from home_page:
lists/views.py
def new_list(request):
return redirect('/lists/the-only-list-in-the-world/')
def new_list(request):
pass
Then we get "The view lists.views.new_list didn’t return an HttpResponse object". (This is getting rather familiar!) We could return a raw HttpResponse, but since we know we’ll need a redirect, let’s borrow a line from home_page:
lists/views.py
def new_list(request):
return redirect('/lists/the-only-list-in-the-world/')
lists/views.py
def new_list(request):
Item.objects.create(text=request.POST['item_text'])
return redirect('/lists/the-only-list-in-the-world/')
lists/tests.py
def test_redirects_after_POST(self):
response = self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
self.assertRedirects(response, '/lists/the-only-list-in-the-world/')
def test_redirects_after_POST(self):
response = self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
self.assertRedirects(response, '/lists/the-only-list-in-the-world/')
Removing Now-Redundant Code and Tests
viewของเราตอนนี้ส่วนใหญ่ทำงานที่ home_page เราควรจะที่จะลดความซับซ้อนของมัน ลองเอาส่วนของ if request.method == 'POST' ออกแล้วดูการเปลี่ยนแปลง
lists/views.py
def home_page(request):
return render(request, 'home.html')
เมื่อเราเอาการทดสอบ test_home_page_only_saves_ items_when_necessary ออกไปแล้วลองรัน UT
ผลคือ OK
Pointing Our Forms at the New URL
เรามี 2 urlที่จะต้องแก้ไขใน home.html และ lists.htmllists/templates/home.html, lists/templates/list.html.
<form method="POST" action="/lists/new">
Adjusting Our Models
lists/tests.py เพิ่มลด ตาม code ด้านล่าง
[...]
from django.template.loader import render_to_string
from django.test import TestCase
-from lists.models import Item
+from lists.models import Item, List
from lists.views import home_page
class HomePageTest(TestCase):
[...]
-class ItemModelTest(TestCase):
+class ListAndItemModelsTest(TestCase):
def test_saving_and_retrieving_items(self):
+ list_ = List()
+ list_.save()
+
first_item = Item()
first_item.text = 'The first (ever) list item'
+ first_item.list = list_
first_item.save()
second_item = Item()
second_item.text = 'Item the second'
+ second_item.list = list_
second_item.save()
+ saved_list = List.objects.first()
+ self.assertEqual(saved_list, list_)
+
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(first_saved_item.list, list_)
self.assertEqual(second_saved_item.text, 'Item the second')
+ self.assertEqual(second_saved_item.list, list_)
from django.template.loader import render_to_string
from django.test import TestCase
-from lists.models import Item
+from lists.models import Item, List
from lists.views import home_page
class HomePageTest(TestCase):
[...]
-class ItemModelTest(TestCase):
+class ListAndItemModelsTest(TestCase):
def test_saving_and_retrieving_items(self):
+ list_ = List()
+ list_.save()
+
first_item = Item()
first_item.text = 'The first (ever) list item'
+ first_item.list = list_
first_item.save()
second_item = Item()
second_item.text = 'Item the second'
+ second_item.list = list_
second_item.save()
+ saved_list = List.objects.first()
+ self.assertEqual(saved_list, list_)
+
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(first_saved_item.list, list_)
self.assertEqual(second_saved_item.text, 'Item the second')
+ self.assertEqual(second_saved_item.list, list_)
เมื่อเราไปเพิ่ม lists/models.py
class List(object):
pass
pass
หลังจากนั้นแก้เป็น
class List(models.Model):
pass
pass
A Foreign Key Relationship
lists/models.py
from django.db import models
class List(models.Model):
pass
class Item(models.Model):
text = models.TextField(default='')
list = models.TextField(default='')
from django.db import models
class List(models.Model):
pass
class Item(models.Model):
text = models.TextField(default='')
list = models.TextField(default='')
lists/models.py
from django.db import models
class List(models.Model):
pass
class Item(models.Model):
text = models.TextField(default='')
list = models.ForeignKey(List, default=None)
from django.db import models
class List(models.Model):
pass
class Item(models.Model):
text = models.TextField(default='')
list = models.ForeignKey(List, default=None)
Adjusting the Rest of the World to Our New Models
เมื่อลองรัน
$ python3 manage.py test lists
[...]
ERROR: test_displays_all_items (lists.tests.ListViewTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_saving_a_POST_request (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
Ran 7 tests in 0.021s
FAILED (errors=3)
[...]
ERROR: test_displays_all_items (lists.tests.ListViewTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_saving_a_POST_request (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
Ran 7 tests in 0.021s
FAILED (errors=3)
แก้ไฟล์ lists/tests.py
class ListViewTest(TestCase):
def test_displays_all_items(self):
list_ = List.objects.create()
Item.objects.create(text='itemey 1', list=list_)
Item.objects.create(text='itemey 2', list=list_)
class ListViewTest(TestCase):
def test_displays_all_items(self):
list_ = List.objects.create()
Item.objects.create(text='itemey 1', list=list_)
Item.objects.create(text='itemey 2', list=list_)
ที่lists/views.py
from lists.models import Item, List
[...]
def new_list(request):
list_ = List.objects.create()
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect('/lists/the-only-list-in-the-world/')
from lists.models import Item, List
[...]
def new_list(request):
list_ = List.objects.create()
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect('/lists/the-only-list-in-the-world/')
$ git status # 3 changed files, plus 2 migrations
$ git add lists
$ git diff --staged
$ git commit
$ git add lists
$ git diff --staged
$ git commit
Each List Should Have Its Own URL
lists/tests.py
class ListViewTest(TestCase):
def test_uses_list_template(self):
list_ = List.objects.create()
response = self.client.get('/lists/%d/' % (list_.id,))
self.assertTemplateUsed(response, 'list.html')
def test_displays_only_items_for_that_list(self):
correct_list = List.objects.create()
Item.objects.create(text='itemey 1', list=correct_list)
Item.objects.create(text='itemey 2', list=correct_list)
other_list = List.objects.create()
Item.objects.create(text='other list item 1', list=other_list)
Item.objects.create(text='other list item 2', list=other_list)
response = self.client.get('/lists/%d/' % (correct_list.id,))
self.assertContains(response, 'itemey 1')
self.assertContains(response, 'itemey 2')
self.assertNotContains(response, 'other list item 1')
self.assertNotContains(response, 'other list item 2')
Capturing Parameters from URLs
superlists/urls.py
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/(.+)/$', 'lists.views.view_list', name='view_list'),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/(.+)/$', 'lists.views.view_list', name='view_list'),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
เมื่อลองรัน UT จะพบว่า
ERROR: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
ERROR: test_uses_list_template (lists.tests.ListViewTest)
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
[...]
TypeError: view_list() takes 1 positional argument but 2 were given
ERROR: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
ERROR: test_uses_list_template (lists.tests.ListViewTest)
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
[...]
TypeError: view_list() takes 1 positional argument but 2 were given
แก้ไข parameterใน lists/views.py
def view_list(request, list_id):
[...]
def view_list(request, list_id):
[...]
lists/views.py
def view_list(request, list_id):
list_ = List.objects.get(id=list_id)
items = Item.objects.filter(list=list_)
return render(request, 'list.html', {'items': items})
def view_list(request, list_id):
list_ = List.objects.get(id=list_id)
items = Item.objects.filter(list=list_)
return render(request, 'list.html', {'items': items})
Adjusting new_list to the New World
เมื่อลองรัน UT
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
ValueError: invalid literal for int() with base 10:'the-only-list-in-the-world'
ValueError: invalid literal for int() with base 10:'the-only-list-in-the-world'
lists/tests.py แก้ code ใน function ใหม่เป็น
class NewListTest(TestCase):
[...]
def test_redirects_after_POST(self):
response = self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
new_list = List.objects.first()
self.assertRedirects(response, '/lists/%d/' % (new_list.id,))
class NewListTest(TestCase):
[...]
def test_redirects_after_POST(self):
response = self.client.post(
'/lists/new',
data={'item_text': 'A new list item'}
)
new_list = List.objects.first()
self.assertRedirects(response, '/lists/%d/' % (new_list.id,))
lists/views.py
def new_list(request):
list_ = List.objects.create()
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect('/lists/%d/' % (list_.id,))
def new_list(request):
list_ = List.objects.create()
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect('/lists/%d/' % (list_.id,))
One More View to Handle Adding Items to an Existing List
lists/tests.py
class NewItemTest(TestCase):
def test_can_save_a_POST_request_to_an_existing_list(self):
other_list = List.objects.create()
correct_list = List.objects.create()
self.client.post(
'/lists/%d/add_item' % (correct_list.id,),
data={'item_text': 'A new item for an existing list'}
)
self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.first()
self.assertEqual(new_item.text, 'A new item for an existing list')
self.assertEqual(new_item.list, correct_list)
def test_redirects_to_list_view(self):
other_list = List.objects.create()
correct_list = List.objects.create()
response = self.client.post(
'/lists/%d/add_item' % (correct_list.id,),
data={'item_text': 'A new item for an existing list'}
)
self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))
class NewItemTest(TestCase):
def test_can_save_a_POST_request_to_an_existing_list(self):
other_list = List.objects.create()
correct_list = List.objects.create()
self.client.post(
'/lists/%d/add_item' % (correct_list.id,),
data={'item_text': 'A new item for an existing list'}
)
self.assertEqual(Item.objects.count(), 1)
new_item = Item.objects.first()
self.assertEqual(new_item.text, 'A new item for an existing list')
self.assertEqual(new_item.list, correct_list)
def test_redirects_to_list_view(self):
other_list = List.objects.create()
correct_list = List.objects.create()
response = self.client.post(
'/lists/%d/add_item' % (correct_list.id,),
data={'item_text': 'A new item for an existing list'}
)
self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))
Beware of Greedy Regular Expressions!
superlists/urls.py
url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
The Last New URL
superlists/urls.py
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
url(r'^lists/(\d+)/add_item$', 'lists.views.add_item', name='add_item'),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
url(r'^lists/(\d+)/add_item$', 'lists.views.add_item', name='add_item'),
url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
# url(r'^admin/', include(admin.site.urls)),
)
The Last New View
lists/views.py
def add_item(request):
pass
def add_item(request):
pass
รัน UT
TypeError: add_item() takes 1 positional argument but 2 were given
lists/views.py
def add_item(request, list_id):
pass
def add_item(request, list_id):
pass
รัน UT
ValueError: The view lists.views.add_item didn't return an HttpResponse object.
It returned None instead.
lists/views.py
def add_item(request, list_id):
list_ = List.objects.get(id=list_id)
return redirect('/lists/%d/' % (list_.id,))
รัน UT
self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1
lists/views.py
def add_item(request, list_id):
list_ = List.objects.get(id=list_id)
Item.objects.create(text=request.POST['item_text'], list=list_)
return redirect('/lists/%d/' % (list_.id,))
รัน UT
Ran 9 tests in 0.022s
OK
But How to Use That URL in the Form?
lists/templates/list.html
<form method="POST" action="/lists/{{ list.id }}/add_item">
For that to work, the view will have to pass the list to the template. Let’s create a new unit test in ListViewTest:
lists/tests.py
def test_passes_correct_list_to_template(self):
other_list = List.objects.create()
correct_list = List.objects.create()
response = self.client.get('/lists/%d/' % (correct_list.id,))
self.assertEqual(response.context['list'], correct_list)
lists/views.py
def view_list(request, list_id):
list_ = List.objects.get(id=list_id)
return render(request, 'list.html', {'list': list_})
lists/templates/list.html
<form method="POST" action="/lists/{{ list.id }}/add_item">
[...]
{% for item in list.item_set.all %}
<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
{% endfor %}
A Final Refactor Using URL includes
$ cp superlists/urls.py lists/
superlists/urls.py
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/', include('lists.urls')),
# url(r'^admin/', include(admin.site.urls)),
)
urlpatterns = patterns('',
url(r'^$', 'lists.views.home_page', name='home'),
url(r'^lists/', include('lists.urls')),
# url(r'^admin/', include(admin.site.urls)),
)
lists/urls.py
from django.conf.urls import patterns, url
urlpatterns = patterns('',
url(r'^(\d+)/$', 'lists.views.view_list', name='view_list'),
url(r'^(\d+)/add_item$', 'lists.views.add_item', name='add_item'),
url(r'^new$', 'lists.views.new_list', name='new_list'),
)
วันพุธที่ 4 กุมภาพันธ์ พ.ศ. 2558
Chapter 5. Saving User Input
Wiring Up Our Form to Send a POST Request
จากบทที่แล้วเราไม่สามารถบันทึกค่า 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())
เราต้องไปสร้าง code path สำหรับ POST Request
ที่ 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
ทำการ refactor functional_tests.py
lists/tests.py
from django.db import models
from django.db import models
lists/models.py

ที่นี้เราก็จะได้ไฟล์ 0002_item_text.py ขึ้นมา
$ git status
ก่อนอื่นเรามาปรับปรุง code ของเราโดยการเพิ่ม 3 บรรทัดใหม่เข้าไป
1 เช็คว่า item ถูกบันทึกลง database.objects.count()ซึ่งเป็นตัวย่อของ objects.all().count()
หลังจากนั้นทำการปรับปรุงแก้ไข code
Redirect After a POST
หลังจากเรารับค่า POST มาแล้ว มันควรจะส่งค่ากลับไปยัง home page แทนที่จะ render ออกมา
Better Unit Testing Practice: Each Test Should Test One Thing
จากบทที่แล้วเราไม่สามารถบันทึกค่า 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"
สมัครสมาชิก:
บทความ (Atom)











































































