วันจันทร์ที่ 23 กุมภาพันธ์ พ.ศ. 2558

Chapter 6 : Getting to the Minimum Viable Site


ทำการสร้าง directory “functional_tests” ซึ่งภายในจะมีไฟล์ "__init__.py” ด้วยคำสั่ง
$ mkdir
functional_tests
$ touch
functional_tests/__init__.py
และทำการ move functional tests เข้าไปใน directory ที่เราสร้างขึ้น และเปลี่ยนชื่อเป็น tests.py ด้วยคำสั่ง
$ git mv
functional_tests.py functional_tests/tests.py
$ git
status
เมื่อรัน tree จะได้
.
├── 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
จากนั้นทำการ แก้ไข functional_tests/tests.py และทำการเปลี่ยน NewVisitorTest class เป็น LiveServerTestCase
from django.test import LiveServerTestCase
from selenium import webdriver 
from selenium.webdriver.common.keys import Keys
class NewVisitorTest(LiveServerTestCase):
    def setUp(self):
        […]
จากนั้นทำการเปลี่ยน localhost เป็น live_server_url 
 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


ทำการ commit ไว้
$ git status #
functional_tests.py renamed + modified, new __init__.py
$ git add functional_tests
$ git diff --staged -M
$ git
commit  # msg eg "make functional_tests an app, use
LiveServerTestCase"
จากนั้นทำการรัน Unit Test






และเราสามารถรันเฉพาะบางส่วนได้ ยกตัวอย่างให้รัน lists






Useful Commands Updated



To run the functional tests
python3 manage.py test functional_tests
To run the unit tests 
       python3 manage.py test lists
 
ทำการเข้าไปเพิ่มในส่วนท้ายของ ไฟล์ functional_tests/tests.py ในส่วน def test_can_start_a_list_and_retrieve_it_later(self): ว่า

    # Edith wonders whether the site will remember her list. Then she sees
    # that the site has generate 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.
 
    # Satisfied, she goes back to sleep

REST

Idea ของ data structure คือ Model-View-Controller (MVC) สำหรับในแต่ละ lists และ lists item ก็จะมี URL ของมันเอง
    /lists/<list identifier>/
ถ้าต้องการสร้าง list ใหม่ ก็จะมี URL พิเศษที่มันสามารถ accept POST request ได
    /lists/new
ถ้าต้องการสร้าง list item ใหม่จาก list ที่มีอยู่แล้ว เราจะมีการ separate URL ซึ่งเราจะสามารถส่ง POST request ได้

    /lists/<list identifier>/add_item
**เราแค่จะใช้แนวคิดของ REST มาใช้**

Implementing the New Design Using TDD

ให้มองหา inputbox.send_keys('Buy peacock feathers') ใน 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 table
    inputbox.send_keys(Keys.ENTER)
    edith_list_url = self.browser.current_url
    self.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
    [...]
และแก้ไขเพิ่มเติม

    [...]
    # 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 
    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


Commit ไว้ก่อน 
 $ git commit -a

Iterating Towards the New Design

ทำการเปลี่ยนแปลง locationใน lists/tests.py หาในส่วนของ test_home_page_redirects_after_POST
 self.assertEqual(response.status_code, 302)
 self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')
และทดลองรัน UT











ให้ทำการเข้าไปแก้ไข home_page redirect ที่ 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/')

    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})
เมื่อรัน Unit Test จะ OK แต่ถ้ารัน FT จะพบว่า URL /the-only-list-in-the-world/ ไม่ได้มีอยู่จริง

Testing Views, Templates, and URLs Together with the Django Test Client

A New Test Class

ให้ทดลอง test client โดยเปิด lists/tests.py แล้ว copy method test_home_page_displays_all_ list_items จาก HomePageTest แล้วทำการเปลี่ยนแปลงค่าเล็กน้อย

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/')
 
       
        self.assertContains(response, 'itemey 1') 

        self.assertContains(response, 'itemey 2') 
ทดลอง รัน UT


จะพบว่า 404 != 200 ซึ่งเราจะต้องไปเพิ่มในส่วนของ URLs

A New URL

เข้าไปที่ superlists/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)),
)
รัน Unit Test อีกครั้ง


จะพบว่า จะต้องมี view function เพิ่มเติมด้วย

A New View Function

ทำการสร้าง dummy view function ใน lists/views.py:

def view_list(request):
    pass
ถ้าพอจำได้ การส่งค่า pass ธรรมดา จะรันไม่ได้ เพราะต้องการ HttpRespone ให้ทำการเพิ่มเติมส่วน return

def view_list(request):
    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})
ทำการรัน Unit Test


และทดลองรัน FT


Green? Refactor

รันคำสั่ง $ grep -E "class|def" lists/tests.py


และเข้าไปทำการลบ test_home_page_displays_all_list_items method ใน lists/tests.py และทดลองรัน manage.py test lists


A Separate Template for Viewing Lists

ทดลองรัน Test ใหม่เพื่อตรวจสอบการใช้งาน template และให้เข้าไปเพิ่มเติมใน  lists/views.py.

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):
        [...]
เมื่อทดลองรันทดสอบ assert จะได้ว่า

AssertionError: False is not true : Template 'list.html' was not a template
used to render the response. Actual template(s) used: home.html
และให้เข้าไปแก้ไขใน lists/views.py. อีกที
def view_list(request):
    items = Item.objects.all()
    return render(request, 'list.html', {'items': items})
ทดลองรัน UT จะได้

django.template.base.TemplateDoesNotExist: list.html
จากนั้นให้เข้าไปทำการสร้างไฟล์ใหม่ ที่ lists/templates/list.html

$ touch lists/templates/list.html
และทำการ copy ไฟล์ home.html ไปเป็น list.html

cp lists/templates/home.html lists/templates/list.html
ทดลองรัน UT จะเห็นว่ารันผ่าน และทำการเปลี่ยนแปลงภายในไฟล์ lists/templates/home.html.

<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>
และทดลองรัน UT อีกครั้ง ก็ยังผ่านเหมือนเดิม ให้เข้าไปแก้ไขใน 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')
รัน UT อีกครั้งก็จะยัง รันผ่านเหมือนเดิม และให้ทดลองรัน FT






เรายังติดปัญหาที่ input ตัวที่ 2 สามารถแก้ไขได้โดยเข้าไปที่ lists/templates/list.html:

    <form method="POST" action="/">
และทดลองรัน FT อีกครั้ง






โอเค เราได้ URL ที่ไม่ซ้ำกันแล้วสำหรับ 1 list ของเรา และต่อไปให้ทำการ git commit ไว้ก่อน

$ git status # should show 4 changed files and 1 new file, list.html
$ git add lists/templates/list.html
$ git diff # should show we've simplified home.html,
           # moved one test to a new class in lists/tests.py added a new view
           # in views.py, and simplified home_page and made one addition to
           # urls.py
$ git commit -a # add a message summarising the above, maybe something like
                # "new URL, view and template to display lists"

Another URL and View for Adding List Items

A Test Class for New List Creation

เปิด lists/tests.py, แล้วทำการเพิ่มเติม class

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/')

A URL and View for New List Creation

เปิด superlists/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)),
)



เปิด lists/views.py. และแก้ไข

def
new_list(request):
    return
redirect('/lists/the-only-list-in-the-world/')



จะได้ว่า




เข้าไป lists/views.py. และแก้ไขอีกครั้ง

def
new_list(request):
   
Item.objects.create(text=request.POST['item_text'])
    return
redirect('/lists/the-only-list-in-the-world/') 



จะพบว่า
AssertionError:
'http://testserver/lists/the-only-list-in-the-world/' !=
'/lists/the-only-list-in-the-world/'
-
http://testserver/lists/the-only-list-in-the-world/
? -----------------
+
/lists/the-only-list-in-the-world/
ให้เข้าไปแก้ไขที่ lists/tests.py. เปลี่ยนเป็น AssertRedirect

    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/')

ผลที่ได้ 
Ran 8 tests in 0.018s

OK

Removing Now-Redundant Code and Tests

เราสามารถ remove if request.method == 'POST' ได้หรือเปล่า (สิ่งที่ซับซ้อน) เข้าไปใน lists/views.py.
 
def
home_page(request):
    return
render(request, 'home.html')
ผลที่ได้
Ran 8 tests in 0.017s

OK


และเรายังสามารถ remove test_home_page_only_saves_ items_when_necessary test ได้อีกด้วย
ผลที่ได้
Ran 7 tests in 0.016s

OK

Pointing Our Forms at the New URL

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

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