วันอาทิตย์ที่ 22 กุมภาพันธ์ พ.ศ. 2558

Chapter 6. Getting to the Minimum Viable Site

Ensuring Test Isolation in Functional Tests

$ mkdir functional_tests
$ touch functional_tests/__init__.py 
 
สร้าง Folder functional_tests และข้างในมีไฟล์ __init__.py 
 
$ git mv functional_tests.py functional_tests/tests.py

ย้าย functional_tests.py ไปอยู่ใน functional_tests และเปลี่ยนชื่อเป็น test.py

├── 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

from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

class NewVisitorTest(LiveServerTestCase):

    def setUp(self):
        [...]
 
แก้ตรง
 
    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) 
 

 
git status
git add functional_tests
git diff --staged -M
git commit
 

Running Just the Unit Tests

ตอนนี้ถ้าเรา run manage.py test Django จะ run ทั้ง functional และ unit tests ทั้งคู่เลย
 
 
 แต่ถ้าอยากรันแค่ unit tests ให้ใช้คำสั่ง python3 manage.py test lists
 
 
คำสั่งในการรัน FT จะเปลี่ยนเป็น
python3 manage.py test functional_tests
คำสั่งในการรัน Unit Test จะเปลี่ยนเป็น
python3 manage.py test lists

Small Design When Necessary 


เราต้องการสร้าง list ใหม่ เพื่อใส่itemเข้าไป1อย่าง และใช้ url สำหรับ 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 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 [...]


เมื่อมีผู้ใช้รายใหม่เข้ามาใช้เราต้องตรวจสอบว่า พวกเขาจะไม่เห็นรายการใดๆของ Edith’s items เมื่อเข้ามาแล้วต้องได้รับ url ที่เป็นเอกลักษณ์ของตนเอง

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



Iterating Towards the New Design

สร้าง test ให้ redirect location หลังจากได้รับ POST


lists/tests.py

self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], '/lists/the-only-list-in-the-world/')




และแก้ใน 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})


จะ error ว่า เนื่องจากยังไม่มี list item ชื่อว่า the-only-list-in-the-world

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

ในบทนี้เราจะใช้ unit tests เช็คความละเอียดของ URL และเช็ค views render templates ถูกต้องหรือไม่

A New Test Class

ไปที่ 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

 
A New URL

URL ของเรายังไม่ได้สร้างจึงไปแก้ไขใน superlists/urls.py

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)),
)


A New View Function

lists/views.py

def view_list(request):
    pass

 
lists/views.py

def view_list(request):
    items = Item.objects.all()
    return render(request, 'home.html', {'items': items})






Green? Refactor


เราสามารถลบ test_home_page_displays_all_list_items method เพราะมันไม่จำเป็น

A Separate Template for Viewing Lists

lists/tests.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):
        [...]


lists/views.py

def view_list(request):
    items = Item.objects.all()
    return render(request, 'list.html', {'items': items}) 



สร้างไฟล์ใหม่ที่ lists/templates/list.html ใช้คำสั่ง $ touch lists/templates/list.html



$ cp lists/templates/home.html lists/templates/list.html

home pageไม่จำเป็นต้องแสดงlist items มันเพียงต้องการ list inputใหม่ เพื่อให้เราสามารถลบบรรทัดจาก lists/templates/home.html และอาจปรับแต่งh1 ที่จะบอกว่า "Start a new To-Do list"
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> 


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



การรับ input item ตัวที่สองยังผิดอยู่ เพราะว่ารูปแบบใหม่ action= attribute หายไป เลยเป็นค่า default หมายความว่ามันจะส่งไปยัง url เดียวกัน

lists/templates/list.html

    <form method="POST" action="/">



$ git status
$ git add lists/templates/list.html
$ git diff
$ git commit -a


Another URL and View for Adding List Items

เปิดไฟล์ lists/tests.py ให้นำ methods  test_home_page_can_save_a_POST_request และ test_home_page_redirects_after_POST ไปใส่ในคลาสใหม่และเปลี่ยนชื่อตามข้างล่าง

A Test Class for New List Creation

lists/tests.py

class NewListTest(TestCase):

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

    def test_redirects_after_POST(self):
        [...]

แล้วเปลี่ยนcode ตามหนังสือ

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

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):
    pass


Then we get "The view lists.views.new_list didn’t return an HttpResponse object". (This is getting rather familiar!) We could return a raw HttpResponse, but since we know we’ll need a redirect, let’s borrow a line from home_page:

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


