Bitbucket : https://bitbucket.org/chananan/test3/commits/all
ใน commit มีของtest2 เนื่องจาก นำงานของ test2 มาแก้ต่อ ทำให้มี commitของtest2 มาอยู๋ในงานนี้ด้วย
วันศุกร์ที่ 15 พฤษภาคม พ.ศ. 2558
วันพฤหัสบดีที่ 7 พฤษภาคม พ.ศ. 2558
Assignment-2
Bitbucket : https://bitbucket.org/panlapa_khemarungsarit/assingment-2
Pythonanywhere : http://galiblenight.pythonanywhere.com/
blog ของสมาชิก
1.bss56-20081.blogspot.com
2.bss56-30132.blogspot.com
comment Assignment-2 ของกลุ่มเพื่อน
กลุ่มที่1 https://bitbucket.org/cprekmutnbb2/assignment_ii/overview
-ยังมีะกดผิดบ้างบางคำเช่น เช็ค -> เชค
-use case เพิ่มภาพยนตร์และใส่รายละเอียดของข้อมูล มีการอธิบายรวมมากเกินไป ควรมีการแยกออกมาเป็นข้อๆ
กลุ่มที่2 https://bitbucket.org/assignment2_WTY/assignment2
-ไม่มีครับ
-ยังมีะกดผิดบ้างบางคำเช่น เช็ค -> เชค
-use case เพิ่มภาพยนตร์และใส่รายละเอียดของข้อมูล มีการอธิบายรวมมากเกินไป ควรมีการแยกออกมาเป็นข้อๆ
กลุ่มที่2 https://bitbucket.org/assignment2_WTY/assignment2
-ไม่มีครับ
วันพฤหัสบดีที่ 9 เมษายน พ.ศ. 2558
ตรวจสอบรายชื่อ และขนาดไฟล์
การหาขนาดของไฟล์ ใน Directories ที่ต้องการ
ทำการเปิด Server แบบ CGI โดยใช้คำสั่ง python -m CGIHTTPServer
#!/usr/bin/python
print "Content-type: text/html\n\n"
import os
path = "/home/chananan/cgi-bin"
print "Path : %s <br>" % path
list_file = os.listdir(path)
for file in list_file:
print "-"
size = os.path.getsize(path+"/"+file)
print file+": size ="
print str(size)+"bytes<br>"
แสดงผล :
code บรรทัดที่ 1 เนื่องจาก ทำการเปิด Server แบบ CGI จึงกำหนด ไฟล์ให้เป็น python
code บรรทัดที่ 2 เป็นการกำหนดให้เป็น HTML
code บรรทัดที่ 3 เป็นการนำเข้าฟังก์ชัน
code บรรทัดที่ 5 กำหนด path ที่เราต้องการจะดู
code บรรทัดที่ 8-9 เป็นคำสั่งในการคนหารายชื่อไฟล์ ใน path
code บรรทัดที่ 11 เป็นคำสั่งหาขนาดของไฟล์ข้อมูล
code บรรทัดที่ 12-13 ให้แสดงชื่อไฟล์ + ขนาด
*คำสั่งเรียกดูขนาดของไฟล์ ไม่สามารถดูขนาดไฟล์ของ Folder ได้ ซึ่งจะแสดงเป็น 4096
Test2
Web application
โปรแกรมคำนวนเงินออม โดยใส่จำนวนเงินเข้าไป และจะมีการแสดง ผลรวม ค่าเฉลี่ย ค่าน้อยสุด ค่ามากสุด ในการออมเงินที่ผ่านมา
source code : https://bitbucket.org/chananan/test2
วันจันทร์ที่ 23 กุมภาพันธ์ พ.ศ. 2558
Test-Driven Development with Python Chapter 6
Getting to the Minimum Viable Site
Ensuring Test Isolation in Functional Tests
เริ่มต้นตามหนังสืออธิบายว่าตอนนี้ database ที่เราใช้ในการ Functional
Test นั้นเป็น database จริง ซึ่งมีการบันทึกข้อมูลจากตัว test ลงไปใน
db.sqlite3 ซึ่งจริงๆแล้วตัว test ไม่ควรจะไปยุ่งกับข้อมูลใน database
ที่เราใช้งาน โดยใน Django มี Class ชื่อว่า
ซึ่ง
LiveServerTestCase ที่จะช่วยจำลอง database ขึ้นมาให้เราซึ่ง
LiveServerTestCase ใช้ตัวรัน
Functional Test ของ Django ซึ่งต้องรันที่ manage.py
และจะค้นหาไฟล์ที่ขึ้นต้นด้วย test ซึ่งเราจะทำการสร้าง directory สำหรับ
FT โดยเฉพาะ และเปลี่ยนชื่อ file เป็น test.py
สร้าง directory ใช้คำสั่ง
mkdir functional_tests
และต้องสร้าง init file สำหรับ directory ของ python ที่จะใช้กับ Django ใช้คำสั่ง
touch functional_tests/__init__.py
ย้ายไปยัง directory functional_testและเปลี่ยนชื่อเป็น tests.py ใช้คำสั่ง
git mv functional_tests.py functional_tests/tests.py
At this point your directory tree should look like this:
.
├── 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
แก้ไข Class ใน FT ให้รับ parameter จาก
LiveServerTestCasefunctional_tests/tests.py
fromdjango.testimportLiveServerTestCase
[....]
และเปลี่ยนจากการเรียก localhost port 8000 เป็นใช้ attribute ของclassNewVisitorTest(LiveServerTestCase):
LiveServerTestCaseคำสั่งในการรัน FT ก็จะเปลี่ยนไปdeftest_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 homepageself.browser.get(self.live_server_url)
python3 manage.py test functional_tests
และคำสั่งที่จะรัน Unit Test ก็ต้องเปลี่ยนด้วยเนื่องจากมี FT เพิ่มเข้ามา จึงต้องเจาะจงไปที่ test ใน lists
python3 manage.py test lists
Small Design When Necessary
ต่อไปเราจะใช้แนวคิดของ REST ซึ่งจะใช้ URL structure ในการทำงานในกรณีต่างๆ ของ app lists ของเรา- โดยการระบุว่าจะเป็น list ไหน (รองรับหลาย list) ให้แต่ละ list มี URL
เป็นของตัวเอง (ส่งการร้องขอไปยัง server แบบ GET Request คือการส่ง
attribute ระบุข้อมูลที่ต้องการจาก server บนส่ง URL)
/lists/<ชื่อ list>/
- สร้าง list ใหม่ให้ส่งการร้องขอแบบ POST Request
/lists/new
- เพิ่มข้อมูลไปยัง list ที่มีอยู่แล้วทาง POST Request
/lists/<ชื่อ list>/add_item
จากนั้นเราจะเพิ่มตัว FT ให้ตรวจสอบการใช้งานหลายๆ 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 tableinputbox.send_keys(Keys.ENTER)edith_list_url=self.browser.current_urlself.assertRegex(edith_list_url,'/lists/.+')#![]()
self.check_for_row_in_list_table('1: Buy peacock feathers')# There is still a text box inviting her to add another item. She[...]
จากนั้นเราจะลองให้ FT สามารถใส่ข้อมูลได้หลายๆ list
เมื่อลองรัน FT แล้วจะ error[...]# The page updates again, and now shows both items on her listself.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 #![]()
self.browser.quit()self.browser=webdriver.Firefox()# Francis visits the home page. There is no sign of Edith's# listself.browser.get(self.live_server_url)page_text=self.browser.find_element_by_tag_name('body').textself.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 URLfrancis_list_url=self.browser.current_urlself.assertRegex(francis_list_url,'/lists/.+')self.assertNotEqual(francis_list_url,edith_list_url)# Again, there is no trace of Edith's listpage_text=self.browser.find_element_by_tag_name('body').textself.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 หลังจากได้รับ POSTlists/tests.py
และแก้ใน lists/views.pyself.assertEqual(response.status_code,302)self.assertEqual(response['location'],'/lists/the-only-list-in-the-world/')
defhome_page(request):ifrequest.method=='POST':Item.objects.create(text=request.POST['item_text'])returnredirect('/lists/the-only-list-in-the-world/')items=Item.objects.all()returnrender(request,'home.html',{'items':items})
ซึ่ง UT จะผ่านแต่ FT จะไม่ผ่าน
จะ error ว่า เนื่องจากยังไม่มี list item ชื่อว่า the-only-list-in-the-world
Testing Views, Templates, and URLs Together with the Django Test Client
ต่อไปเราจะใช้ Django test
client ช่วย Unit Test ในการตรวจสอบ การ map URL ,การตรวจสอบ views
และการตรวจสอบการ render template ของ views โดยไปที่ 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
error เนื่องจากยังหา URL ไม่เจอ
A New URL
เราจึงไปสร้าง URL ใหม่ที่ superlists/urls.py
รัน UT อีกครั้ง error ว่า import view_list ไม่ได้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)),)
AttributeError: 'module' object has no attribute 'view_list'
[...]
django.core.exceptions.ViewDoesNotExist: Could not import
lists.views.view_list. View does not exist in module lists.views.
ดังนั้นจึงไปสร้าง view_list ใน lists/views.py
เมื่อรัน UT จะผ่านแต่รัน FT มี errordefview_list(request):items=Item.objects.all()returnrender(request,'home.html',{'items':items})
ต่อไปจะทำการ refactoring เอา test ที่ไม่จำเป็นแล้วออกจาก unit test
ดูว่าในไฟล์นั้นมี class และ function อะไรบ้างใช้คำสั่ง
grep -E "class|def" lists/tests.py
เราจะลบ
test_home_page_displays_all_list_items ออกเื่นองจากไ่มจำเ็ปนแ้ลว A Separate Template for Viewing Lists ยังทำไม่ได้
ต่อไปเราจะทำการแบ่ง template ของ home page กับ แสดง list ออกจากกันเริ่มต้นด้วยการสร้าง Unit Test ตรวจสอบว่าหน้าแสดง list ก็ใช้ template ของมันเอง
lists/tests.py
และไปเปลี่ยนที่ views.py ให้เรียก list.htmlclassListViewTest(TestCase):deftest_uses_list_template(self):response=self.client.get('/lists/the-only-list-in-the-world/')self.assertTemplateUsed(response,'list.html')deftest_displays_all_items(self):[...]
lists/views.py
เมื่อสร้าง test เสร็จจึงไปสร้าง template มา สร้างไฟล์ใหม่ใช้คำสั่งdefview_list(request):items=Item.objects.all()returnrender(request,'list.html',{'items':items})
touch lists/templates/list.html
แล้วก็ copy เนื้อใน file มาจาก home.html ก่อน
cp lists/templates/home.html lists/templates/list.html
จากนั้นที่หน้า home เราจะเปลี่ยนคำที่แสดงบนหน้าแรกว่า start a new to-do list ให้สื่อความหมายว่าเป็นหน้าเริ่มต้นlists/templates/home.html
และไปแก้ที่ views.py ให้หลังจาก POST Request แล้วให้ redirect ไปหน้า list<body><h1>Start a new To-Do list</h1><formmethod="POST"><inputname="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')
จากนั้นแก้ที่ list.html ให้มี action ไปยัง home_page (ไม่ให้กลับมาที่เดิมตาม default ของมัน)
lists/templates/list.html
ทดลองรัน FT<formmethod="POST"action="/">
Another URL and View for Adding List Items
aยังทำไม่ได้
วันพฤหัสบดีที่ 5 กุมภาพันธ์ พ.ศ. 2558
Test-Driven Development with Python Chapter 5
Saving User Input
Wiring Up Our Form to Send a POST Request
-ทดลองใช้ standard HTML POST request โดยสามารถใช้ HTML5 และ JavaScript
การส่ง Post request ให้ browser ทำได้โดยการให้ <form method ="POST"> ครอบคลุม
<input> element a
name= attribute
ปรับปรุง template at lists/templates/home.html ดังนี้
ทอลองรันจะขึ้น error ดังนี้
$ python3 functional_tests.py
[...]
Traceback (most recent call last):
File "functional_tests.py", line 39, in
test_can_start_a_list_and_retrieve_it_later
table = self.browser.find_element_by_id('id_list_table')
[...]
selenium.common.exceptions.NoSuchElementException: Message: 'Unable to locate
element: {"method":"id","selector":"id_list_table"}' ; Stacktrace [...]
เมื่อการทดสอบล้มเหลว มีวิธีแก้ไขดังนี้
- ให้เพิ่ม print statements เพื่อดูข้อความerrorที่โปรแกรมบอก
- ปรับปรุงข้อผิดพลาดนั้น
- ดูเว็บไซต์ ด้วยตัวเอง
- ใช้คำสั่ง time.sleep เพื่อหยุดการทดสอบระหว่างการทำงาน
เพิ่มคำสั่ง time.sleep ที่ไฟล์ functional_tests.py เพื่อหยุดการทดสอบระหว่างการประมวลผล
ทดลองรันไฟล์ functional_tests.py อีกครั้งจะทำการเรียกหน้าต่าง browser ขึ้นมาแสดงผลดังนี้
-Django จำเป็นต้องสร้าง token เพื่อป้องกันการโจมตีจาก CSRF เพื่อที่จะเพิ่ม CSRF token จำเป็นจะต้องใช้ template tag
{% … %} -CSRF คือ การปลอมแปลงคำขอระหว่างเว็บไซต์ (Cross Site Request Forgery) คือ
ภัยคุกคามประเภทหนึ่งทางเว็บไซต์ที่เกิดจากการที่ผู้ประสงค์ร้ายลักลอบปลอม
คำสั่งข้อมูลให้เสมือนเป็นคำสั่งจากเจ้าของบัญชีจริงเพื่อติดต่อกับระบบ
ทำให้ระบบเชื่อและเข้าใจว่าเจ้าของบัญชีนั้น -แก้ไขไฟล์ lists/templates/home.html.
Django จะ render <input type="hidden"> ที่ประกอบด้วย CSRF_token เป็นการสร้าง token เพื่อป้องกันการโจมตีจาก CSRF ทำให้เรารันโปรแกรมได้อย่างปกติ
ทดลองรันโปรแกรม จะเห็นว่า web page ที่เราเรียกนั้นไม่มีปัญหาแล้ว
จากนั้นให้ลบคำสั่ง time.sleep ออก
Processing a POST Request on the Server
แก้ไขไฟล์ lists/tests.py โดยการเพิ่ม method to HomePage Tast ดังนี้
ทดลองรัน
แก้ไขไฟล์ที่ lists/views.py
Passing Python Variables to Be Rendered in the Template
which is to pass variables from our Python
view code into HTML templates
การแสดงค่าตัวแปรจากโค้ด python บน template html Django Template Tag จะใช้สัญลักษณ์ {{ชื่อตัวแปร}}
การแสดงค่าตัวแปรจากโค้ด python บน template html Django Template Tag จะใช้สัญลักษณ์ {{ชื่อตัวแปร}}
แก้ไขไฟล์ lists/templates/home.html
แก้ไขไฟล์ lists/tests.py เพิ่มcode ส่วนล่างเข้าไป
โดยการส่งค่ามายัง template จากตัว Unit Test นั้นจะใช้คำสั่ง render_to_string
ตัว parameter แรกคือ template ที่จะส่งไป ตัวหลังคือการ map ระหว่างชื่อของตัวแรก ('new_item_text') กับค่าของตัวแปร ('A new list item')
ทดลองรัน
แก้ไขไฟล์ lists/views.py
ทดลองรัน unit test จะเกิด Key Error ขึ้น
ให้แก้ไขไฟล์ lists/views.py
ทอลองรัน unit test อีกครั้งก็จะผ่านแบบไม่มี error แต่เมื่อรัน functional_tests จะขึ้นerror
เราต้องการข้อมูลมากกว่านี้จึงใช้เทคนิคการ debugging FT คือ การแก้ให้ error message แสดงรายละเอียดมากขึ้น เช่น print สิ่งที่มีอยู่ใน 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,
)
)
[...]
ยังมี error อยู่ให้แก้ไขโค้ดจากตัวอย่างด้านบน ให้เหลือเพียง
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
แต่ยังเกิด error เนื่องจากว่า
เนื่องจากค่าตัวแปลที่เราจะส่งให้ template จาก functional_tests นั้นไม่มี '1:' แต่ในfunctional_tests ของเรานั้นมี '1:' ให้ไปที่lists/templates/home.html เพิ่ม'1:' เข้าไป เพราะ FT ต้องการให้เราระบุ '1:' ที่จุดเริ่มต้นของรายการ
เมื่อลองรัน functional test ก็จะผ่าน<tr><td>1: {{ new_item_text }}</td></tr>
ทดลองเพิ่มการส่งค่าให้ template จาก functional test โดยการ copy&paste แต่เปลี่ยนจาก '1: Buy peacock feathers' เป็น
'2: Use peacock feathers to make a fly' # 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 listtable=self.browser.find_element_by_id('id_list_table')rows=table.find_elements_by_tag_name('tr')self.assertIn('1: Buy peacock feathers',[row.textforrowinrows])self.assertIn('2: Use peacock feathers to make a fly',[row.textforrowinrows])# 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.
เมื่อทดลองรัน
จะ error เนื่องจาก template ยังไม่รองรับเลข2
Three Strikes and Refactor
หลักการของ DRY (Don't Repeat Yourself (DRY)) คือ
เมื่อมีการใช้คำสั่งเดิมๆ
ซ้ำๆไม่ว่าจะอยู่ในไฟล์เดียวกันหรืออยู่คนละไฟล์ก็ไม่ควรใช้วิธีการ
copy&paste
เพราะหากโค้ดมีการผิดพลาดจำเป็นที่จะต้องไปแก้หลายที่จึงควรใช้การเรียกใช้
ฟังก์ชัน(หากอยู่ในไฟล์เดียวกัน) หรือการ import หากอยู่คนละไฟล์แก้ไขไฟล์ functional_tests.py เพื่อทำการ Refactor เพิ่มcode ในส่วนนี้เข้าไป
[...]
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
[...]
# 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!')
[...]
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
[...]
# 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!')
[...]
ทดลองรัน functional_tests ก็ยังขึ้น Error เหมือนเดิม
เนื่องจาก template ยังไม่รองรับเลข 2
The Django ORM and Our First Model
การสร้าง Model ของ Database ด้วย Django ORM
แก้ไขไฟล์ lists/tests.py โดยการเพิ่มcode ด้านล้าง
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')
ทดลองรัน
แต่เมื่อรันแล้วจะ error ว่า import item ไม่ได้ เนื่องจากเรายังไม่มี item ดังนั้นเราจะต้องไปสร้าง item ซึ่งเป็น model ของ database
เริ่มต้นการสร้าง model ไปที่ไฟล์ lists/models.py ใส่code ตามนี้
from django.db import models
class Item(object):
pass
ทำการรัน unit test
เมื่อทำการรันจะได้ error ว่า Item ไม่มี attribute ชื่อว่า save จึงต้องสืบทอดมาจาก class model ของ Django
โดยแก้เป็นโค้ดดังนี้ lists/models.py
from django.db import models
class Item(models.Model):
pass
ทดลองรัน
เนื่องจากการสร้าง Django ORM เป็นเพียงแค่การ model Database แต่ยังไม่ได้เป็น Database จริงๆจึงมีอีกขั้นตอนนึงในการสร้าง Database เรียกว่า MigrationsOur First Database Migration
Migrations ซึ่งจะเป็นตัวช่วยในการจัดการกับ Database ทั้งการเพิ่มข้อมูลเข้า การดึงข้อมูลออก แม้แต่การทำตัวเป็นเหมือน Version Control สำหรับ database สามารถเรียกคือข้อมูลเดิมได้โดยการสร้าง Migrations ใช้คำสั่ง python3 manage.py makemigrations
โดยจะสร้างไฟล์ 0001_initial.py เป็น Migrations เริ่มต้น สำหรับ model Item
The Test Gets Surprisingly Far
เมื่อลอง test app 'list' ใช้คำสั่ง python3 manage.py test lists
เนื่องจาก Django ไม่รู้ว่าเรามี table ที่เก็บเป็น text อยู่ใน Item ของเรา เราจึงต้องสร้าง text ใหม่ขึ้นมาโดยใช้ชื่อว่า text field
ไปที่ไฟล์ lists/models.py เพิ่ม code
class Item(models.Model):
text = models.TextField()
ทดลองรันก็จะเกิด error ขึ้นว่าdjango.db.utils.OperationalError: no such column: lists_item.text
เพราะว่าเรามี field ใหม่(text field) ใน database เราจึงต้องสร้าง Migrates อันใหม่สำหรับ field อันใหม่ด้วย โดยใช้คำสั่ง python3 manage.py makemigrations
โดยให้เลือกตัวเลือก 2 คือ ให้เรา add ค่าเริ่มต้นของ field
ไปในไฟล์ lists/models.py
class Item(models.Model):
text = models.TextField(default='')
ใช้คำสั่งสร้าง Migrations อีกครั้ง
คราวนี้สามารถสร้างได้เป็นไฟล์ 0002_item_text.py
ทดลอง test app 'list' ใช้คำสั่ง python3 manage.py test lists จะรันผ่าน คือสามารถส่งข้อมูลไปยัง database ได้
$ git status
$ git diff
$ git add lists
$ git commit -m "Model for list Items and associated migration"
Saving the POST to the Database
ก่อนอื่นสร้างตัว Unit Test สำหรับทดสอบการบันทึกข้อมูลก่อน ใน test.py โดยการเพิ่ม 3 บรรทัดใหม่เข้าไป
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 เช็คว่าค่าที่รับมานั้นถูกต้องหรือไม่
ทดลองรัน
จะเห็นว่าขึ้น error ตามภาพที่ได้ ไฮไลท์เอาไว้
ปรับ views.py ให้เข้ากับตัว test
from django.http import HttpResponse
from django.shortcuts import render
from lists.models import Item
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',''),
})
ทดลองรัน
เมื่อ test Unit Test ก็จะผ่าน
ดังนั้นก็จะเริ่ม refactoring ต่อไป
แก้โค้ด views.py ดังนี้
return render(request, 'home.html', {
'new_item_text': item.text
})
'new_item_text': item.text
})
แก้ไขโค๊ด 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)
request = HttpRequest()
home_page(request)
self.assertEqual(Item.objects.count(), 0)
เมื่อลองรัน Unit Test จะฟ้อง
1 != 0 failure
ตามหนังสือให้แก้ใน 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()
เมื่อรัน Unit Test ก็จะผ่าน
Redirect After a POST
เมื่อเรารับค่ามาจาก POST แล้ว แทนที่จะ render ผลการตอบสนองกลับไป มันควรจะเปลี่ยนเส้นทาง(Redirect)ส่งค่ากลับ ไปยัง home page
เริ่มต้นด้วยการเขียน Unit Test
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'], '/')
ทดลองรัน
การเปลี่ยนเส้นทางควรมี HTTP Status 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'], '/')
[...]
แยกเป็น test สำหรับการรับข้อมูลเข้ามา และการ test Redirect เมื่อทดลองรันก็จะผ่าน
Rendering Items in the Template
ต่อไปหนังสือต้องการทำให้ app lists นี้สามารถแสดงข้อมูลทั้งหมดที่อยู่ใน database
ดังนั้นเริ่มจากการสร้าง Unit Test ก่อน
ดังนั้นเริ่มจากการสร้าง Unit Test ก่อน
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())
เมื่อรันก็จะ error
แก้ template ให้สามารถแสดงผลได้หลายๆแถว ในตารางโดยการใช้ Template Tag for loop เพื่อสร้างตารางที่แสดงข้อมูลใน lists ทั้งหมด คำสั่ง {% for .. in .. %}AssertionError: 'itemey 1' not found in '<html>\n <head>\n [...]
lists/templates/home.html
<table id="id_list_table">
{% for item in items %}
<tr><td>1: {{ item.text }}</td></tr>
{% endfor %}
</table>
เมื่อรันก็ยังไม่ผ่านจึงจะไปทำการปรับใน 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 ก็จะผ่าน แต่เมื่อลองรัน Functional Test จะเกิด error
โดยในหนังสือจะมีเทคนิคการแก้ ด้วยการเข้าไปใน http://localhost:8000
จะเห็นว่า "no such table: lists_item"
Creating Our Production Database with migrate
เราต้องทำการสร้าง database ของจริงขึ้นมาเพราะ database ที่เราใช้นั้นเป็น database พิเศษ สำหรับใช้ test
ในการสร้าง database เราจะใช้คำสั่ง
python3 manage.py migrate
เมื่อลองรัน Functional Test จะยัง error
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy peacock feathers', '1: Use peacock feathers to make a fly']
เนื่องจาก Templates ของเรายังไม่เรารับการเปลี่ยนค่าตัวเลข '1: ', '2: ',...
ดังนั้นเราจะใช้ Template Tag ในการช่วยดังนี้
lists/templates/home.html
{% for item in items %}
<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
{% endfor %}
เมื่อรับ Functional Test ก็จะผ่าน
หากต้องการลบ database ใช้คำสั่ง
rm db.sqlite3
ถ้าจะสร้าง database เปล่าใหม่ใช้คำสั่ง
python3 manage.py migrate --noinput
สมัครสมาชิก:
ความคิดเห็น (Atom)
































