วันพฤหัสบดีที่ 5 กุมภาพันธ์ พ.ศ. 2558

Test-Driven Development with Python - สรุป Chapter. 5

คำสำคัญที่พูดถึงในหนังสือ TDD with Python

POST Request
Request ในที่นี้หมายถึงการร้องขอข้อมูลระหว่าง 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
<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">
ทดลองรัน functional test

เมื่อลองรัน functional test จะเกิด error HTTP 403 เนื่องจากในการป้องกัน CSRF แต่ละ form ของเว็บเพจจะต้องมีการวาง token ไว้ให้รู้ว่าเป็นการส่งค่าจากหน้าเว็บเดิม โดยใส่ Django Template Tag เพิ่มใน home.html ดังนี้
<form method="POST">
    <input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
    {% csrf_token %}
</form>
ซึ่งก็จะมี error ว่ายังไม่มี to-do item ใน table เนื่องจากยังไม่มีการส่งค่าอะไร แต่ก็แสดงว่าเราได้ทำการป้องกัน CSRF สำเร็จแล้ว

เริ่มต้นการประมวลผล POST Request บน Server

เราจะเริ่มต้นด้วยการสร้าง Unit Test สำหรับการประมวณผล POST Request บน Server เริ่มด้วย Test ว่าหน้าเว็บ home.html สามารถบันทึก POST Request จากผู้ใช้ได้แล้ว return มา แล้วดูว่ามีค่าที่มาด้วยหรือไม่ (เพิ่มไปยัง test.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.assertIn('A new list item', response.content.decode())
 จากที่เห็นข้างต้นตามหนังสือจะอธิบายว่าการเขียน Unit Test นั้นจะต้องประกอบไปด้วย  3 ส่วนได้แก่
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 ดังนี้
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')
ซึ่งก็สามารถรันผ่านได้แต่ยังเป็นโค้ดที่รันค่าคงที่และ test ด้วยค่าคงที่อยู่ซึ่งเราจะต้อง refactoring ต่อไป

เริ่มต้นส่งค่าตัวแปรจากโค้ด Python ไป render บน Template

 การแสดงค่าตัวแปรจากโค้ด python บน template html  Django Template Tag จะใช้สัญลักษณ์ {{ชื่อตัวแปร}}
<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>
โดยการส่งค่ามายัง template จากตัว Unit Test นั้นจะใช้คำสั่ง render_to_string
    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)
ตัว parameter แรกคือ template ที่จะส่งไป ตัวหลังคือการ map ระหว่างชื่อของตัวแรก ('new_item_text') กับค่าของตัวแปร ('A new list item')
แล้วเปลี่ยน view.py เป็นคำสั่งดังนี้
def home_page(request):
    return render(request, 'home.html', {
        'new_item_text': request.POST['item_text'],
    })
แต่เกิด error ที่ไม่คาดคิด เกิด KeyError: 'item_text' ตามหนังสือจึงให้เราแก้ไขเป็น
def home_page(request):
    return render(request, 'home.html', {
        'new_item_text': request.POST.get('item_text', ''),
    })
เมื่อรัน Unit Test อีกครั้งก็จะผ่าน แต่เมื่อลองรัน Functional Test นั้น error ว่า ยังไม่มี item ใน table เราต้องการข้อมูลมากกว่านี้จึงเข้าสู่เทคนิคการ debugging FT คือ การแก้ให้ error message แสดงรายละเอียดมากขึ้น เช่น print สิ่งที่มีอยู่ใน table
    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 อยู่จึงได้ลองเปลี่ยนจาก assertTrue เป็น arssertIn
    self.assertIn('1: Buy peacock feathers', [row.text for row in rows])

แต่ยังเกิด error ว่า
self.assertIn('1: Buy peacock feathers', [row.text for row in rows])
AssertionError: '1: Buy peacock feathers' not found in ['Buy peacock feathers']
เนื่องจากค่าตัวแปรที่เราจะส่งให้ template render จาก functional test นั้นมันไม่มี '1: ' อยู่ด้วย เราจึงเพิ่มใน template ให้มีการแสดง '1: ' ด้วย
 <tr><td>1: {{ new_item_text }}</td></tr>
