เนื่องจากเดิมอันเดิมยาวและติดกันเกินไปทำให้ยากต่อการอ่าน
จึงแยก (Week 3-2) - (Week 5-1) Test-Driven Development with Python ออกเป็น 7 part
Chapter 7
เริ่ม chapter 7 หนังสือบอกจะพัฒนาให้ app ของเราสามารถลองรับ user ได้มากขึ้น โดยแบ่งเป็นหลายๆ list แยกกัน โดยมี url ดังรูป
โดยมี use case ดังนี้
ในขั้นต้นแยก functional test เป็นสำหรับ user เดียว และ multi user (เพิ่มเรื่องราว user story โดย Francis เข้ามาใช้งานด้วยอีกคนนอกจาก Edith)
โดยมีหน้าตาเต็มๆดังนี้
- ในขั้นต้น update test_redirects_after_POST() โดย assert locationว่า ไปที่ '/lists/the-only-list-in-the-world/' หรือไม่
- เมื่อ unit test ก็ fail ตามคาดเพราะยังไม่ได้แก้อะไรเพิ่มตามที่ test
- แก้แบบลวกๆโดยให้ redirect ไปที่ '/lists/the-only-list-in-the-world/'
- unit test ผ่าน
แต่ functional test จะติด error มากมายดังรูปเพราะ page not found (ยังไม่รู้จัก url path '/the-only-list-in-the-world/' )
- จึงมา write minimal code (เขียน unit test) ต่อ โดยเขียน test ว่าแสดง item ครบหรือไม่
- เมื่อ unit test จะ error ที่ self.assertContains(response, 'itemey 1') เพราะยังไม่ได้สร้าง function view_list() ใน views.py และเชื่อม url ใน urls.py
- จึงสร้าง url ของ the-only-list-in-the-world
- เมื่อ test ทั้งหมด จะติด error เพราะยังไม่ได้สร้าง function view_list() ใน views.py
จึงสร้าง view_list() ใน views.py รับ items ทั้งหมดจาก database แล้ว render หน้าเดิม(home.html)
เมื่อ functional test จะติด error
test สำหรับ user เดียว : หา '2: Use peacock feathers to make a fly' ไม่เจอ
test สำหรับ multi-user : 'Buy peacock feathers' ดันไปรวมอยู่กับ 'Your To-Do list' กลายเป็น 'Your To-Do list\n1: Buy peacock feathers'
- test สำหรับ user เดียวเป็นเพราะไม่เลือก url ที่จะไปไว้เฉพาะเจาะจง ค่าถึงถูก reset จึงเพิ่ม action="/" เข้าไปใน <form> (เดิมผ่านเพราะเรามีหน้าเดียว POST ถึง auto ส่งข้อมูลกลับไปหน้าเดิม)
- เมื่อเรา functional test อีกที error สำหรับ test user เดียวจะหายไป เหลือแต่ error สำหรับ test multi-user
โดยเมื่อเรามี test เยอะขึ้น ทำให้ยากต่อการมอง สามารถใช้คำสั่ง grep -E "class|def" lists/tests.py เพื่อดู class และ function(def) ต่างๆได้และลบอันที่ไม่จำเป็นออกไปได้
- ต่อมาเพิ่ม test_uses_list_template เข้าไปใน class ListViewTest เพื่อ test template สำหรับ function view_list โดยมีชื่อ template ชื่อว่า list.html
- ต่อมาจึงทำการสร้าง list.html
- แล้ว copy ทั้งหมดใน home.html มาใส่
- แล้วจึงไปแก้หน้า home.html ให้ไม่ show data ใดๆอีกต่อไป
- แก้ home_page() ใน views.py ให้ถ้า POST เก็บข้อมูลแล้ว redirect ไปที่ url path '/lists/the-only-list-in-the-world/' เลย
- run unit test จะผ่าน แต่ functional test จะติดไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
ทำการ commit แล้วสร้าง unit test class ใหม่ NewListTest พร้อม 2 test
- test_can_save_a_POST_request ทดสอบ save POST ลงใน database
- test_redirects_after_POST ทดสอบ redirect หลัง POST ว่าไปถูกหน้าหรือไม่
เมื่อ run unit จะโชว์ error ของทั้ง 2 function test ที่เขียนขึ้นมา คือ
1. object count = 0 เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
2. ไม่ได้ redirect '/lists/the-only-list-in-the-world/' เพราะติด 404 page not found เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
ทำการสร้าง function new_list ใน views.py และใส่ path ใน urls.py สำหรับ function
ทำการลบการเอาข้อมูลใส่ data base เพื่อไม่ให้ redundant (ใส่ข้อมูลลง database ทั้งใน home_page() และ new_list() )
แต่เมื่อ functional test ติดของ one user กลับมาเพราะ forms ยังคงชี้ไปที่ URL เก่าอยู่ทั้ง home.html และ lists.html
- จึงแก้ form action ให้ชี้ไปที่ function new_list()
- แต่ functional test ยังคงติด muti-user อันเดิม คือ ไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
จึงทำการปรับ unit test ต่างๆดังนี้ (เพิ่ม list เข้ามาใน ItemModelTest เปลี่ยนชื่อต่างๆให้เหมาะสม)
lists/tests.py
@@ -1,5 +1,5 @@
from django.test import TestCase
-from lists.models import Item
+from lists.models import Item, List
class HomePageTest(TestCase):
@@ -44,22 +44,32 @@ class ListViewTest(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_)
- ทำการเพิ่ม List เข้ามาใน models.py และ test ไปด้วยคล้าย TDD Chapter 5 จนถึงขั้นตอนดังภาพด้านล่าง(รูปบนสุด)
- unit test จะพบ error message ดังรูป (รูปรองลงมา) เป็นเพราะ Django จะ save string สำหรับ List object ไม่ได้เซฟสำหรับ relationship ระหว่าง object
จึงทำการ update ใน models.py ให้ list เป็น foreinkey ระหว่าง 2 ตาราง
(Foreign key คือ คีย์นอก เป็นคีย์ที่เชื่อม Table ที่เกี่ยวข้องหรือมีความสัมพันธ์กัน เช่น ใน Table หลักสูตร กำหนดให้รหัสวิชาเป็น Primary Key และทำการเชื่อมโยงไปยัง Table ลงทะเบียนเพื่อต้องการทราบชื่อวิชาและหน่วยกิตที่นักเรียนลงทะเบียน โดยกำหนดฟิลด์ รหัสวิชา ใน Table ลงทะเบียนเป็น Foreign Key ในลักษณะความสัมพันธ์ One to Many หมายความว่า รหัสวิชา 1 วิชา สามารถให้นักเรียนลงทะเบียนได้มากกว่า 1 คน ดังนั้นจึงมีรหัสซ้ำกันได้ใน Table ลงทะเบียน
ต่อมาทำการ makemigration แต่ไม่เป็นไปตามหนังสือ เนื่องจาก django update version ใหม่
จึงทำการเปิด reference ใน website ของ django ดูและ update ตามกรอบแดงในภาพข้างล่างลงไปใน models.py ของเรา
เมื่อแก้แล้วเป็นดังภาพด้านล่างและ makemigration ได้
แต่จะติด test อันเก่าเนื่องจาก foreignkey ทำให้เกิดความสัมพันธ์แบบ one-to-many ระหว่าง 2 ตาราง
จึงทำการแก้ unit test และ function ใน views.py ของเราให้เหมาะสมกับ database
functional test จะติด error เดิม
( AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buymilk'] )
แต่ลอง unit test จะผ่านทั้งหมด จึงทำการ commit
แล้วจึงทำการปรับ code บางส่วน test_displays_all_items test เปลี่ยนชื่อเป็น test_displays_only_items_for_that_list ให้สื่อความหมาย แล้ว update ให้ตรวจสอบเป็น list ของใครของมัน
แก้ urls.py (ตามหนังสือใช้ url() แต่ computer ผมรันแล้ว error จึงใช้ path repath มาตั้งแต่ต้น)
ใช้ repath() กับ function view_list ให้แสดงแยกว่า list ไหนเป็น list ผ่านตามลำดับ โดยใช้ regular expression ของ django บอก link url
( ข้อมูลเพิ่มเติม regular expression ผมได้ใส่ไว้ใน Django tutorial 3 )
ซึ่งเมื่อ run unit test จะติด error ของ function view_list() เพราะ ไม่ได้ใส่ argument ว่าอยู่ list ไหน
- ทำการแก้โดยใส่ list_id กับตัวแปรรับค่ามาใช้
- เมื่อ unit test จะติด test เก่า ( test_redirects_after_POST() ) เนื่องจาก ยังคงดู url 'the-only-list-in-the-world' อยู่
แก้ test_redirects_after_POST และ new_list ให้ตาม urls.py อันใหม่ที่อัพเดท
จะรัน unit test ผ่าน
แต่ functional test จะติด one user test เพราะ ทุกครั้งที่เราใส่อะไรลงไปจะสร้าง list ใหม่ ยังไม่สามารถเพิ่มค่าลงใน list เดิมได้
update unit test function 2 อันได้แก่ test_can_save_a_POST_request_to_an_existing_list และ test_redirects_to_list_view ก่อนเขียน Add Items ไปยัง List ที่มีอยู่แล้ว
เมื่อ unit test จะได้
ดูจาก wiki list http code ได้ดังนี้
หนังสือบอกเป็นเพราะว่า "we’ve used a very "greedy" regular expression in our URL"
เพราะเมื่อใครก็ตามขอ URL เช่น /lists/1/add_item/ นั้น (.+) ของเราจะหาทั้งประโยคคือ ( 1/add_item/ ) และไปใช้ function view_list() ไม่ใช้ add_item() เพราะ (.+) เป็นตัวอักษรหรืออะไรก็ได้
จึงได้ทำการแก้ให้เป็น numerical digit (ตัวเลข) เท่านั้น
- จึงปรับ urls.py ที่เกี่ยวข้องให้เป็น (.d+) และ ใส่ path ของ add_item()
- สร้าง function add_item()
- run unit test ผ่าน
ต่อมาทำการแก้ action ใน list ให้เรียกใช้ add_item()
update unit test ทดสอบว่า list ที่ถูกส่งไปยัง template ถูก list หรือไม่
เมื่อ run unit test จะได้ KeyError: 'list' เพราะเราไม่ได้ส่ง list ไปยัง template
KeyError: 'list'
- จึงทำการแก้ view_list ให้เก็บค่า list_id ไว้ในตัวแปร list_ และส่งไปยัง template ด้วย
- นำ list_ ที่ได้มา มาชี้ที่ loop
run test ทั้งหมดจะผ่านทั้งหมด
เหมือนจะเสร็จแล้วแต่ในหนังสือเค้าบอกว่า เอา urls ของ app ไปใส่ไว้ใน urls.py ของ project มันไม่เป็นมืออาชีพ จึงทำการแก้ดังภาพด้านล่าง
เริ่ม chapter 7 หนังสือบอกจะพัฒนาให้ app ของเราสามารถลองรับ user ได้มากขึ้น โดยแบ่งเป็นหลายๆ list แยกกัน โดยมี url ดังรูป
โดยมี use case ดังนี้
ในขั้นต้นแยก functional test เป็นสำหรับ user เดียว และ multi user (เพิ่มเรื่องราว user story โดย Francis เข้ามาใช้งานด้วยอีกคนนอกจาก Edith)
โดยมีหน้าตาเต็มๆดังนี้
- ในขั้นต้น update test_redirects_after_POST() โดย assert locationว่า ไปที่ '/lists/the-only-list-in-the-world/' หรือไม่
- เมื่อ unit test ก็ fail ตามคาดเพราะยังไม่ได้แก้อะไรเพิ่มตามที่ test
- แก้แบบลวกๆโดยให้ redirect ไปที่ '/lists/the-only-list-in-the-world/'
- unit test ผ่าน
แต่ functional test จะติด error มากมายดังรูปเพราะ page not found (ยังไม่รู้จัก url path '/the-only-list-in-the-world/' )
- จึงมา write minimal code (เขียน unit test) ต่อ โดยเขียน test ว่าแสดง item ครบหรือไม่
- เมื่อ unit test จะ error ที่ self.assertContains(response, 'itemey 1') เพราะยังไม่ได้สร้าง function view_list() ใน views.py และเชื่อม url ใน urls.py
- จึงสร้าง url ของ the-only-list-in-the-world
- เมื่อ test ทั้งหมด จะติด error เพราะยังไม่ได้สร้าง function view_list() ใน views.py
จึงสร้าง view_list() ใน views.py รับ items ทั้งหมดจาก database แล้ว render หน้าเดิม(home.html)
เมื่อ functional test จะติด error
test สำหรับ user เดียว : หา '2: Use peacock feathers to make a fly' ไม่เจอ
test สำหรับ multi-user : 'Buy peacock feathers' ดันไปรวมอยู่กับ 'Your To-Do list' กลายเป็น 'Your To-Do list\n1: Buy peacock feathers'
- test สำหรับ user เดียวเป็นเพราะไม่เลือก url ที่จะไปไว้เฉพาะเจาะจง ค่าถึงถูก reset จึงเพิ่ม action="/" เข้าไปใน <form> (เดิมผ่านเพราะเรามีหน้าเดียว POST ถึง auto ส่งข้อมูลกลับไปหน้าเดิม)
- เมื่อเรา functional test อีกที error สำหรับ test user เดียวจะหายไป เหลือแต่ error สำหรับ test multi-user
โดยเมื่อเรามี test เยอะขึ้น ทำให้ยากต่อการมอง สามารถใช้คำสั่ง grep -E "class|def" lists/tests.py เพื่อดู class และ function(def) ต่างๆได้และลบอันที่ไม่จำเป็นออกไปได้
- ต่อมาเพิ่ม test_uses_list_template เข้าไปใน class ListViewTest เพื่อ test template สำหรับ function view_list โดยมีชื่อ template ชื่อว่า list.html
- ต่อมาจึงทำการสร้าง list.html
- แล้ว copy ทั้งหมดใน home.html มาใส่
- แล้วจึงไปแก้หน้า home.html ให้ไม่ show data ใดๆอีกต่อไป
- แก้ home_page() ใน views.py ให้ถ้า POST เก็บข้อมูลแล้ว redirect ไปที่ url path '/lists/the-only-list-in-the-world/' เลย
- run unit test จะผ่าน แต่ functional test จะติดไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
ทำการ commit แล้วสร้าง unit test class ใหม่ NewListTest พร้อม 2 test
- test_can_save_a_POST_request ทดสอบ save POST ลงใน database
- test_redirects_after_POST ทดสอบ redirect หลัง POST ว่าไปถูกหน้าหรือไม่
เมื่อ run unit จะโชว์ error ของทั้ง 2 function test ที่เขียนขึ้นมา คือ
1. object count = 0 เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
2. ไม่ได้ redirect '/lists/the-only-list-in-the-world/' เพราะติด 404 page not found เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
ทำการสร้าง function new_list ใน views.py และใส่ path ใน urls.py สำหรับ function
ทำการลบการเอาข้อมูลใส่ data base เพื่อไม่ให้ redundant (ใส่ข้อมูลลง database ทั้งใน home_page() และ new_list() )
แต่เมื่อ functional test ติดของ one user กลับมาเพราะ forms ยังคงชี้ไปที่ URL เก่าอยู่ทั้ง home.html และ lists.html
- จึงแก้ form action ให้ชี้ไปที่ function new_list()
- แต่ functional test ยังคงติด muti-user อันเดิม คือ ไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
จึงทำการปรับ unit test ต่างๆดังนี้ (เพิ่ม list เข้ามาใน ItemModelTest เปลี่ยนชื่อต่างๆให้เหมาะสม)
lists/tests.py
@@ -1,5 +1,5 @@
from django.test import TestCase
-from lists.models import Item
+from lists.models import Item, List
class HomePageTest(TestCase):
@@ -44,22 +44,32 @@ class ListViewTest(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_)
- ทำการเพิ่ม List เข้ามาใน models.py และ test ไปด้วยคล้าย TDD Chapter 5 จนถึงขั้นตอนดังภาพด้านล่าง(รูปบนสุด)
- unit test จะพบ error message ดังรูป (รูปรองลงมา) เป็นเพราะ Django จะ save string สำหรับ List object ไม่ได้เซฟสำหรับ relationship ระหว่าง object
จึงทำการ update ใน models.py ให้ list เป็น foreinkey ระหว่าง 2 ตาราง
(Foreign key คือ คีย์นอก เป็นคีย์ที่เชื่อม Table ที่เกี่ยวข้องหรือมีความสัมพันธ์กัน เช่น ใน Table หลักสูตร กำหนดให้รหัสวิชาเป็น Primary Key และทำการเชื่อมโยงไปยัง Table ลงทะเบียนเพื่อต้องการทราบชื่อวิชาและหน่วยกิตที่นักเรียนลงทะเบียน โดยกำหนดฟิลด์ รหัสวิชา ใน Table ลงทะเบียนเป็น Foreign Key ในลักษณะความสัมพันธ์ One to Many หมายความว่า รหัสวิชา 1 วิชา สามารถให้นักเรียนลงทะเบียนได้มากกว่า 1 คน ดังนั้นจึงมีรหัสซ้ำกันได้ใน Table ลงทะเบียน
ต่อมาทำการ makemigration แต่ไม่เป็นไปตามหนังสือ เนื่องจาก django update version ใหม่
จึงทำการเปิด reference ใน website ของ django ดูและ update ตามกรอบแดงในภาพข้างล่างลงไปใน models.py ของเรา
เมื่อแก้แล้วเป็นดังภาพด้านล่างและ makemigration ได้
แต่จะติด test อันเก่าเนื่องจาก foreignkey ทำให้เกิดความสัมพันธ์แบบ one-to-many ระหว่าง 2 ตาราง
จึงทำการแก้ unit test และ function ใน views.py ของเราให้เหมาะสมกับ database
functional test จะติด error เดิม
( AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buymilk'] )
แต่ลอง unit test จะผ่านทั้งหมด จึงทำการ commit
แล้วจึงทำการปรับ code บางส่วน test_displays_all_items test เปลี่ยนชื่อเป็น test_displays_only_items_for_that_list ให้สื่อความหมาย แล้ว update ให้ตรวจสอบเป็น list ของใครของมัน
แก้ urls.py (ตามหนังสือใช้ url() แต่ computer ผมรันแล้ว error จึงใช้ path repath มาตั้งแต่ต้น)
ใช้ repath() กับ function view_list ให้แสดงแยกว่า list ไหนเป็น list ผ่านตามลำดับ โดยใช้ regular expression ของ django บอก link url
( ข้อมูลเพิ่มเติม regular expression ผมได้ใส่ไว้ใน Django tutorial 3 )
ซึ่งเมื่อ run unit test จะติด error ของ function view_list() เพราะ ไม่ได้ใส่ argument ว่าอยู่ list ไหน
- ทำการแก้โดยใส่ list_id กับตัวแปรรับค่ามาใช้
- เมื่อ unit test จะติด test เก่า ( test_redirects_after_POST() ) เนื่องจาก ยังคงดู url 'the-only-list-in-the-world' อยู่
แก้ test_redirects_after_POST และ new_list ให้ตาม urls.py อันใหม่ที่อัพเดท
จะรัน unit test ผ่าน
แต่ functional test จะติด one user test เพราะ ทุกครั้งที่เราใส่อะไรลงไปจะสร้าง list ใหม่ ยังไม่สามารถเพิ่มค่าลงใน list เดิมได้
update unit test function 2 อันได้แก่ test_can_save_a_POST_request_to_an_existing_list และ test_redirects_to_list_view ก่อนเขียน Add Items ไปยัง List ที่มีอยู่แล้ว
เมื่อ unit test จะได้
ดูจาก wiki list http code ได้ดังนี้
หนังสือบอกเป็นเพราะว่า "we’ve used a very "greedy" regular expression in our URL"
เพราะเมื่อใครก็ตามขอ URL เช่น /lists/1/add_item/ นั้น (.+) ของเราจะหาทั้งประโยคคือ ( 1/add_item/ ) และไปใช้ function view_list() ไม่ใช้ add_item() เพราะ (.+) เป็นตัวอักษรหรืออะไรก็ได้
จึงได้ทำการแก้ให้เป็น numerical digit (ตัวเลข) เท่านั้น
- จึงปรับ urls.py ที่เกี่ยวข้องให้เป็น (.d+) และ ใส่ path ของ add_item()
- สร้าง function add_item()
- run unit test ผ่าน
ต่อมาทำการแก้ action ใน list ให้เรียกใช้ add_item()
update unit test ทดสอบว่า list ที่ถูกส่งไปยัง template ถูก list หรือไม่
เมื่อ run unit test จะได้ KeyError: 'list' เพราะเราไม่ได้ส่ง list ไปยัง template
KeyError: 'list'
- จึงทำการแก้ view_list ให้เก็บค่า list_id ไว้ในตัวแปร list_ และส่งไปยัง template ด้วย
- นำ list_ ที่ได้มา มาชี้ที่ loop
run test ทั้งหมดจะผ่านทั้งหมด
เหมือนจะเสร็จแล้วแต่ในหนังสือเค้าบอกว่า เอา urls ของ app ไปใส่ไว้ใน urls.py ของ project มันไม่เป็นมืออาชีพ จึงทำการแก้ดังภาพด้านล่าง
ตอนนี้ก็เสร็จ Obey the Testing Goat! "Test-Driven Web Development with Python" ใน Part 1: The Basics of TDD and Django ทั้ง 7 บทเรียบร้อย
ตอนนี้ก็เสร็จ Obey the Testing Goat! "Test-Driven Web Development with Python" ใน Part 1: The Basics of TDD and Django ทั้ง 7 บทเรียบร้อย











































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