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

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

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

Big Design Up Front (BDUF)

เป็นแนวทางการพัฒนา software ที่จะมีการออกแบบโปรแกรมให้เสร็จสิ้นก่อนจะเริ่มสร้างโปรแกรมขึ้นมา ซึ่งเกี่ยวข้องกับการพัฒนา software แบบ "Waterfall Model"
(ซึ่งตามหนังสืออธิบายว่าแนวทางของ TDD นั้นตรงข้ามกับ BDUF คือเป็นการพัฒนาที่มีความคล่องตัว นั่นคือการแก้ไขปัญหาจากการเขียนโค้ดและให้ได้โค้ดที่สามารถใช้งานจริงได้ในระดับนึง แล้วพัฒนาโปรแกรมนั้นตามการใช้งานจริงของ User (พัฒนาตาม feedback ของ User))
ศึกษาข้อมูลเพิ่มเติม :
http://en.wikipedia.org/wiki/Big_Design_Up_Front

Representational State Transfer (REST)

เป็นสถาปัตยกรรมที่ใช้สำหรับการแพร่กระจายสื่อ ที่ออกแบบมาให้มีความง่าย โดยการแลกเปลี่ยนข้อมูลกันระหว่าง HTTP และ XML โดยการร้องขอแบบ GET Request (การร้องขอโดยการส่ง attribute ของข้อมูลที่ต้องการแนบมากับ URL)
ศึกษาข้อมูลเพิ่มเติม :
http://java-thai-talk.blogspot.com/2012/01/restful.html
https://eddyatlab.wordpress.com/2009/06/11/rest-representational-state-transfer/

Key

การแก้ไขเปลี่ยนแปลงข้อมูล หรือการกำหนดความสัมพันธ์ระหว่างข้อมูลจะเกิดขึ้นอย่างมีประสิทธิภาพ จะต้องกำหนด คีย์ (Key) ให้กับ Table ก่อน นอกจากนี้การกำหนดคีย์จะทำให้การอ้างอิงและการประมวลผลข้อมูลได้สะดวกขึ้นและยังช่วยประหยัดเนื้อที่ในการจัดเก็บ
ศึกษาข้อมูลเพิ่มเติม :
http://www2.tsu.ac.th/cst/course/computer_it/database/key.html

สรุปการทดลองใช้งานคำสั่งตามหนังสือ TDD with Python

เริ่มต้นตามหนังสืออธิบายว่าตอนนี้ 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

เปลี่ยน Class ใน FT ให้รับ parameter จาก LiveServerTestCase
from django.test import LiveServerTestCase 
     [....]
    class NewVisitorTest(LiveServerTestCase):
และเปลี่ยนจากการเรียก localhost port 8000 เป็นใช้ attribute ของ LiveServerTestCase
    def test_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 homepage
        self.browser.get(self.live_server_url)
คำสั่งในการรัน FT ก็จะเปลี่ยนไปสั่งที่ manage.py
python3 manage.py test functional_tests
และคำสั่งที่จะรัน Unit Test ก็ต้องเปลี่ยนด้วยเนื่องจากมี FT เพิ่มเข้ามา จึงต้องเจาะจงไปที่ test ใน lists
python3 manage.py test lists

-------------------------------------------------------------------------------------------------------------------------------

ต่อไปเราจะใช้แนวคิดของ REST ซึ่งจะใช้ URL structure ในการทำงานในกรณีต่างๆ ของ app lists ของเรา
  • โดยการระบุว่าจะเป็น list ไหน (รองรับหลาย list) ให้แต่ละ list มี URL เป็นของตัวเอง (ส่งการร้องขอไปยัง server แบบ GET Request คือการส่ง attribute ระบุข้อมูลที่ต้องการจาก server บนส่ง URL)
/lists/<คำที่จะเจาะจงว่าเป็น list ไหน (ชื่อ list)>/
  • สร้าง list ใหม่ให้ส่งการร้องขอแบบ POST Request
/lists/new
  • เพิ่มข้อมูลไปยัง list ที่มีอยู่แล้วทาง POST Request
/lists/<คำที่จะเจาะจงว่าเป็น list ไหน (ชื่อ list)>/add_item

จากนั้นเราจะเพิ่มตัว FT ให้ตรวจสอบการใช้งานหลายๆ list
    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 table
    inputbox.send_keys(Keys.ENTER)
    edith_list_url = self.browser.current_url
    self.assertRegex(edith_list_url, '/lists/.+') #1
    self.check_for_row_in_list_table('1: Buy peacock feathers')

    # There is still a text box inviting her to add another item. She
    [...]