เมื่อลองรัน functional test ก็จะผ่าน
เมื่อลองเพิ่มการส่งค่าให้ 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 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.
ก็จะ error ว่า
AssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock
feathers to make a fly']
เนื่องจากตัว template เรายังไม่รองรับ '2: ' ซึ่งจะต้อง refactoring ต่อไป
ตามหนังสือก็จะอธิบายว่าเป็นการทดสอบที่เรียกว่า 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 เป็นฟังก์ชันดังนี้
    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])
สังเกตว่าหากฟังก์ชันไม่ได้ขึ้นต้นด้วย test ก็จะไม่เรียกใช้ว่าเป็นฟังก์ชันสำหรับ functional test แต่เป็นฟังก์ชันที่เอาไว้เรียกใช้ซ้ำ
    # 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
    [...]
ซึ่งผลที่ได้ก็ยังเหมือนเดิมคือ error ว่า
AssertionError: '1: Buy peacock feathers' not found in ['1: Use peacock
feathers to make a fly']
 เนื่องจากตัว template เรายังไม่รองรับ '2: '

การสร้าง Model ของ Database ด้วย Django ORM

ก่อนที่ะเริ่มสร้าง เหมือนทุกครั้งเราจะต้องสร้างตัว Unit Test ก่อน โดยใช้ฟังก์ชันนี้ใน test.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')
จากฟังก์ชันด้านบนก็จะมีการสร้าง save ค่า 2 ค่า ลงไปใน item และมีการ assert ว่าใน item มีข้อมูลอยู่ 2 ตัว และข้อมูลทั้ง 2 ตัวคือ '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 ทดลองใส่โค้ดตามนี้
from django.db import models

class Item(object):
    pass
เมื่อทำการรันจะได้ error ว่า Item ไม่มี attribute ชื่อว่า save
การที่เราจะสร้าง Item นี้ให้เป็น model ที่มี attribute ตามที่ model ของ Django ควรมีทำได้โดย สืบทอด(inherit)มาจาก class model ของ Django โดยแก้เป็นโค้ดดังนี้
from django.db import models

class Item(models.Model):
    pass
โดย Class ที่เราสร้างขึ้นมาจะเป็น Database table, ตัว atrribute ชนิดต่างๆ จะเป็น column และค่าที่เราเก็บนั้นจะอยู่เป็น row ใน database

เมื่อลองรัน Unit Test ก็จะขึ้นว่า Database error
django.db.utils.OperationalError: no such table: lists_item
 เนื่องจากการสร้าง Django ORM เป็นเพียงแค่การ model Database แต่ยังไม่ได้เป็น Database จริงๆ ดังนั้นจึงมีอีกขั้นตอนนึงในการสร้าง Database เรียกว่า Migrations ซึ่งจะเป็นตัวช่วยในการจัดการกับ Database ทั้งการเพิ่มข้อมูลเข้า การดึงข้อมูลออก แม้แต่การทำตัวเป็นเหมือน Version Control สำหรับ database สามารถเรียกคือข้อมูลเดิมได้
โดยการสร้าง Migrations ใช้คำสั่ง
python3 manage.py makemigrations
จะสร้าง Migrations สำหรับ app 'lists' ของเรา โดยจะสร้างไฟล์ 0001_initial.py เป็น Migrations เริ่มต้น สำหรับ model Item
เมื่อลอง test app 'list' ใช้คำสั่ง
python3 manage.py test lists
จะ error ว่า
AttributeError: 'Item' object has no attribute 'text'
คือไม่มี attribute 'text' สำหรับ model Item ของเรา
เนื่องจาก Django ไม่รู้ว่าเรามี table ที่เก็บเป็น text อยู่ใน Item ของเรา เนื่องจาก Class Item ที่เราสือทอดมาจาก Class models ตอนแรกจะสร้างให้แค่ column หลักแรกใน Database ไว้ให้เราเท่านั้นซึ่ง id attribute แต่ถ้าหากเราอยากได้ column ชนิดอื่นๆ เราต้องสร้างขึ้นมาเอง โดยในที่นี้เราจะสร้าง text field
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
โดยจะขึ้นว่า
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
class Item(models.Model):
    text = models.TextField(default='')
ใช้คำสั่งสร้าง Migrations อีกครั้ง คราวนี้สามารถสร้างได้เป็นไฟล์ 0002_item_text.py
เมื่อลอง test app 'list' ใช้คำสั่ง python3 manage.py test lists จะรันผ่าน คือสามารถส่งข้อมูลไปยัง database ได้

บันทึกข้อมูลจาก POST Request ลง Database

ก่อนอื่นสร้างตัว Unit Test สำหรับทดสอบการบันทึกข้อมูลก่อน ใน test.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)
มีการเช็คจำนวนข้อมูล ตรวจสอบข้อมูลตัวแรกว่าเหมือนกับ'A new list item' ไหม