lists/tests.py

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


Removing Now-Redundant Code and Tests

viewของเราตอนนี้ส่วนใหญ่ทำงานที่ home_page เราควรจะที่จะลดความซับซ้อนของมัน ลองเอาส่วนของ if request.method == 'POST' ออกแล้วดูการเปลี่ยนแปลง

lists/views.py

def home_page(request):
    return render(request, 'home.html') 

 

เมื่อเราเอาการทดสอบ test_home_page_only_saves_ items_when_necessary ออกไปแล้วลองรัน UT
ผลคือ OK

Pointing Our Forms at the New URL

เรามี 2 urlที่จะต้องแก้ไขใน home.html และ lists.html
lists/templates/home.html, lists/templates/list.html.

    <form method="POST" action="/lists/new">

Adjusting Our Models

lists/tests.py เพิ่มลด ตาม code ด้านล่าง

[...]
 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):
[...]
-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_)


เมื่อเราไปเพิ่ม lists/models.py
 
class List(object):
    pass


หลังจากนั้นแก้เป็น

class List(models.Model):
    pass




A Foreign Key Relationship

lists/models.py

from django.db import models

class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.TextField(default='')

 






lists/models.py

from django.db import models

class List(models.Model):
    pass

class Item(models.Model):
    text = models.TextField(default='')
    list = models.ForeignKey(List, default=None)




Adjusting the Rest of the World to Our New Models

 

เมื่อลองรัน
$ python3 manage.py test lists
[...]
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)


แก้ไฟล์ lists/tests.py

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_)

ที่lists/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/')


$ git status # 3 changed files, plus 2 migrations
$ git add lists
$ git diff --staged
$ git commit

Each List Should Have Its Own URL

 

lists/tests.py

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


Capturing Parameters from URLs

superlists/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)),
)

เมื่อลองรัน UT จะพบว่า
ERROR: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
ERROR: test_uses_list_template (lists.tests.ListViewTest)
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
[...]
TypeError: view_list() takes 1 positional argument but 2 were given

แก้ไข parameterใน lists/views.py

def view_list(request, list_id):
    [...]


lists/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})

Adjusting new_list to the New World

เมื่อลองรัน UT
 ERROR: test_redirects_after_POST (lists.tests.NewListTest)
ValueError: invalid literal for int() with base 10:
'the-only-list-in-the-world'

lists/tests.py แก้ code ใน function ใหม่เป็น

class NewListTest(TestCase):
    [...]

    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,))

lists/views.py

def new_list(request):
    list_ = List.objects.create()
    Item.objects.create(text=request.POST['item_text'], list=list_)
    return redirect('/lists/%d/' % (list_.id,))



One More View to Handle Adding Items to an Existing List

 

lists/tests.py

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,))


Beware of Greedy Regular Expressions!

 

superlists/urls.py

    url(r'^lists/(\d+)/$', 'lists.views.view_list', name='view_list'),




The Last New URL

 

superlists/urls.py

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)),
)


The Last New View

 

lists/views.py

def add_item(request):
    pass

รัน UT

TypeError: add_item() takes 1 positional argument but 2 were given


lists/views.py

def add_item(request, list_id):
    pass

รัน UT

ValueError: The view lists.views.add_item didn't return an HttpResponse object.
It returned None instead.

 

lists/views.py

def add_item(request, list_id):
    list_ = List.objects.get(id=list_id)
    return redirect('/lists/%d/' % (list_.id,))


รัน UT

    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1

 

lists/views.py

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,))


รัน UT

Ran 9 tests in 0.022s
OK

 

But How to Use That URL in the Form?

 lists/templates/list.html

 <form method="POST" action="/lists/{{ list.id }}/add_item">

 

For that to work, the view will have to pass the list to the template. Let’s create a new unit test in ListViewTest:

lists/tests.py


    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)

 

lists/views.py

def view_list(request, list_id):
    list_ = List.objects.get(id=list_id)
    return render(request, 'list.html', {'list': list_})

 lists/templates/list.html


    <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 %}


A Final Refactor Using URL includes

$ cp superlists/urls.py lists/

superlists/urls.py

urlpatterns = patterns('',
    url(r'^$', 'lists.views.home_page', name='home'),
    url(r'^lists/', include('lists.urls')),
    # url(r'^admin/', include(admin.site.urls)),
)

lists/urls.py

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


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

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