คำสำคัญที่พูดถึงในหนังสือ TDD with Python
POST RequestRequest ในที่นี้หมายถึงการร้องขอข้อมูลระหว่าง Client กับ Server โดยใน HTTP Protocol มี method ที่ใช้ในการส่งการร้องขอข้อมูลอยู่ 2 แบบ หนึ่งในนั้นคือ POST method ซึ่งจะร้องขอโดยทำการใส่ข้อมูลเข้าไปในส่วนของ message body ของ request message โดยมักใช้กับข้อมูลที่มีความสำคัญ เช่น การส่ง Password จาก Client ไปยัง Server เป็นต้น
ศึกษาข้อมูลเพิ่มเติม :
http://www.w3schools.com/tags/ref_httpmethods.asp
http://ispying.blogspot.com/2013/09/get-post-request-get-php-script-html.html
http://www.jarticles.com/tutorials/protocol/httpbasic.html
Cross-Site Request Forgery (CSRF)
เป็นการโจมตีทาง Internet รูปแบบหนึ่ง โดยการส่ง request ที่มีการ tag หมายเลข cookie ของ Client ที่มีข้อมูลสำคัญอยู่ เช่น Password, รหัสบัญชีธนาคาร เป็นต้น ไปกับ url (ในการส่งการร้องขอแบบ GET Request) เพื่อเป็นการปลอมแปลงตัวตนว่าเป็น Client คนนั้นๆ
ศึกษาข้อมูลเพิ่มเติม :
https://www.blognone.com/node/37959
http://en.wikipedia.org/wiki/Cross-site_request_forgery
http://potisarnpittayacormhightschool.blogspot.com/2011/05/cross-site-request-forgery-csrf.html
Django Template Tags
เป็นภาษาที่ Django สร้างขึ้นมาเพื่ออำนวยความสะดวกสำหรับผู้ใช้ในการใช้งาน HTML เช่นการแสดงค่าของตัวแปลที่สามารถเปลี่ยนแปลงได้, การวน loop เป็นต้น
ศึกษาข้อมูลเพิ่มเติม :
https://docs.djangoproject.com/en/1.7/topics/templates/
http://aorjoa.blogspot.com/2014/02/template-tags-django.html
Triangulation
เป็นแนวคิดทางด้านการตรวจสอบโดยการเปรียบเทียบผลที่ได้ของสิ่งที่ต้องการจะศึกษา(ตรวจสอบ) จากมุมมองที่แตกต่างกัน ซึ่งเป็นการยืนยันถึงความน่าเชื่อถือของสิ่งที่ค้นพบ
ศึกษาข้อมูลเพิ่มเติม :
https://www.gotoknow.org/posts/77646
การเปรียบเทียบข้อค้นพบ (Finding) ของปรากฏการณ์ที่ทำการศึกษา (Phenomenon) จากแหล่งและมุมมองที่แตกต่างกัน นักวิจัยจำนวนมากคาดหมาย (Assume) ว่า Triangulation เป็นแนวทางการยืนยันความน่าเชื่อถือ (..... อ่านต่อได้ที่: https://www.gotoknow.org/posts/77646
การเปรียบเทียบข้อค้นพบ (Finding) ของปรากฏการณ์ที่ทำการศึกษา (Phenomenon) จากแหล่งและมุมมองที่แตกต่างกัน นักวิจัยจำนวนมากคาดหมาย (Assume) ว่า Triangulation เป็นแนวทางการยืนยันความน่าเชื่อถือ (..... อ่านต่อได้ที่: https://www.gotoknow.org/posts/77646
Object-Relational Mapper (ORM)
เป็นเทคนิคการเขียนโปรแกรมที่เป็นการแปลงข้อมูลหลายๆชนิด เช่น int string ฯลฯ มาอยู่รวมกันในลักษณะของ Object ของ Class (อยู่ในลักษณธของ OOP) ซึ่งจะสามารถสร้างเป็น Object Database
ศึกษาข้อมูลเพิ่มเติม :
http://en.wikipedia.org/wiki/Object-relational_mapping
http://www.thaicreate.com/community/object-relational-mapping-orm.html
Integrated Test
เป็นการทดสอบลักษณะหนึ่ง โดยเป็นการทดสอบที่จะสนใจการส่งข้อมูลระหว่างส่วนต่างๆ (module) เป็นหลัก เช่นการส่งข้อมูลระหว่าง Database กับส่วนของ Code ภาษา Python เป็นต้น
ศึกษาข้อมูลเพิ่มเติม :
http://tccom.lannapoly.ac.th/tc53/Ebook/se/se9-2.html
Migration
เปรียบเทียบเหมือนเป็น Version Control ของ Database โดยทำหน้าที่แก้ไขโครงสร้าง(column,row)หรือชนิดข้อมูลที่จะเก็บใน Database อีกทั้งสามารถเรียกคืนข้อมูลย้อนหลังที่เคยบันทึกไว้ใน Database ได้อีกด้วย
ศึกษาข้อมูลเพิ่มเติม :
http://ethaizone.com/blog/2012/10/%E0%B9%80%E0%B8%A3%E0%B8%B4%E0%B9%88%E0%B8%A1%E0%B8%95%E0%B9%89%E0%B8%99%E0%B8%81%E0%B8%B1%E0%B8%9A-laravel-migrations/
Redirect -> Post/Redirect/Get (PRG)
การพัฒนาเว็บที่ต้องมีการปกป้องการส่งข้อมูลจากการกรอก form บนเว็บเพจแล้วส่งข้อมูลไปยัง Database โดยการร้องขอแบบ POST Request โดยเมื่อส่ง request ออกไปแล้ว หากมีการกด refresh อาจทำให้มีการส่ง POST Request ซ้ำซ้อน ดังนั้นแทนที่จะกลับไปยังเว็บเพจโดยตรงจึงมีการเปลี่ยนเส้นทาง โดยการส่ง request code HTTP 302 เพื่อให้แน่ใจว่าจะมีการ refresh ได้อย่างปลอดภัย
ศึกษาข้อมูลเพิ่มเติม :
https://en.wikipedia.org/wiki/Post/Redirect/Get
สรุปการทดลองใช้งานคำสั่งตามหนังสือ TDD with Python
เริ่มต้น POST Request และการป้องกัน CSRF
จะให้ผู้ใช้มีการส่งค่าจาก browser มาเก็บไว้ที่ Database ของ server เริ่มต้นด้วยการกำหนดให้มีการส่งค่าจาก form แล้วร้องขอการส่งข้อมูลมายัง server ด้วย POST Request โดยการใส่คำสั่งmethod="POST" ใน tag <form> ของหน้า home.htmlทดลองรัน functional test<h1>Your To-Do list</h1><formmethod="POST"><inputname="item_text"id="id_new_item"placeholder="Enter a to-do item"/></form><tableid="id_list_table">
เมื่อลองรัน functional test จะเกิด error HTTP 403 เนื่องจากในการป้องกัน CSRF แต่ละ form ของเว็บเพจจะต้องมีการวาง token ไว้ให้รู้ว่าเป็นการส่งค่าจากหน้าเว็บเดิม โดยใส่ Django Template Tag เพิ่มใน home.html ดังนี้
ซึ่งก็จะมี error ว่ายังไม่มี to-do item ใน table เนื่องจากยังไม่มีการส่งค่าอะไร แต่ก็แสดงว่าเราได้ทำการป้องกัน CSRF สำเร็จแล้ว<formmethod="POST"><inputname="item_text"id="id_new_item"placeholder="Enter a to-do item"/>{% csrf_token %}</form>
เริ่มต้นการประมวลผล POST Request บน Server
เราจะเริ่มต้นด้วยการสร้าง Unit Test สำหรับการประมวณผล POST Request บน Server เริ่มด้วย Test ว่าหน้าเว็บ home.html สามารถบันทึก POST Request จากผู้ใช้ได้แล้ว return มา แล้วดูว่ามีค่าที่มาด้วยหรือไม่ (เพิ่มไปยัง test.py)จากที่เห็นข้างต้นตามหนังสือจะอธิบายว่าการเขียน Unit Test นั้นจะต้องประกอบไปด้วย 3 ส่วนได้แก่deftest_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())
1. ส่วนที่เป็น Set up ได้แก่การกำหนดค่าต่างๆให้ตัวที่เรากำลังจะ test ในที่นี้คือการกำหนด ว่า request มีการส่งเป็น method แบบ POST และคำที่จะส่งคือ 'A new list item'
2. ส่วนที่เรียกใช้ function ที่เราต้องการจะ assert ผล ได้แต่ function home_page
3. ทำการ assertion
ซึ่งเมื่อรัน Unit Test จะเกิด error ตามหนังสือก็จะบอกว่ามันจะสามารถรันผ่านได้หากเราสร้าง code path สำหรับ POST Request โดยเฉพาะแก้ใน view.py ดังนี้
ซึ่งก็สามารถรันผ่านได้แต่ยังเป็นโค้ดที่รันค่าคงที่และ test ด้วยค่าคงที่อยู่ซึ่งเราจะต้อง refactoring ต่อไปfromdjango.httpimportHttpResponsefromdjango.shortcutsimportrenderdefhome_page(request):ifrequest.method=='POST':returnHttpResponse(request.POST['item_text'])returnrender(request,'home.html')
เริ่มต้นส่งค่าตัวแปรจากโค้ด Python ไป render บน Template
การแสดงค่าตัวแปรจากโค้ด python บน template html Django Template Tag จะใช้สัญลักษณ์ {{ชื่อตัวแปร}}โดยการส่งค่ามายัง template จากตัว Unit Test นั้นจะใช้คำสั่ง render_to_string<body><h1>Your To-Do list</h1><formmethod="POST"><inputname="item_text"id="id_new_item"placeholder="Enter a to-do item"/>{% csrf_token %}</form><tableid="id_list_table"><tr><td>{{ new_item_text }}</td></tr></table></body>
ตัว parameter แรกคือ template ที่จะส่งไป ตัวหลังคือการ map ระหว่างชื่อของตัวแรก ('new_item_text') กับค่าของตัวแปร ('A new list item')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)
แล้วเปลี่ยน view.py เป็นคำสั่งดังนี้
แต่เกิด error ที่ไม่คาดคิด เกิด KeyError: 'item_text' ตามหนังสือจึงให้เราแก้ไขเป็นdefhome_page(request):returnrender(request,'home.html',{'new_item_text':request.POST['item_text'],})
เมื่อรัน Unit Test อีกครั้งก็จะผ่าน แต่เมื่อลองรัน Functional Test นั้น error ว่า ยังไม่มี item ใน table เราต้องการข้อมูลมากกว่านี้จึงเข้าสู่เทคนิคการ debugging FT คือ การแก้ให้ error message แสดงรายละเอียดมากขึ้น เช่น print สิ่งที่มีอยู่ใน tabledefhome_page(request):returnrender(request,'home.html',{'new_item_text':request.POST.get('item_text',''),})
ยังเกิด error อยู่จึงได้ลองเปลี่ยนจาก assertTrue เป็น arssertInself.assertTrue(any(row.text=='1: Buy peacock feathers'forrowinrows),"New to-do item did not appear in table -- its text was:\n%s"%(table.text,))
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
แต่ยังเกิด error ว่า
เนื่องจากค่าตัวแปรที่เราจะส่งให้ template render จาก functional test นั้นมันไม่มี '1: ' อยู่ด้วย เราจึงเพิ่มใน template ให้มีการแสดง '1: ' ด้วยself.assertIn('1: Buy peacock feathers', [row.text for row in rows]) AssertionError: '1: Buy peacock feathers' not found in ['Buy peacock feathers']
เมื่อลองรัน 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'ก็จะ error ว่า# 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.
เนื่องจากตัว template เรายังไม่รองรับ '2: ' ซึ่งจะต้อง refactoring ต่อไปAssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock feathers to make a fly']
ตามหนังสือก็จะอธิบายว่าเป็นการทดสอบที่เรียกว่า Triangulation คือลองเปลี่ยนมุมมองในการทดสอบดู ตอนแรกเรามีแค่ '1: ' มันก็ผ่าน แต่พอเพิ่ม '2: ' เข้าไปด้วยกลับไม่ผ่าน
Three Strikes and Refactor
หลักการของ DRY (Don't Repeat Yourself (DRY)) คือ เมื่อมีการใช้คำสั่งเดิมๆ ซ้ำๆไม่ว่าจะอยู่ในไฟล์เดียวกันหรืออยู่คนละไฟล์ก็ไม่ควรใช้วิธีการ copy&paste เพราะหากโค้ดมีการผิดพลาดจำเป็นที่จะต้องไปแก้หลายที่จึงควรใช้การเรียกใช้ฟังก์ชัน(หากอยู่ในไฟล์เดียวกัน) หรือการ import หากอยู่คนละไฟล์ข้อมูลเพิ่มเติม : http://hayaak.com/dont-repeat-yourself-dry/
เราจะเปลี่ยนจาก copy&paste การตรวจสอบแต่ละข้อมูลใน list to-do เป็นฟังก์ชันดังนี้
สังเกตว่าหากฟังก์ชันไม่ได้ขึ้นต้นด้วย test ก็จะไม่เรียกใช้ว่าเป็นฟังก์ชันสำหรับ functional test แต่เป็นฟังก์ชันที่เอาไว้เรียกใช้ซ้ำdefcheck_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.textforrowinrows])
ซึ่งผลที่ได้ก็ยังเหมือนเดิมคือ error ว่า# When she hits enter, the page updates, and now the page lists# "1: Buy peacock feathers" as an item in a to-do list tableinputbox.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 listself.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[...]
เนื่องจากตัว template เรายังไม่รองรับ '2: 'AssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock feathers to make a fly']
การสร้าง Model ของ Database ด้วย Django ORM
ก่อนที่ะเริ่มสร้าง เหมือนทุกครั้งเราจะต้องสร้างตัว Unit Test ก่อน โดยใช้ฟังก์ชันนี้ใน test.pyจากฟังก์ชันด้านบนก็จะมีการสร้าง save ค่า 2 ค่า ลงไปใน item และมีการ assert ว่าใน item มีข้อมูลอยู่ 2 ตัว และข้อมูลทั้ง 2 ตัวคือfromlists.modelsimportItem[...]classItemModelTest(TestCase):deftest_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')
'The first (ever) list item'และ 'Item the second'ใช่หรือไม่ซึ่งการ test ดังกล่าวเรียกว่า Integrated Test เนื่องจากเป็นการ test การส่งข้อมูลระหว่างส่วนก็คือ โค้ดภาษา python และ database
แต่เมื่อรันแล้วจะ error ว่า import item ไม่ได้ เนื่องจากเรายังไม่มี item ดังนั้นเราจะต้องไปสร้าง item ซึ่งเป็น model ของ database
เริ่มต้นการสร้าง model ไปที่ lists/models.py ทดลองใส่โค้ดตามนี้
เมื่อทำการรันจะได้ error ว่า Item ไม่มี attribute ชื่อว่า savefromdjango.dbimportmodelsclassItem(object):pass
การที่เราจะสร้าง Item นี้ให้เป็น model ที่มี attribute ตามที่ model ของ Django ควรมีทำได้โดย สืบทอด(inherit)มาจาก class model ของ Django โดยแก้เป็นโค้ดดังนี้
โดย Class ที่เราสร้างขึ้นมาจะเป็น Database table, ตัว atrribute ชนิดต่างๆ จะเป็น column และค่าที่เราเก็บนั้นจะอยู่เป็น row ใน databasefromdjango.dbimportmodelsclassItem(models.Model):pass
เมื่อลองรัน Unit Test ก็จะขึ้นว่า Database error
เนื่องจากการสร้าง Django ORM เป็นเพียงแค่การ model Database แต่ยังไม่ได้เป็น Database จริงๆ ดังนั้นจึงมีอีกขั้นตอนนึงในการสร้าง Database เรียกว่า Migrations ซึ่งจะเป็นตัวช่วยในการจัดการกับ Database ทั้งการเพิ่มข้อมูลเข้า การดึงข้อมูลออก แม้แต่การทำตัวเป็นเหมือน Version Control สำหรับ database สามารถเรียกคือข้อมูลเดิมได้django.db.utils.OperationalError: no such table: lists_item
โดยการสร้าง Migrations ใช้คำสั่ง
python3 manage.py makemigrations
จะสร้าง Migrations สำหรับ app 'lists' ของเรา โดยจะสร้างไฟล์ 0001_initial.py เป็น Migrations เริ่มต้น สำหรับ model Itemเมื่อลอง test app 'list' ใช้คำสั่ง
python3 manage.py test lists
จะ error ว่าคือไม่มี attribute 'text' สำหรับ model Item ของเราAttributeError: 'Item' object has no attribute 'text'
เนื่องจาก Django ไม่รู้ว่าเรามี table ที่เก็บเป็น text อยู่ใน Item ของเรา เนื่องจาก Class Item ที่เราสือทอดมาจาก Class models ตอนแรกจะสร้างให้แค่ column หลักแรกใน Database ไว้ให้เราเท่านั้นซึ่ง id attribute แต่ถ้าหากเราอยากได้ column ชนิดอื่นๆ เราต้องสร้างขึ้นมาเอง โดยในที่นี้เราจะสร้าง text field
classItem(models.Model):text=models.TextField()
เมื่อลองรันก็ error ว่า
เพราะว่าเรามี field ใหม่(text field) ใน database ดังนั้นเราจึงต้องสร้าง Migrates อันไหมสำหรับ field ของเราใช้คำสั่งdjango.db.utils.OperationalError: no such column: lists_item.text
python3 manage.py makemigrations
โดยจะขึ้นว่า
You are trying to add a non-nullable field 'text' to item without a default;
we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option:2
โดยให้เลือกตัวเลือก 2 คือ ให้เรา add ค่าเริ่มต้นของ field นี้ ในไฟล์ lists/models.pyใช้คำสั่งสร้าง Migrations อีกครั้ง คราวนี้สามารถสร้างได้เป็นไฟล์ 0002_item_text.pyclassItem(models.Model):text=models.TextField(default='')
เมื่อลอง test app 'list' ใช้คำสั่ง python3 manage.py test lists จะรันผ่าน คือสามารถส่งข้อมูลไปยัง database ได้
บันทึกข้อมูลจาก POST Request ลง Database
มีการเช็คจำนวนข้อมูล ตรวจสอบข้อมูลตัวแรกว่าเหมือนกับdeftest_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.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)
'A new list item' ไหมตามหนังสือนั้นอธิบายว่าเราควรมีการสังเกตโค้ด Unit Test ว่ามันยาวไปหรือไม่ เพราะ Unit Test เป็นการ test แยกเป็นส่วนๆ หากยาวเกินไปอาจมีการ test ที่ซ้ำซ้อน หรือ มีการ test หลายส่วนรวมอยู่ในตัว test ตัวเดียวกัน เพื่อเป็นการให้ test มีประสิทธิภาพจึงไม่ควรให้ Unit Test ยาวเกินไป
เมื่อลองรัน Unit test จะ error ว่า
ตามหนังสือให้ปรับ views.py ให้เข้ากับตัว testself.assertEqual(Item.objects.count(), 1) AssertionError: 0 != 1
เมื่อ test Unit Test ก็จะผ่าน ดังนั้นก็จะเริ่ม refactoring ต่อไปfromdjango.shortcutsimportrenderfromlists.modelsimportItemdefhome_page(request):item=Item()item.text=request.POST.get('item_text','')item.save()returnrender(request,'home.html',{'new_item_text':request.POST.get('item_text',''),})
แก้โค้ด views.py ดังนี้
จะเห็นว่าส่วนของค่าตัวแปรมีการรับค่าเข้ามาจริงๆreturnrender(request,'home.html',{'new_item_text':item.text})
ขั้นต่อมาจะ refactoring ให้ไม่มีการรับค่า ' ' (blank) เข้ามาใน database โดยการเพิ่ม Unit Test
เมื่อลองรัน Unit Test จะฟ้องclassHomePageTest(TestCase):[...]deftest_home_page_only_saves_items_when_necessary(self):request=HttpRequest()home_page(request)self.assertEqual(Item.objects.count(),0)
1 != 0 failure ตามหนังสือให้แก้ใน views.py ดังนี้เปลี่ยนจากการส่งค่า blank [defhome_page(request):ifrequest.method=='POST':new_item_text=request.POST['item_text']#![]()
Item.objects.create(text=new_item_text)#![]()
else:new_item_text=''#![]()
returnrender(request,'home.html',{'new_item_text':new_item_text,#![]()
})
'new_item_text': request.POST.get('item_text', ''),] เป็นอ่านจาก Item ['new_item_text': new_item_text] ที่มีค่าเป็น blank เหมือนกันเมื่อรัน Unit Test ก็จะผ่าน
Redirect After a POST
เมื่อเรารับค่ามาจาก POST แล้ว แทนที่จะ render ผลการตอบสนองกลับไป มันควรจะเปลี่ยนเส้นทาง (Redirect) ที่จะกลับไปยัง home pageเริ่มต้นด้วยการเขียน Unit Test ในการ test การส่งข้อมูลกลับ
การ Redirect ควรมี HTTP status code 302 แต่เมื่อรันแล้วมัน error 200deftest_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'],'/')
ตามหนังสือจึงให้ไปปรับในไฟล์ views.py
เมื่อรัน test ก็จะผ่านfromdjango.shortcutsimportredirect,renderfromlists.modelsimportItemdefhome_page(request):ifrequest.method=='POST':Item.objects.create(text=request.POST['item_text'])returnredirect('/')returnrender(request,'home.html')
Better Unit Testing Practice: Each Test Should Test One Thing
ตามที่กล่าวไป การ test Unit Test ที่ดีควรสั้น และแยกเป็นส่วนๆ เพื่อให้รู้ได้ชัดเจนว่า bug ที่ตรงไหน จะทำให้ debug ได้ถูกจุดหนังสือก็ได้แก้เป็นดังนี้
แยกเป็น test สำหรับการรับข้อมูลเข้ามา และการ test Redirectdeftest_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')deftest_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'],'/')
เมื่อรันก็ควรจะผ่าน
ต่อไปหนังสือต้องการทำให้ app lists นี้สามารถแสดงข้อมูลทั้งหมดที่อยู่ใน database
ดังนั้นเริ่มจากการสร้าง Unit Test ก่อน
เมื่อรันก็จะ errorclassHomePageTest(TestCase):[...]deftest_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 ให้สามารถแสดงผลได้หลายๆแถว ในตารางโดยการใช้ Template Tag for loop เพื่อสร้างตารางที่แสดงข้อมูลใน lists ทั้งหมดAssertionError: 'itemey 1' not found in '<html>\n <head>\n [...]
เมื่อรันก็ยังไม่ผ่านจึงจะไปทำการปรับใน views.py<tableid="id_list_table">{% for item in items %}<tr><td>1: {{ item.text }}</td></tr>{% endfor %}</table>
เมื่อลองรัน Unit Test ก็จะผ่าน แต่เมื่อลองรัน Functional Test จะเกิด errordefhome_page(request):ifrequest.method=='POST':Item.objects.create(text=request.POST['item_text'])returnredirect('/')items=Item.objects.all()returnrender(request,'home.html',{'items':items})
ตามหนังสือให้วิธีการ Debug Functional Test อีกเทคนิคหนึ่งคือให้เข้าไปที่ http://localhost:8000AssertionError: 'To-Do' not found in 'OperationalError at /'
จะเห็นว่า "no such table: lists_item"
สร้าง Database with migrate
Database ที่เรามีนั้นเป็นเพียงแค่ Class ยังไม่ได้มีการบันทึกค่าลงบนไฟล์ใดจริงๆ ในการสร้าง Database ใช้คำสั่ง
python3 manage.py migrate
จะมีการบันทึก Database ไว้ในไฟล์ db.sqlite3เมื่อลองรัน Functional Test จะยัง error
เนื่องจาก Templates ของเรายังไม่เรารับการเปลี่ยนค่าตัวเลข '1: ', '2: ',...AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy peacock feathers', '1: Use peacock feathers to make a fly']
ดังนั้นเราจะใช้ Template Tag ในการช่วยดังนี้
{% 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
อ้างอิงจากหนังสือ Test-Driven Development with Python by Harry Percival :
http://chimera.labs.oreilly.com/books/1234000000754

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