ตามหนังสือนั้นอธิบายว่าเราควรมีการสังเกตโค้ด Unit Test ว่ามันยาวไปหรือไม่ เพราะ Unit Test เป็นการ test แยกเป็นส่วนๆ หากยาวเกินไปอาจมีการ test ที่ซ้ำซ้อน หรือ มีการ test หลายส่วนรวมอยู่ในตัว test ตัวเดียวกัน เพื่อเป็นการให้ test มีประสิทธิภาพจึงไม่ควรให้ Unit Test ยาวเกินไป

เมื่อลองรัน Unit test จะ error ว่า
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1
ตามหนังสือให้ปรับ views.py ให้เข้ากับตัว test
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
    })
จะเห็นว่าส่วนของค่าตัวแปรมีการรับค่าเข้ามาจริงๆ

ขั้นต่อมาจะ refactoring ให้ไม่มีการรับค่า ' ' (blank) เข้ามาใน database โดยการเพิ่ม Unit Test
class HomePageTest(TestCase):
    [...]

    def test_home_page_only_saves_items_when_necessary(self):
        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
    })
เปลี่ยนจากการส่งค่า blank ['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 การส่งข้อมูลกลับ
    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'], '/')
การ Redirect ควรมี HTTP status code 302 แต่เมื่อรันแล้วมัน error 200
ตามหนังสือจึงให้ไปปรับในไฟล์ 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')
เมื่อรัน test ก็จะผ่าน

Better Unit Testing Practice: Each Test Should Test One Thing

ตามที่กล่าวไป การ test Unit Test ที่ดีควรสั้น และแยกเป็นส่วนๆ เพื่อให้รู้ได้ชัดเจนว่า bug ที่ตรงไหน จะทำให้ debug ได้ถูกจุด
หนังสือก็ได้แก้เป็นดังนี้
    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
เมื่อรันก็ควรจะผ่าน

ต่อไปหนังสือต้องการทำให้ app lists นี้สามารถแสดงข้อมูลทั้งหมดที่อยู่ใน database
ดังนั้นเริ่มจากการสร้าง Unit Test ก่อน
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
AssertionError: 'itemey 1' not found in '<html>\n    <head>\n [...]
ต่อไปจะเป็นการแก้ template ให้สามารถแสดงผลได้หลายๆแถว ในตารางโดยการใช้ Template Tag for loop เพื่อสร้างตารางที่แสดงข้อมูลใน lists ทั้งหมด
<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
AssertionError: 'To-Do' not found in 'OperationalError at /'
ตามหนังสือให้วิธีการ Debug Functional Test อีกเทคนิคหนึ่งคือให้เข้าไปที่ http://localhost:8000 

จะเห็นว่า "no such table: lists_item"

สร้าง Database with migrate

Database ที่เรามีนั้นเป็นเพียงแค่ Class ยังไม่ได้มีการบันทึกค่าลงบนไฟล์ใดจริงๆ ในการสร้าง Database ใช้คำสั่ง
python3 manage.py migrate
จะมีการบันทึก Database ไว้ในไฟล์ db.sqlite3
เมื่อลองรัน 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 ในการช่วยดังนี้
    {% 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

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

แสดงความคิดเห็น