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ยังทำไม่ได้


