จะตรวจสอบว่า URL นี้ตรงกับ regular expression (ลำดับของการเขียน URL) หรือไม่

จากนั้นเราจะลอง test ให้สามารถใส่ข้อมูลได้หลาย list
    [...]
    # The page updates again, and now shows both items on her list
    self.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 #1
    self.browser.quit()
    self.browser = webdriver.Firefox()

    # Francis visits the home page.  There is no sign of Edith's
    # list
    self.browser.get(self.live_server_url)
    page_text = self.browser.find_element_by_tag_name('body').text
    self.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 URL
    francis_list_url = self.browser.current_url
    self.assertRegex(francis_list_url, '/lists/.+')
    self.assertNotEqual(francis_list_url, edith_list_url)

    # Again, there is no trace of Edith's list
    page_text = self.browser.find_element_by_tag_name('body').text
    self.assertNotIn('Buy peacock feathers', page_text)
    self.assertIn('Buy milk', page_text)

    # Satisfied, they both go back to sleep
เมื่อลองรัน FT แล้วจะ error ว่า
AssertionError: Regex didn't match: '/lists/.+' not found in
'http://localhost:8081/'
-------------------------------------------------------------------------------------------------------------------------------

ต่อไปหนังสือจะให้สร้างตัว Unit Test สำหรับการ Redirect เมื่อได้รับการร้องขอแบบ POST ให้ไปยัง
/lists/the-only-list-in-the-world/)เปลี่ยนจากกลับไปยัง home_page ('/')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
และไปเปลี่ยนที่ 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/')

    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})
ซึ่ง UT จะผ่านแต่ FT จะ error ว่า
    self.check_for_row_in_list_table('1: Buy peacock feathers')
[...]
selenium.common.exceptions.NoSuchElementException: Message: 'Unable to locate
element: {"method":"id","selector":"id_list_table"}' ; Stacktrace:
เนื่องจากยังไม่มี list item ชื่อว่า the-only-list-in-the-world

-------------------------------------------------------------------------------------------------------------------------------

ต่อไปเราจะใช้ Django test client ช่วย Unit Test ในการตรวจสอบ การ map URL ,การตรวจสอบ views และการตรวจสอบการ render template ของ views โดยการสร้าง Class ชื่อ
ListViewTest และย้ายฟังก์ชัน test_displays_all_items(self) มา
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 ไม่เจอว่า
AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404
เราจึงไปสร้าง URL ใหม่ที่ urls.py
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)),
)
รัน UT อีกครั้ง error ว่า import view_list ไม่ได้
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 ใน views.py
def view_list(request):
    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})
เมื่อรัน UT จะผ่านแต่รัย FT จะ error ว่า
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy
peacock feathers']
เนื่องจากเราใช้ template เดียวกันทั้งหน้า home page และ หน้าที่จะแสดง list (home.html)


ต่อไปจะทำการ refactoring เอา test ที่ไม่จำเป็นแล้วออกจาก unit test
ดูว่าในไฟล์นั้นมี class และ function อะไรบ้างใช้คำสั่ง
grep -E "class|def" lists/tests.py

เราจะลบ test_home_page_displays_all_list_items ออกเนื่องจากไม่จำเป็นแล้ว

-------------------------------------------------------------------------------------------------------------------------------

ต่อไปเราจะทำการแบ่ง template ของ home page กับ แสดง list ออกจากกัน
เริ่มต้นด้วยการสร้าง Unit Test ตรวจสอบว่าหน้าแสดง list ก็ใช้ template ของมันเอง
class ListViewTest(TestCase):

    def test_uses_list_template(self):
        response = self.client.get('/lists/the-only-list-in-the-world/')
        self.assertTemplateUsed(response, 'list.html')


    def test_displays_all_items(self):
        [...]
และไปเปลี่ยนที่ views.py ให้เรียก list.html
def view_list(request):
    items = Item.objects.all()
    return render(request, 'list.html', {'items': items})
เมื่อสร้าง test เสร็จจึงไปสร้าง template มา สร้างไฟล์ใหม่ใช้คำสั่ง
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 ให้สื่อความหมายว่าเป็นหน้าเริ่มต้น
<body>
    <h1>Start a new To-Do list</h1>
    <form method="POST">
        <input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
        {% csrf_token %}
    </form>
