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

Test-Driven Development with Python - Chapter. 2

Extending Our Functional Test Using the unittest Module

      ทำการเเก้ไขไฟล์ functional_tests.py เพื่อทำการทดสอบ

 
    from selenium import webdriver
    browser = webdriver.Firefox()
    browser.get('http://localhost:8000')
    assert 'To-Do' in browser.title
    browser.quit()
 

     จากนั้นทำการทดสอบไฟล์ functional_tests.py โดยการ runserver เเละ functional_tests.py เพื่อทำการทดสอบ

 
   $ python3 manage.py runserver
  
 
   $ python3 functional_tests.py
 
ภาพ (run functional_tests.py เเละ มีการ AssertionError)
ภาพ (run functional_tests.py เเละมีการเรียกหน้า localhost:8000 ขึ้นมา)

     จากนั้นให้เราทำการเเก้ไข AssertionError โดยการระบุ title ของหน้าที่ Error เข้าเพื่อให้ง่ายต่อการตรวจสอบนั้นเอง ไปโดยการเพื่ม "Browser title was " + browser.title เข้าไป
 
    from selenium import webdriver
    browser = webdriver.Firefox()
    browser.get('http://localhost:8000')
    assert 'To-Do' in browser.title, "Browser title was " + browser.title
    browser.quit()
 
ภาพ (run functional_tests.py เเละ มีการ AssertionError เเละ บอกหน้าที่ทำการทดสอบ)
ภาพ (run functional_tests.py เเละมีการเรียกหน้า localhost:8000 ขึ้นมา)

     ทำการเเก้ไขไฟล์ functional_tests.py จากนั้นทำการ run functional_tests.py เพื่อทำการทดสอบอีกที

 
    from selenium import webdriver
    import unittest

    class NewVisitorTest(unittest.TestCase):  #1

        def setUp(self):  #2
            self.browser = webdriver.Firefox()

        def tearDown(self):  #3
            self.browser.quit()

        def test_can_start_a_list_and_retrieve_it_later(self):  #4
            # Edith has heard about a cool new online to-do app. She goes
            # to check out its homepage
            self.browser.get('http://localhost:8000')

            # She notices the page title and header mention to-do lists
            self.assertIn('To-Do', self.browser.title)  #5
            self.fail('Finish the test!')  #6

            # She is invited to enter a to-do item straight away
            #[...rest of comments as before]

    if __name__ == '__main__':  
        unittest.main()  
  
  1. จัดระเบียบการ test เป็น class
  2. เป็นการเริ่มการทำงานของ browser  
  3. เป็นการหยุดการทำงาน browser  
  4. เป็นส่วน main ที่เอาไว้ใช้ test การทำงาน โดยจะเปิด browser ไปที่ http://localhost:8000
  5. ตรวจสอบว่า 'To-Do' ตรงกับ title ของ browser หรือไม่ 
  6. ใช้เป็นตัวเตือนว่าเสร็จสิ้นการ test


     เกิด การ Error เนื่องจาก 'To-Do' ไม่ตรงกับ title ของ browser และนอกจากนี้จะเห็นได้ว่า browser มีการเปิดและปิดลงอัตโนมัติเนื่องจากการทำงานของฟังก์ชั่น setUp และ tearDown ที่ได้กำหนดไว้


แก้ไขไฟล์ functional_tests.py
 
    [...]
    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.implicitly_wait(3)

    def tearDown(self):
    [...]
 
     เมื่อทำการรันจะพบว่าหน้าต่าง browser จะเปิดค้างไว้ 3 วินาที ซึ่งเป็นค่าที่ถูกกำหนดไว้ ก่อนจะปิดลงอัตโนมัติ

     Commit
     คำสั่ง  git diff จะแสดงความแตกต่างของสถานะของไฟล์ระหว่างการ commit ครั้งล่าสุด กับสถานะที่เป็นอยู่บนดิส




- - - - - - - - - -

Test-Driven Development with Python - Chapter. 1

Test-Driven Development (TDD)

      Test-Driven Development (TDD) คือรูปแบบการพัฒนา app โดยการเริ่มจากการสร้างตัว test ก่อน แล้วจึงเขียนโค้ดของโปรแกรม เปรียบได้กับการวาง spec ของโปรแกรมที่เราต้องการก่อนจึงค่อยสร้างโปรแกรมตาม spec ที่เราวางไว้ ช่วยให้สามารถสร้างโปรแกรมได้ตามความต้องการ การสร้างตัว test ย่อยๆของแต่ละส่วนสามารถทำให้รู้ตำแหน่งที่เกิด bug ได้โดยไม่ต้องไล่โค้ดทั้งหมด อีกทั้งยังสามารถลอง test โค้ดในขณะกำลังสร้างได้เรื่อยๆ