</body>
และไปแก้ที่ views.py ให้หลังจาก POST Request แล้วให้ redirect ไปหน้า list
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')
เมื่อรัน UT จะผ่าน  แต่ FT จะ error
จากนั้นแก้ที่ list.html ให้มี action ไปยัง home_page (ไม่ให้กลับมาที่เดิมตาม default ของมัน)
   <form method="POST" action="/">
เมื่อลองรัน FT จะ error ว่า
    self.assertNotEqual(francis_list_url, edith_list_url)
AssertionError: 'http://localhost:8081/lists/the-only-list-in-the-world/' ==
'http://localhost:8081/lists/the-only-list-in-the-world/'
แต่เราก็ได้ทำการ refactoring การเรียกใช้ template แยกกันเสร็จแล้ว

-------------------------------------------------------------------------------------------------------------------------------

ตอนนี้เรามี URL สำหรับแต่ละ list แต่ยังไม่มีการสร้าง list หลายๆ list ไว้เก็บ
ดังนั้นเริ่มต้นด้วยการสร้าง UT ใหม่ แต่ย้าย 2 ฟังก์ชั่นนี้มา
class NewListTest(TestCase):

    def test_saving_a_POST_request(self):
        request = HttpRequest()
        request.method = 'POST'
        [...]

    def test_redirects_after_POST(self):
        [...]
แล้วเราก็จะแก้ไขให้ใช้การทำงานของ Django test client
class NewListTest(TestCase):

    def test_saving_a_POST_request(self):
        self.client.post(
            '/lists/new',
            data={'item_text': 'A new list item'}
        )
        self.assertEqual(Item.objects.count(), 1)
        new_item = Item.objects.first()
        self.assertEqual(new_item.text, 'A new list item')


    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text': 'A new list item'}
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
สังเกตว่า URL ที่ไม่มี '/' ปิดท้ายจะหมายถึง URL ที่จะดำเนินการแก้ไขข้อมูลใน database

ลองรัน UT จะ error ว่า
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1
[...]
    self.assertEqual(response.status_code, 302)
AssertionError: 404 != 302
เพราะเรายังไม่มี URL ชื่อว่า list/new ดังนั้นไปเพิ่มที่ urls.py
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'^lists/new$', 'lists.views.new_list', name='new_list'),
    # url(r'^admin/', include(admin.site.urls)),
)
และเพิ่มที่ views.py ด้วย
def new_list(request):
    pass
UT จะ error ว่า "The view lists.views.new_list didn’t return an HttpResponse object" ดังนั้นต้องให้มัน return
def new_list(request):
    return redirect('/lists/the-only-list-in-the-world/')
เมื่อรันจะกลับไป error
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1
[...]
AssertionError: 'http://testserver/lists/the-only-list-in-the-world/' !=
'/lists/the-only-list-in-the-world/'
เราจึงไปเพิ่มฟังก์ชันใน views.py
def new_list(request):
    Item.objects.create(text=request.POST['item_text'])
    return redirect('/lists/the-only-list-in-the-world/')
และไปแก้การ redirect ใน list.py ให้ใช้ความสามารถของ Django test client
    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text': 'A new list item'}
        )
        self.assertRedirects(response, '/lists/the-only-list-in-the-world/') 
 เมื่อรัน UT ก็จะผ่าน

-------------------------------------------------------------------------------------------------------------------------------

ต่อไปก็จะไป refactoring ใน views.py ด้วยการลบส่วน if request.method == 'POST' ออก
def home_page(request):
    return render(request, 'home.html')

 ต่อไปเราจะแก้ไขให้ template ทั้งสองอันเชื่อมต่อกันด้วย list/new URL โดยไปแก้ทั้งสอง template เป็น
    <form method="POST" action="/lists/new">
-------------------------------------------------------------------------------------------------------------------------------

ต่อไปจะเป็นการปรับ model ของเราให้มีโมเดลที่รวบรวม list ด้วย โดยเริ่มแก้ไขที่ UT
@@ -3,7 +3,7 @@ from django.http import HttpRequest
 from django.template.loader import render_to_string
 from django.test import TestCase

-from lists.models import Item
+from lists.models import Item, List
 from lists.views import home_page

 class HomePageTest(TestCase):
@@ -60,22 +60,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_)

 เมื่อรัน UT ก็แน่นอนว่าจะไม่พบ List ดังนั้นจึงไปเพิ่มที่ models.py ให้มี Class ของ List
class List(models.Model):
    pass
 จะ error ว่า
django.db.utils.OperationalError: no such table: lists_list
 ดังนั้นเราก็จะไปสร้าง table ของ list ขึ้นมา ด้วย migrations เป็น table ของ list
python3 manage.py makemigrations
 และเมื่อลองรันอีกครั้งจะ error ว่า
    self.assertEqual(first_saved_item.list, list_)
AttributeError: 'Item' object has no attribute 'list'
 เนื่องจาก table ของ Item และ list นั้นยังไม่มีความสัมพันธ์ซึ่งกันและกัน ดังนั้นเราจะเพิ่มให้มี
Foreign Key ซึ่งจะเชื่อม table ให้มีความสัมพันธ์กัน โดยการแก้ดังนี้
from django.db import models

class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.TextField(default='')
และสร้าง migrations ใหม่สำหรับ list โดยจะ add field list เข้าไปใน Item
python3 manage.py makemigrations
แต่ UT error ว่า
AssertionError: 'List object' != <List: List object>
 ซึ่งต้องบอก Django ว่า 2 Class นี้สัมพันธ์กัน
from django.db import models

class List(models.Model):
    pass


class Item(models.Model):
    text = models.TextField(default='')
    list = models.ForeignKey(List, default=None)
 ลบ migrations แล้วสร้างใหม่