Functional Test  Unit Test เเละ Refactoring

     Functional Test คือการทดสอบในมุมมองของ user เป็นการมอง app ที่เราทดสอบจากด้านนอกเปรียบเสมือน "black box" โดยไม่สนใจว่ามีการทำงานอย่างไรอยู่ภายใน สนใจเพียงว่า output ที่ได้นั้นมีการทำงานที่ถูกต้องตรงตามความต้องการหรือไม่
     Unit Test คือการทดสอบในมุมมองของ programmer คือการมอง app โดยสนใจที่ตัวโค้ด เป็นการ test ที่ช่วยในแก้ bug ในการเขียนโค้ด ตรวจสอบว่าโค้ดที่เราเขียนนั้น error ตรงไหน อย่างไร ช่วยให้สามารถเขียนโค้ดได้ถูกต้อง ไม่มี bug 
     Refactoring คือ การกลับมาปรับปรุงโค้ดที่เคยเขียนไปแล้วและทำงานได้อย่างถูกต้องแล้ว ให้สั้นลง กระชับมากขึ้น ทำงานได้เร็วมากขึ้น อ่านได้ง่ายมากขึ้น ลดความซับซ้อนของโค้ด 

Functional Test

  •     การสร้าง Project ของ Django สำหรับเก็บ web app ของเรา

    1. สร้างไฟล์ functional_tests.py สำหรับเป็นตัวทดสอบ functional test ซึ่งตัว Functional Test เราจะใช้ library ของ Selenium ในการเขียน
    2. สร้างที่สำหรับเก็บโปรเจคด้วยคำสั่ง

 
   $ django-admin.py startproject superlists
 

     จะสร้างโฟล์เดอร์ชื่อ superlists และมีไฟล์ย่อยๆดังนี้
.
├── functional_tests.py
└── superlists
    ├── manage.py
    └── superlists
        ├── __init__.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
 
 3. ย้ายไฟล์ functional_tests.py เข้าไปอยู่ในโฟล์เดอร์ superlists อันนอกโดยคำสั่ง
 
   $ mv functional_tests.py superlists/
        
  • การรัน Development Server ของ Django
      การทดสอบ functional test จะเป็นที่จะต้องสร้าง Server ขึ้นมาสำหรับรัน functional test โดยใช้คำสั่ง
 
   $ python3 manage.py runserver
        
      จะได้ Server บน IP 127.0.0.1 port 8000

  •     การรัน Functional Test นั้นต้องรันบน Server ข้างต้นซึ่งรันโดยใช้คำสั่ง
 
   $ python3 functional_tests.py
        

Git Repository

     Git Repository คือ library หนึ่งที่สำหรับใช้จัดการ version control system (VCS) ซึ่งจะผูกมัดกับไฟล์และบันทึกแต่ละ version ของไฟล์งานเราไว้ หากเราเขียนโค้ดแล้วเกิดบัคเราก็สามารถเรียกเวอร์ชั่นก่อนหน้าเพื่อที่จะเรียกกลับมาดูหรือเขียนใหม่ต่อจากเวอร์ชั่นก่อนหน้าที่จะบัคได้

    การสร้าง Git Repository ใหม่เราจะเข้าไปในโฟลเดอร์ superlists แล้วใช้คำสั่ง
 
   $ git init .
        
    จะสร้าง Repository เปล่าๆขึ้นมาให้เรา
    เราสามารถเพิ่มส่วนที่เหลือก็คือไฟล์งานต่างๆของที่อยู่ใน superlists ซึ่งเป็น current folder ของเราในตอนนี้โดยการใช้คำสั่ง

 
   $ git add .
        
    โดยที่ "." หมายถึง current folder
    การเรียกดูการเปลี่ยนแปลงของ Git Repository ใช้คำสั่ง

 
   $ git status
        
    ซึ่งจะแสดงว่าไฟล์ที่เราผูกมัด (commit) ไว้กับ git อันไหนมีการแก้ไขบ้าง ไฟล์ไหนที่เราพึ่งทำการผูกมัด ไฟล์ไหนที่ git ติดตาม (tracked)และไฟล์ไหนที่เราไม่ได้ให้ git ติดตาม (untracked)
    การยืนยันไฟล์ที่มาจาก git add ให้เข้าไปใน Git Repository ใช้คำสั่ง

 
   $ git commit
        
    ซึ่งจะเก็บไฟล์ที่ add เข้ามาใหม่และบันทึกการแก้ไขไฟล์ไว้เป็นเวอร์ชั่นใหม่และมีการเปิดหน้า nano ให้เขียน comment ไว้เพื่อให้เรารู้ว่าตอนนี้โปรแกรมที่เราเขียนนั้นสำเร็จไปถึงไหนแล้ว เวลาเรียกกลับมาดูจะได้หาได้ง่าย
 
   $ git diff
        
เป็นคำสั่งที่จะแสดงการแก้ไขไฟล์ที่เราผูกมัดไว้อย่างละเอียด ว่ามีการเพิ่มเข้าลบออกอย่างไร
 
   $ git commit -m "<comment>"
        
-m คือเราสารมารถเขียน comment ต่อท้ายคำสั่งได้เลยโดยไม่ต้องเข้าไปแก้ในหน้า nano
 
   $ git log --oneline
       
    จะแสดงเวอร์ชั่นและ comment ต่างๆที่เราได้บันทึกไว้ 

วันเสาร์ที่ 6 กุมภาพันธ์ พ.ศ. 2559

Exercise

Exercise 

  • Create homepage at http://localhost:8000
  • Create a view that shows all questions and results
  • Load data from csv file

Create homepage at http://localhost:8000

     ทดลองเข้าที่ http://localhost:8000 จะเห็นดั้งรูปเพราะยังไม่ได้สร้างหน้าเเรกน่ะครับ เเต่เราสามารถที่จะเข้าไปยังหน้า http://localhost:8000/polls ได้เพราะเราได้ทำการสร้างมาก่อนหน้านี้เเล้วเพราะฉนั้นเรามาสร้างหน้า homepage กันดีกว่า
  
ภาพ(http://localhost:8000)

1. ทำการสร้าง app ขึ้นมาใหม่ 1 app ชื่อว่า home เพื่อเป็นตัวกลางในการเรียกใช้งาน app อื่นๆๆ โดยใช้คำสั่งผ่าน Terminal ที่ directory งานของเราครับ
 
  $ python3 manage.py startapp home
 

1.1 จากนั้นทำการเเก้ไขไฟล์ mysite/settings.py ตรง 
 
   INSTALLED_APPS = [
        'home.apps.HomeConfig', # เพิ่ม app home ที่เรา สร้างใหม่เข้าไป
        'polls.apps.PollsConfig',
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]
 
1.2 ทำการเเก้ไขไฟล์ mysite/urls.py ตรง 
 
    urlpatterns = [
        url(r'', include('home.urls')), # เพิ่ม url app home ที่เราสร้างใหม่เข้าไป               
        url(r'^polls/', include('polls.urls')),
        url(r'^admin/', admin.site.urls),
    ]
 
  • url(r'', include('home.urls')) บอกว่าถ้า url ที่ใส่เข้าไปเป็น url ว่างๆให้ไปใช้ไฟล์ urls ที่ home นั้นคือ http://localhost:8000
  • url(r'^polls/', include('polls.urls')) บอกว่าถ้า url ที่ใส่เข้าไปเป็น url ที่ตามด้วย /polls ว่างๆให้ไปใช้ ไฟล์ urls ที่ polls นั้นคือ http://localhost:8000/polls
  • url(r'^admin/', admin.site.urls) บอกว่าถ้า url ที่ใส่เข้าไปเป็น url ที่ตามด้วย /admin ให้ไปยังหน้า admin  
 -------------------
1.3 ทำการเเก้ไขไฟล์ home/urls.py
 
    from django.conf.urls import url
    from . import views
    app_name = 'home'
    urlpatterns = [
        url(r'^$', views.index, name='index')
    ]
 
  •   url(r'^$', views.index, name='index') ถ้า url ว่างๆให้ใช้งานไฟล์ views ดูที่ index
 -------------------
1.4 ทำการเเก้ไขไฟล์ home/views.py
        
    from django.shortcuts import render
    def index(request):
        return render(request, 'home/index.html')
    
  •  ให้ render ไฟล์ home/index.html
-------------------
1.4 ทำการสร้างไฟล์ home/index.html
        
<html>
 <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Home</title></head>
 <body>
  <div align="center"><h3>Django Tutorials Polls</h3><hr/></div>
   <center><table border ="0">
   <tr>
    <td width="260" align="center">
     <a href="/admin"><strong>Administrator</strong></a><br>
    </td>
    <td width="260" align="center">
     <a href="/polls"><strong>Question List Polls</strong></a><br>
    </td>  
                <td width="260" align="center">
     <a href=""><strong>Question List Update</strong></a><br>
    </td>
    <td width="260" align="center">
     <a href=""><strong>View All Vote</strong></a><br>
    </td >  
   </tr>
 </body>
</html>
    
 จากนั้นเข้าไปที่ http://localhost:8000 เราก็จะได้หน้า homepage ของเรามาดั้งรูปครับ
ภาพ(http://localhost:8000 หลังจากที่ทำตามขั้นตอนดั้งกล่าว)
  • Administrator เข้าไปยังหน้า admin
  • Question List Polls เข้าไปยังหน้า polls เพื่อทำการโหวด
  • Question List Update เข้าไปยังหน้าของ Update คำถามด้วย csv ตาม Exercise ข้อ 3
  • View All Vote เข้าไปยังหน้า เเสดงคำถาม เเละ การโหวดทั้งหมด ตาม Exercise ข้อ 2

Create a view that shows all questions and results

ภาพ (http://localhost:8000/polls/) 

ภาพ (http://localhost:8000/polls/1/)
ภาพ (http://localhost:8000/polls/1/results/)
 1.ทำการเเก้ไขไฟล์ polls/views.py เพิ่ม Function "indexviewall" เพื่อทำการเเสดงผลของคำถาม เเละการโหวดทั้งหมด
        
 def indexviewall(request):
    question_list = Question.objects.all()
    sumvote = {}
    savesum = 0
    for question in question_list:
        choice_list = question.choice_set.all()
        for choice in choice_list:
            savesum = savesum+int(choice.votes)
        sumvote[str(question)]=savesum
        savesum = 0
    return render(request, 'polls/view-all.html', {'question_list': question_list,'sumvote': sumvote})
   
 2.ทำการสร้างไฟล์ view-all.html ใน templates เพื่อทำการเเสดงผลของข้อมูล
        
 <title>View All Vote</title>
 <div align="center"><h1>View All Vote</h1><hr/>
 {% if question_list %}
  {% for question in question_list %}
  <table border="1" width="40%"> 
  <tr>
  <td colspan="3" bgcolor="#CC00FF" align="center">{{ question.question_text }}</td>
  </tr>
   {% for choice in question.choice_set.all %}
   <tr>
    <td th width="70%">&nbsp;{{ choice.choice_text }}</td>
    <td th width="15%" align="right">{{ choice.votes }}&nbsp;</td>
    <td th width="15%" align="center">vote{{ choice.votes|pluralize }}</td>
   </tr>
   {% endfor %}

                        {% for key, value in sumvote.items %}
                  {% if question.question_text == key %}
     <tr><td colspan="3" bgcolor="#CC66FF" align="center">จำนวนโหวดทั้งหมด = {{value}}</td></tr>
    {% endif %}
   {% endfor %}

   </table><br/>
  {% endfor %}
 {% else %}
  <p>No polls are available.</p>
 {% endif %}
<hr/><a href="/"><button>Home</button></a></div>
</div>
  

 Load data from csv file

1.ทำการเเก้ไขไฟล์ polls/views.py เพิ่ม Function "updatecsvnew" กับ "updatecsvadd" เพื่อทำการโหลดคำถามเเละตัวเลือกจากไฟล์ csv โดยที่ updatecsvnew เป็นการลบคำถามเก่าเเล้วใส่คำถามใหม่ไปเเทน เเต่ updatecsvadd เป็นการเพิ่มคำถามใหม่เข้าไป
        
 def updatecsvnew(request):
    question_list = Question.objects.all()
    for question in question_list:
        choice_list = question.choice_set.all()
        for choice in choice_list:
            choice.delete()
        question.delete()
    save_csv = []
    with open('polls/CSV/Question.csv') as csvfile:
        readCSV = csv.reader(csvfile, delimiter = ',')
        for row in readCSV:
            save_csv.append(row)
        for i in range(1,len(save_csv),1):
            question = Question(question_text = str(save_csv[i][0]) ,pub_date = timezone.now())
            question.save()
            for l in range(1,len(save_csv[i]),1):
                question.choice_set.create(choice_text = str(save_csv[i][l]) , votes = 0)
    return HttpResponseRedirect(reverse('polls:up-date'))

 def updatecsvadd(request):
    question_list = Question.objects.all()
    save_csv = []
    with open('polls/CSV/Question.csv') as csvfile:
        readCSV = csv.reader(csvfile, delimiter = ',')
        for row in readCSV:
            save_csv.append(row)
        for i in range(1,len(save_csv),1):
            question = Question(question_text = str(save_csv[i][0]) ,pub_date = timezone.now())
            question.save()
            for l in range(1,len(save_csv[i]),1):
                question.choice_set.create(choice_text = str(save_csv[i][l]) , votes = 0)
    return HttpResponseRedirect(reverse('polls:up-date'))
           

- - - - - - - - -