rm lists/migrations/0004_item_list.py
python3 manage.py makemigrations
แต่ UT จะ error ว่า
ERROR: test_displays_all_items (lists.tests.ListViewTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id
[...]
ERROR: test_saving_a_POST_request (lists.tests.NewListTest)
django.db.utils.IntegrityError: NOT NULL constraint failed: lists_item.list_id

Ran 7 tests in 0.021s

FAILED (errors=3)
โดยจะต้องมีการสร้าง list ให้ตอนที่ test การใส่ item ด้วย
class ListViewTest(TestCase):

    def test_displays_all_items(self):
        list_ = List.objects.create()
        Item.objects.create(text='itemey 1', list=list_)
        Item.objects.create(text='itemey 2', list=list_)
และแก้ใน views.py
from lists.models import Item, List
[...]
def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/the-only-list-in-the-world/')
เมื่อรัน Unit Test ก็จะผ่าน

-------------------------------------------------------------------------------------------------------------------------------

ต่อไปเราจะทำให้แต่ละ list มี URL เป็นของตัวเองโดยเป็นชื่อของ list นั่นเอง โดยแก้ที่ unit test
class ListViewTest(TestCase):

    def test_uses_list_template(self):
        list_ = List.objects.create()
        response = self.client.get('/lists/%d/' % (list_.id,))
        self.assertTemplateUsed(response, 'list.html')


    def test_displays_only_items_for_that_list(self):
        correct_list = List.objects.create()
        Item.objects.create(text='itemey 1', list=correct_list)
        Item.objects.create(text='itemey 2', list=correct_list)
        other_list = List.objects.create()
        Item.objects.create(text='other list item 1', list=other_list)
        Item.objects.create(text='other list item 2', list=other_list)

        response = self.client.get('/lists/%d/' % (correct_list.id,))

        self.assertContains(response, 'itemey 1')
        self.assertContains(response, 'itemey 2')
        self.assertNotContains(response, 'other list item 1')
        self.assertNotContains(response, 'other list item 2')
และแก้ที่ urls.py
urlpatterns = patterns('',
    url(r'^$', 'lists.views.home_page', name='home'),
    url(r'^lists/(.+)/$', 'lists.views.view_list', name='view_list'),
    url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
    # url(r'^admin/', include(admin.site.urls)),
)
โดยในตำแหน่งของ (.+) จะใส่เป็นชื่อของ list ที่จะรับมาจากการ parameter ของ views.py
def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    items = Item.objects.filter(list=list_)
    return render(request, 'list.html', {'items': items})
และไปแก้ที่ UT ในการตรวจสอบการ redirect ว่า
    def test_redirects_after_POST(self):
        response = self.client.post(
            '/lists/new',
            data={'item_text': 'A new list item'}
        )
        new_list = List.objects.first()
        self.assertRedirects(response, '/lists/%d/' % (new_list.id,))
เมื่อรัน Unit Test จะผ่าน แต่ Functional Test จะ error ว่า
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use
peacock feathers to make a fly']
-------------------------------------------------------------------------------------------------------------------------------

ต่อไปจะเป็นการเพิ่ม item ที่รับมาไปยัง list ที่ต้องการ โดยเริ่มจาก UT
class NewItemTest(TestCase):

    def test_can_save_a_POST_request_to_an_existing_list(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        self.client.post(
            '/lists/%d/add_item' % (correct_list.id,),
            data={'item_text': 'A new item for an existing list'}
        )

        self.assertEqual(Item.objects.count(), 1)
        new_item = Item.objects.first()
        self.assertEqual(new_item.text, 'A new item for an existing list')
        self.assertEqual(new_item.list, correct_list)


    def test_redirects_to_list_view(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()

        response = self.client.post(
            '/lists/%d/add_item' % (correct_list.id,),
            data={'item_text': 'A new item for an existing list'}
        )

        self.assertRedirects(response, '/lists/%d/' % (correct_list.id,))
จะ error
AssertionError: 301 != 302 : Response didn't redirect as expected: Response
code was 301 (expected 302)
เราจะแก้ที่ urls.py โดยใช้ regular expression \d
    url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
ก็จะเปลี่ยนมาเป็น
AssertionError: 404 != 302 : Response didn't redirect as expected: Response
code was 404 (expected 302)
เนื่องจากไม่พบหน้า add_item โดยจะเพิ่ม
urlpatterns = patterns('',
    url(r'^$', 'lists.views.home_page', name='home'),
    url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),
    url(r'^lists/(\d+)/add_item$', 'lists.views.add_item', name='add_item'),
    url(r'^lists/new$', 'lists.views.new_list', name='new_list'),
    # url(r'^admin/', include(admin.site.urls)),
)
และจะต้องไปเพิ่ม add ที่ views ด้วย
def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/%d/' % (list_.id,))

และในหน้า template เราอยากให้มีการ action ที่ add_item ก็แก้ที่ form ว่า
    <form method="POST" action="/lists/{{ list.id }}/add_item">
และสร้าง UT ใหม่เพื่อทดสอบว่า list ที่ list template นั้นถูกอัน
    def test_passes_correct_list_to_template(self):
        other_list = List.objects.create()
        correct_list = List.objects.create()
        response = self.client.get('/lists/%d/' % (correct_list.id,))
        self.assertEqual(response.context['list'], correct_list)
และแก้ไขที่ views.py
def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    return render(request, 'list.html', {'list': list_})
จะ error ว่า
AssertionError: False is not true : Couldn't find 'itemey 1' in response

 โดยจะแก้ที่ list template ให้เอาข้อมูลมาจาก list
   <form method="POST" action="/lists/{{ list.id }}/add_item">

    [...]

        {% for item in list.item_set.all %}
            <tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
        {% endfor %}
 เมื่อรัน UT และ FT จะผ่าน

-------------------------------------------------------------------------------------------------------------------------------

สุดท้ายจะเป็นการแยก URL ที่เกี่ยวข้องกับหน้า home_page และหน้า list ออกจากกัน

โดยจะย้ายของ list ซึ่งเป็นของ app ไปไว้ใน app lists
cp superlists/urls.py lists/

urls.py ของ home_page ใน superlists
urlpatterns = patterns('',
    url(r'^$', 'lists.views.home_page', name='home'),
    url(r'^lists/', include('lists.urls')),
    # url(r'^admin/', include(admin.site.urls)),
)
urls.py ของ list ใน lists
from django.conf.urls import patterns, url

urlpatterns = patterns('',
    url(r'^(\d+)/$', 'lists.views.view_list', name='view_list'),
    url(r'^(\d+)/add_item$', 'lists.views.add_item', name='add_item'),
    url(r'^new$', 'lists.views.new_list', name='new_list'),
)


ศึกษาข้อมูลเพิ่มเติม :
http://chimera.labs.oreilly.com/books/1234000000754/ch06.html#_a_final_refactor_using_url_includes

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

GIT - การนำกลับข้อมูลที่แก้ไขแล้วในรูปแบบต่างๆ


Data Recovery

รูปแบบนี้จะเป็นการเลือกย้อนไปยังการ commit ที่เราต้องการและล้างการแก้ไขก่อนหน้าการ commit ครั้งนั้นทั้งหมด

คำสั่งเรียกดูประวัติการ commit ของ git
git log --pretty=oneline
เมื่อใช้คำสั่งแล้วจะแสดงประวัติการ commit โดยจะแสดงตัวเลขฐาน 16 และ comment ที่เรา commit ไว้


คำสั่งกู้คืนการแก้ไขข้อมูลที่เคย commit ไว้ เป็นการย้อนสถาณะของไฟล์ทั้งหมดกลับไปเหมือนตอนที่ commit ครั้งนั้นไว้
git reset --hard 8436f9b2b51283f5088f2de488b58282c7a5e432
โดยใส่ตัวเลขด้านหน้า commit ที่เราต้องการกู้คืน
เมื่อใช้คำสั่งแล้วจะเห็นว่า commit ล่าสุดจะเป็น commit ที่เราต้องการ

ศึกษาเพิ่มเติม :
http://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery#_data_recovery


Unmodifying a Modified File

รูปแบบนี้จะเป็นการล้างการแก้ไขก่อนหน้าการ commit ครั้งล่าสุด

ลองทำการแก้ไขไฟล์ที่ได้ commit ไปแล้ว (functional_tests.py) ก่อนหน้าการ commit จะเป็นลักษณะดังภาพ

ทดลองใส่ comment code ส่วนฟังก์ชััน setUp และ tearDown (ทำการแก้ไขไฟล์)

เช็ค git status จะเห็นว่าไฟล์ functional_tests.py ถูกแก้ไข และบอกว่าการแก้ไขนั้นยังไม่ได้ถูก commit ไปยัง git

หากต้องการให้เนื้อหาของไฟล์กลับไปเป็นเหมือนก่อนหน้าที่ commit ใช้คำสั่ง
git checkout -- ชื่อไฟล์
เมื่อลองใช้คำสั่งและลองเช็ค git status จะเห็นว่าไม่มีการแก้ไขไฟล์ functional_tests.py แล้ว

เมื่อลองดูไฟล์ functional_tests.py จะเห็นว่าไม่มีการ comment code เหมือนกับก่อนที่จะทำการ commit

ศึกษาเพิ่มเติม :
http://git-scm.com/book/en/v2/Git-Basics-Undoing-Things 


Branches

รูปแบบนี้จะเป็นการสร้าง "กิ่ง" แยกออกไปเพื่อแก้ไขเพิ่มเติม และเมื่ออยากกลับมาเริ่มแก้ไขที่ commit เก่าๆก็สามารถสร้างกิ่งใหม่ออกไปอีกทาง โดยจะไม่มีผลกระทบต่อกัน และไม่มีผลกระทบต่อ commit เดิมด้วย

เริ่มต้นด้วยการดูการ commit ครั้งล่าสุดด้วยคำสั่ง
git log --oneline --decorate
โดยคำสั่ง --decorate จะแสดงกิ่งและตัวชี้(pointer)ว่าขณะนี้อยู่ที่ commit ไหน โดยกิ่งที่ git จะสร้างขึ้นมาตั้งแต่เริ่มต้นจะชื่อว่า master ซึ่งจะอยู่ที่ commit ครั้งล่าสุดของเรา และมีตัวชี้ชื่อว่า HEAD 

จากนั้นเราจะสร้างกิ่งใหม่ชื่อว่า testing โดยใช้คำสั่ง
git branch testing
จะได้กิ่งใหม่ขึ้นมาอยู่ตำแหน่งเดียวกับกิ่งที่เราทำงานด้วยอยู่ปัจจุบันคือ master

จากนั้นเราจะย้ายตัว pointer ไปยังกิ่ง testing เพื่อเริ่มทำงานที่กิ่ง testing ใช้คำสั่ง
git checkout testing


ทดลองทำการสร้างไฟล์ชื่อว่า new_file.py

ทำการ add ไฟล์นี้ไปยัง git และทำการ commit การแก้ไขนี้ว่า 'made a change'

เมื่อลองใช้คำสั่ง git log --oneline --decorate อีกครั้งจะเห็นว่ามีการ commit เพิ่มขึ้นมา
สังเกตว่าตัวกิ่ง master ไม่ได้ตามมาด้วย

เมื่อลองกลับไปให้ HEAD ชี้ที่ master ใช้คำสั่ง
git checkout master
จะเห็นว่าไฟล์ new_file.py หายไป

เมื่อลองกลับมาที่ testing ไฟล์ new_file.py ก็จะกลับมา

ศึกษาเพิ่มเติม :
http://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell

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