วันอาทิตย์ที่ 28 มกราคม พ.ศ. 2561

(Week 3-2) - (Week 5-2) Test-Driven Development with Python

update split 7 part (เนื่องรวมในกระทู้เดียวจะยาวเกินไป)

- Chapter 1 : TDD Chapter 1
- Chapter 2 : TDD Chapter 2
- Chapter 3 : TDD Chapter 3
- Chapter 4 : TDD Chapter 4
- Chapter 5 : TDD Chapter 5
- Chapter 6 : TDD Chapter 6
- Chapter 7 : TDD Chapter 7

Test Driven Development (TDD) 

“เวลาเราเขียนโค้ด เราก็มักจะพบปัญหาเหล่านี้ในการทำงานอยู่เสมอ”
เจอ bug ที่หลุดรอดสายตาเราไปได้ กว่าจะรู้ตัวก็ตอนส่งให้ tester ดูแล้ว
หลังจากที่แก้ bug นั้นไปแล้ว พอส่งงานไปให้ตรวจอีกที tester กลับไปเจอ bug ตัวใหม่
bug ที่เจอนั้นเกิดได้จากหลายสาเหตุ เราต้องมานั่งไล่โค้ดทีละส่วนๆ กว่าจะเจอสาเหตุ
จึงเกิดแนวคิดการเขียนโค้ดที่เรียกว่า Test Driven Development(TDD) ขึ้นมา

1. สร้าง Test ขึ้นมาก่อน
2. ลองรัน Test => ไม่ผ่าน
3. เริ่มเขียนโค้ดจริงๆ
4. รัน Test อีกที => ผ่าน
5. ปรับโค้ดที่รันผ่านแล้วให้ดีขึ้น

( ข้อมูลจาก : https://medium.com/@atikom17315/test-driven-development-tdd-%E0%B8%84%E0%B8%B7%E0%B8%AD%E0%B8%AD%E0%B8%B0%E0%B9%84%E0%B8%A3-721689c9a01c )
( ข้อมูลจาก : http://www.siamhtml.com/test-driven-development-introduction/ )

Install selenium & geck driver

selenium



geckdriver




Chapter 1

ขั้นแรกสร้างไฟล์เพื่อจะทดสอบ functional test แรกของหนังสือ

functional test : test ในมุมมองของ user

unit test : test ในมุมมองของ ผู้พัฒนา

แล้วจึงลอง import selenium แล้วเปิด browser firefox ไปที่ localhost ที่พอร์ต 8000
แล้วดูว่า title ของหน้า html คือ 'Django' หรือไม่

ในไฟล์ functional test ที่เราสร้างขึ้น (functional_tests.py)



ลองรันไฟล์

จะเปิดหน้า browser firefox แสดง unable to connect เพราะยังไม่ run server และสร้าง project



ต่อมาสร้างโปรเจคชื่อ superlists แล้วจึง runserver





จะแสดงหน้า default ของ framework django



ต่อมาทำการย้าย functional_test.py เข้ามาในโปรเจค superlists แล้วทำการสร้าง vcs ของเรา

(vcs Version Control System คือ ระบบที่จัดเก็บการเปลี่ยนแปลงที่เกิดขึ้นกับไฟล์หนึ่งหรือหลายไฟล์เพื่อที่คุณสามารถเรียกเวอร์ชั่นใดเวอร์ชั่นหนึ่งกลับมาดูเมื่อไรก็ได้ หนังสือเล่มนี้จะยกตัวอย่างจากไฟล์ที่เป็นซอร์สโค้ดของซอฟต์แวร์ แต่ขอให้เข้าใจว่าจริง ๆ แล้วคุณสามารถใช้ version control กับไฟล์ชนิดใดก็ได้
ที่มา : https://git-scm.com/book/th/v1/%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%B9%83%E0%B8%8A%E0%B9%89%E0%B8%87%E0%B8%B2%E0%B8%99-%E0%B9%80%E0%B8%81%E0%B8%B5%E0%B9%88%E0%B8%A2%E0%B8%A7%E0%B8%81%E0%B8%B1%E0%B8%9A-Version-Control)

ทำการตั้งค่า database >> .gitignore อยู่ใน vcs add file ทั้งหมดใน folder project แล้วทดสอบโชว์สถานะ


แล้วจึงทำการลบ cache



แล้วทำการ add gitignore เงื่อนไขทำเราตั้งไว้ในขั้นต้น แล้วจึง commit



ทดสอบทำการแสดง log commit ต่างๆ(ซึ่งในขั้นนี้มีอยู่เพียง commit เดียว)




Chapter 2

ตามในหนังสือ ทำการเพิ่ม user story ในการประกอบ functional test เรื่องราวของ Edith ที่ได้ใช้ webapp. ตัวอย่าง superlists ตามในหนังสือ ในขั้นต้นเพียงแค่ เทียบ html title ว่าตรงกับคำว่า 'To-Do' หรือไม่


ทดสอบรันซึ่งแน่นอนว่า Assert Error เพราะเรายังไม่ได้จัดการอะไรกับ webapp. ของเราเลย จึงเทียบ 'To-Do' กับหน้า default ของ django


ต่อมาเพิ่ม code ให้แสดงด้วยว่า ถ้าไม่ตรงแล้วแสดง title อะไร


ทำการ run funcitonal test อีกครั้งจึงเทียบ 'To-Do' กับหน้า default ของ django


ต่อมาทำการจัดระเบียบ functional test ของเรา
#1 : ทำเป็น class unittest.TestCase
#2 : setUp() เป็นฟังก์ชันแรกเริ่ม default ตอนเริ่มเทส
#3 : tearDown() เป็นฟังก์ชันสุดท้าย default ตอนจบเทส
#4 : ฟังก์ชันหลักในการ functional test ในมุมมองของ user ของเรา โดยในหนังสือตั้งชื่อว่า "test_can_start_​a_list_and_retrieve_it_later"
#5 : เทียบ html title ว่าตรงกับคำว่า 'To-Do' หรือไม่
#6 : จงใจ fail test ก่อน develop ต่อ โดยมี message error ว่า "Finish the test!" (ทำ test ให้เสร็จ)
#7 : if __name__ == '__main__' คือวิธีการเช็คของ python ว่ารันจาก command line ไม่ใช่ import มาจาก spript อื่นๆหรือไม่
#8 : จาก #7 warnings='ignore' ระงับ ResourceWarning เพราะใช้งานฟุ่มเฟือยที่จะเตือนในขณะที่ writing



เมื่อทดสอบรันก็จะติด error เก่าเพราะยังไม่ได้ทำการแก้ใน webapp

ต่อมาทำการทดสอบใน browser รอโหลดก่อนเริ่มเทสเป็นเวลา 3 วินาที โดยใส่ไว้ใน setUp()


ใช้คำสั่ง git diff ดูการเปลี่ยนแปลงว่าเราได้แก้อะไรไปบ้างเทียบกับ commit ก่อนหน้า


ทำการ commit -a (คือการ automatically เพิ่มการเปลี่ยนแปลงไปยัง tracked files)



Chapter 3

เริ่มต้น chapter 3 ทำการสร้าง app ใน project superlists ของเราชื่อ app lists

ทดสอบใช้ tests.py ที่ django สร้างให้ใน default สร้าง app ขั้นต้นเป็น unittest ของเรา test ในมุมมองของผู้พัฒนา โดยในขั้นต้นลองเขียนทดสอบ function math ง่ายก่อนคือ 1+1 เท่ากับ 3 หรือไม่ ว่าจะเฟลเทสหรือไม่ ถ้าไม่เฟลแปลว่าเครื่องคอมพิวเตอร์ของเรามีปัญหา


เมื่อ test จะแสดง message ว่า 2 != 3 แสดงว่า machinery ทำงานปกติ
แล้วจึงทดสอบใช้ git status แสดงสถานะว่ามีไฟล์ไหนที่ยังไม่เพิ่มเข้ามาบ้าง แล้ว add app lists ที่เราสร้างขึ้นมาใหม่เข้าไปใน vcs ของเรา




ทดสอบ git diff --staged จะแสดงการเปลี่ยนเปลงเกี่ยวกับ commit



ทำการ commit โดยมี message ว่า "Add app for lists, with deliberately failing unit test"



ต่อมาทำการแก้ไฟล์ unit test ของเราโดย import home_page (ซึ่งยังไม่ได้สร้าง) จาก views.py ของ app



ทดสอบ run เกิด message (ไม่เป็นตามในหนังสือ) บอกว่าหา module ชื่อ 'django.core.urlresolvers' ไม่เจอ เกิดจากตอน import ที่อยู่ในหนังสือเป็นเวอร์ชันเก่า




จึงทำการค้นหาว่า version ใหม่กว่า (django 2.0) import อย่างไร








แล้วนำไปแก้ใน tests.py


หลังทำการแก้ จะแสดง message บอกว่า หา home_page เพราะยังไม่ได้สร้าง

จึงไปสร้าง home_page เป็นตัวแปรแบบง่ายๆเพื่อทดสอบ error message ของ unit test

เมื่อ run unit test จะติด test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest) ฟังก์ชันเทสที่เราสร้างมา error โดยไม่เจอ url เพราะเรายังไม่ได้ทำการ map urls

โดยเครื่องคอมพิวเตอร์ติดปัญหาใช้ function เก่าของ django ซึ่งคือ url() ไม่ได้ จึงแก้ปัญหาโดยใช้ path แทน

ดังรูป


และทำการ import views ของ app lists แล้วจึงเพิ่ม path ของ home_page เข้าไปใน urls.py
เมื่อ run unit test จะแสดง TypeError เพราะ home_page ยังเป็นแค่ตัวแปร
ต่อมาเปลี่ยน home_page เป็น function ใน views.py
แล้วจึงลอง unit test จะผ่าน
ทำการ commit โดยมี message ว่า "First unit test and url mapping, dummy view"
หลังจากนั้นว่านึง link ของอาจารย์มีการ maintenance
จึงเปลี่ยนไปใช้ website ของต้นตำหรับ โดยหนังสือของต้นฉบับมีการ update เป็น version ใหม่เรียบร้อยทำให้ไม่ต้องกังวลเกี่ยวกับ version django เก่าใหม่อีกต่อไป


ต่อมาเพิ่ม unit test อีก 1 function คือ test ว่า home_page ของเรา return html กลับไปถูกหรือไม่

#1 : สร้าง httpRequest

#2 : ส่ง home_page view เป็น HttpResponse

#3 : กำหนด decode() เป็น utf-8 format

#4 : ตรวจสอบว่าเริ่มด้วย '<html>' หรือไม่
#5 : ตรวจสอบว่ามี '<title>To-Do lists</title>' ซักที่ใน html ของเราหรือไม่
#6 : ตรวจสอบว่าลงท้ายด้วย '</html>' หรือไม่
เมื่อรัน test จะแสดง typeError เพราะ home_page() ที่เราสร้างไม่ได้กำหนด argument
ทดสอบเพิ่ม argument ใน home_page
เมื่อรัน test จะแสดง AttributeError เพราะ ไม่มี attribute ภายใน home_page() (เรียกแล้ว pass เลย)
ทดสอบเพิ่ม return HttpResponse() กลับไป
เมื่อรัน test จะแสดง AssertionError เพราะยังไม่ได้ใส่ html code / สร้าง template html ใดๆ


ต่อมาทดสอบโดยการเพิ่มส่ง HttpResponse <html> กลับไป
ทดสอบอีกรอบ ก็จะติด assert ถัดไปใน test (#5 ในข้างต้น)
จึงเพิ่ม <title>To-Do lists</title> ตอน return HttpResponse เข้าไปอีก
ทดสอบอีกรอบ ก็จะติด assert ถัดไปใน test (#6 ในข้างต้น)
จึงเพิ่ม </html> ตอน return HttpResponse เข้าไปอีก
เมื่อทำทั้งหมดให้ตาม test แล้ว เมื่อ unit test แล้วจึง unit test ผ่าน
ต่อมารัน functional test จะถึงจุดที่จงใจ return fail message finish the test! พอดี

ถึงขั้นนี้จึงไกลพอที่จะ commit -am (คล้ายๆ -a แต่เพิ่ม message ตามหลังได้)
ต่อมาทดสอบดู log ที่เราทำมาเป็นแบบ --online โชว์รายละเอียดภายในบรรทัดเดียวแบบย่อๆ

Chapter 4

ทำการ import keys เพื่ออนุญาตให้เราสามารถ special keys เช่น Enter และ time เวลา

และทำการ update functional test ของเราให้เช็คเนื้อหาต่างๆมากขึ้นดังนี้
#1 - เช็ค h1
#2 - เช็ค To-Do header
#3 - เช็ค element ชื่อ 'id_new_item'
#4 - เช็ค placeholder ใน html form 'Enter a to-do item'
#5 - ทดสอบใส่คำว่า 'Buy peacock feathers'
#6 - กด enter
#7 - รอ 1 วินาที
#8 - เช็คตารางว่ามีตารางหรือไม่ และมีคำว่า '1: Buy peacock feathers'
#9 - แล้วจงใจ error พร้อม message 'Finish the test!'
เมื่อลอง run functional test จะติด error จากข้อ #1 ในข้างต้น (หา h1 ไม่เจอ)


ทำการ commit


ต่อมาหนังสือเค้าบอกว่าอย่า unit test constants และ template html text เพราะมันไม่จำเป็น unit test ใช้ในการ test logic, flow control และ configulation
ทำการสร้าง template หน้า home.html ดังภาพข้างล่าง


และทำการแก้ function home_page ใน views.py ให้ render template home.html


เมื่อทำการ unit test จะติด error หา template ไม่เจอ

จึงทำการ install app polls ลงไปใน setting.py ของโปรเจค superlists

เมื่อ test จะติด error ที่ไม่หน้าติดคือ ไม่ลงท้ายด้วย </html>

ทั้งๆที่ใน template home.html ก็ลงท้ายด้วย </html>

ซึ่งเป็นเพราะว่า ตอนจบมีการ auto "(\n)" ขึ้นบรรทัดใหม่ทำให้ test error จึงแก้ test ใช้ html.strip() แทนเพื่อกันปัญหาที่เกิด

เมื่อ run unit test จึงผ่าน

ต่อมาใน unit test (tests.py) เพิ่ม import render_to_string เพื่อให้ง่ายต่อการเช็คโดย ให้ render หน้า home เป็น string

ซึ่งก็สามารถใช้แบบแรกได้เช่นกัน แล้วเพิ่ม assert template เข้ามาเช็คหน้า home.html ตอนท้าย

เมื่อ unit test จะผ่าน

ต่อมาลองจงใจ error โดย assert กับ wrong.html แทน home.html

เมื่อ unit test จะ error ตามคาด

ซึ่งในขั้นนี้แก้ test_root_​url_resolves() เป็น test_uses_home_template()

เมื่อแก้แล้วจึง unit test ผ่าน

แล้วจึง commit

ทำการเพิ่ม body ให้ home.html ของเรามี <h1>Your To-Do list</h1>

เมื่อ functional test จะผ่าน #1 และ #2 จากข้างต้น แล้ว แต่ติด #3 ต่อ(ไม่เจอ element 'id_new_item')

จึงเพิ่ม <input id="id_new_item" />

เมื่อ functional test จะติด #4 (ไม่เจอ placeholder)

จึงเพิ่ม placeholder="Enter a to-do item"

เมื่อ functional test จะติดไม่เจอตาราง(#5)ต่อ

จึงเพิ่มตาราง

เมื่อ functional test จะติด #8 (ไม่มี '1: Buy peacock feathers' แสดงในตาราง)

ลองแก้ functional test ให้ message อ่านง่ายมากขึ้น ถ้าติด assert นี้จะแสดง 'New to-do item did not appear in table'

เมื่อ functional test error จะเปลี่ยนไป

ภาพกระบวนการ tdd

ภาพกระบวนการ tdd django

ตั้งแต่ Chapter 5 ขึ้นไป ผมขออนุญาติใช้รูปประกอบเป็นสไลด์ download เป็น jpeg มาจาก google slide ครับ เนื่องจากรูปที่เซฟไว้ในเครื่องหายเพราะผิดพลาดส่วนตัวล้าง folder download แล้วลืมไปว่าเก็บภาพทั้งหมดไว้ใน folder download เพราะอย่างงั้นภาพประกอบจะค่อนข้างใหญ่

Chapter 5


update template home.html ต่อจาก chapter 4 โดยใส่ html form เข้าไป แต่เมื่อ functional test กลับไม่เจอตาราง
คาดว่า เช็คเร็วเกินไปตารางยังไม่ได้โหลด จึงเพิ่ม time sleep 10 วินาที แต่เมื่อ run functional test ปรากฏว่า Forbidden(403) แล้วก็ยังไม่เจอตาราง เช่นเดิม
เพราะ ยังไม่ได้ใส่ CSRF token
จึงใส่ {% csrf_token %} แล้ว functional test จะเจอ error เดิมเพราะเรายังไม่ตั้งแต่อัพเดทที่อะไรที่พิมพ์ลงไปใน input ไปใส่ตาราง

เพราะเช่นนั้นแปลว่าไม่ใช่หน้าโหลดทัน จึงแก้ timesleep กลับไป 1 วินาทีเช่นเดิม
เพิ่ม test function test_can_save_a_POST_request() เช็คข้อมูลที่ response มา
เมื่อ unit test จะพบข้อมูลที่ response มา ไม่มี text "A new list item" เพราะเรายังไม่ได้แก้ให้ส่งข้อมูลอะไรกลับมา
ทำแก้ function home_page() ใน views.py ให้ถ้าได้รับการ POST จาก html form จะ response request POST'item_text' ใน input
- นำตัวแปร new_item_text(ยังไม่ได้สร้าง) ไปใส่ในตาราง
- แล้วทำการ update test_can_save_a_POST_request ใน unit test ให้เช็ค template home.html ว่าถูกหรือไม่ในตอนท้าย
- เมื่อ unit test จะขึ้นว่าไม่ได้ render home.html ในตอนท้าย
- แก้ home_page() ให้ render หน้า home และรับค่า item_text ไปใส่ไว้ในตัวแปร new_item_text ในตัว
- แต่ไม่รัน unit test จะติด error multikey เนื่องจาก we broke the code path where there is no POST request.
แก้โดยการตัวเป็น default ถ้าไม่มีค่าอะไรใน item_text จะไม่ส่งค่า '' แต่เป็น 'None' แทน
- จึงเพิ่ม '' ต่อหลังดังรูป
- unit test อีกรอบจะผ่าน
- แต่ functional test ยังติด error เดิม?
เป็นเพราะ แสดง Buy peacock feathers แต่ใน functional_tests.py เทียบคำว่า 1: Buy pea cock feathers
ติดปัญหาเรื่อง version ตามลิงค์

https://www.blogger.com/blogger.g?blogID=9071948380338177471#editor/target=post;postID=7119590937389645335;onPublishedMenu=allposts;onClosedMenu=allposts;postNum=19;src=postname

จึงใช้เป็นของ version เก่าแทน

- ต่อมาแก้เป็นเช็คตารางรายบรรทัดต่อบรรทัด
- functional test แล้วยังติด error 1: เช่นเดิม
- โกงตามหนังสือใส่ 1: ใน template home.html ตรงตาราง
- ต่อมา update functional test ดังนี้
    #1 - ใส่คำว่า Use peacock feathers to make a fly แล้วกด enter
    #2 - เช็ค assert ในตารางคำว่า 1: Buy peacock feathers
    #3 - เช็ค assert ในตารางคำว่า 2: Use peacock feathers to make a fly
- เมื่อ functional test จะว่าติดตรง #2 เพราะพบคำว่า 1: Use peacock feathers to make a fly แทนคำว่า 1: Buy peacock feathers เพราะหน้า home ของเราอัพอันใหม่แล้วอันเก่าหาย
จากในหนังสือเค้าเห็นว่าเช็คในตารางบ่อยจึงทำเป็น function ไปเลยเพื่อง่ายต่อการเรียกใช้

เมื่อ functional test ก็จะเจอ error เดิม (พบคำว่า 1: Use peacock feathers to make a fly แทนคำว่า 1: Buy peacock feathers เพราะหน้า home ของเราอัพอันใหม่แล้วอันเก่าหาย)
- ต่อมาเพิ่ม test class ใน unit test เพื่อ test ข้อมูลใน data base
- รัน unit test ติด error import เพราะยังไม่ได้สร้าง item ใน model
สร้าง item ใน models.py แล้ว test ตามลำดับ
- error กรอบสีแดง AttributeError เพราะ class Item เป็น object จึง save ไม่ได้
- error กรอบสีน้ำเงิน database error เพราะ ยังไม่สร้าง migration file ก่อนสร้าง database
ทำการ makemigrations แล้ว unit test จะติด error เพราะยังไม่มีตัวแปร text ใน model
- ทำการใส่ตัวแปร text เป็น TextFeild
- unit test จะแสดง database error ในอกแบบเพราะ ยังไม่ makemigration แก้ไข migration file
- ทำการ makemigration แต่ django เตือนเราว่า ยังไม่กำหนดเงื่อนไขอะไร(Null)ใน TextFeild
- กลับไปแก้ Item ใน models.py กำหนด TextFeild เป็น default
- makemigration แล้วจึงทำการ unit test และผ่าน
- ต่อมา update test_can_save_a_POST_request 3 บรรทัด
    1. object count
    2. objects.first() ทำเหมือน objects.all()[0]
    3. เช็คว่า item text ถูกหรือไม่
- unit test จะติด error ว่าไม่มี object ใน Item text (Object = 0)
ทำการแก้ใน home_page() ของเราให้ save item_text ลง database และแก้ return ให้สอดคล้องกัน
- เพิ่ม test_only_saves_items_when_necessary เข้ามาใน unit test
- แก้ home_page() ให้ save ลง database เมื่อมีการ POST เท่านั้น
- เมื่อ unit test จะผ่านทั้งหมด
- เพิ่ม test_can_save_a_POST_request โดนให้ test http status ด้วย
- ลอง unit test
unit test ไม่ผ่านเพราะดังรูป status เป็น OK ไม่ใช่ Found ดังรูปข้างล่าง
แต่เราควรจะได้ status code 302 (redirect ไปที่ location เดิมใหม่อีกครั้ง)
จึงแก้ home_page() ให้เมื่อได้รับ POST บันทึกลง database แล้ว redirect กลับหน้าเดิม
เมื่อ unit test จึงผ่าน
ทำตามหนังสือ แยก test_can_save_a_POST_request ออกเป็น 2 test function เพราะ unit test ที่ดี 1 test function ควร test แค่เพียง 1 อย่าง
- ต่อมาเพิ่ม unit test - test_displays_all_list_items เพื่อเช็คว่า template สามารถ display multiple list items ได้หรือไม่
- เมื่อ unit test ไม่ผ่านตามคาดเพราะยังไม่ได้แก้ template home.html
- update template home.html ให้วนลูป เอา item ใน database item ใส่ตาราง
- update home_page ใส่ตัวแปร item และส่งให้ home
เมื่อ unit test จะ ผ่าน


แต่เมื่อ functional test debug ของ django จะแสดง "no such table: lists_item" เพราะยังไม่ได้ migrate (ข้อมูลเพิ่มเติมช่วงต้น django tutorial 2 : django tutorial 2)
- ทำการเช็ค database setting ใน settings.py ว่าเป็น sqlite3 หรือไม่
- ทำการ migrate สร้าง database sqlite3
- แต่เมื่อ functional test อีกรอบจะ error ตรงที่เราโกงตามในหนังสือ ("1: ")
- แก้โดยการให้เลขที่นับตาม loop counter ตามด้วย : แล้วต่อด้วยตามเดิม ตัวแปร item_text
แต่เมื่อทำการทดสอบหลายๆรอบข้อมูลเดิมที่อยู่ใน database ยังไม่ถูกล้างทำให้เป็นดังภาพข้างล่าง
แก้โดยการลบ database แล้ว migrate ใหม่เป็น --noinput จะไม่ได้ไม่มีข้อมูลค้างอยู่ขณะทดสอบ

Chapter 6


เพื่อเปลี่ยนไปใช้ LiveServerTestCase เพื่อจะผ่าน manage.py ของ django
- สร้าง folder functional_tests
- สร้างไฟล์ __init__.py เพื่อให้ django สามารถเข้าไปจัดการได้
- ย้ายไฟล์ functional_tests.py ไปข้างใน folder ที่สร้างขึ้นแล้วเปลี่ยนชื่อเป็น tests.py
- แล้วจึงทำการเช็คสถานะ vcs ของเรา (git)
- ทำการแก้ใน functional test file ของเราให้ใช้ LiveServerTestCase แทน NewVisitorTest
- แก้จาก localhost port 8000 เป็น live_server_url ของ LiveServerTestCase
ตอนนี้ test ของเราเปลี่ยนไปตามรูปด้านล่าง
ทดสอบ test ทั้งหมด
ทดสอบ functional test
ทดสอบ unit test
การรอต่างดังรูปด้านล่าง เค้าบอกว่าที่เราทำมาเป็นรอแบบ voodoo พึ่งศัยศาสตร์ ทำให้อาจเสียเวลาถ้าเรากำหนดเวลารอไม่ใกล้เคียงกับเวลาโหลด หรืออาจ error ถ้าโหลดช้ากว่าเวลารอ

#slide-wait
จึงทำการแก้ implicit wait (timesleep) ทั้งหมด
แก้เป็นใช้ฟังก์ชันเสริมที่สร้างขึ้นมาใหม่ซึ่งรวมกับฟังก์ชันหาภายในตารางไปด้วยในตัว
#1 - สร้างตัวแปร MAX_WAIT ว่ารอสูงสุด 10 วินาที
#2 - ตั้งตัวแปร start_time เท่ากับเวลาขณะนั้นแล้วเข้าลูปทันที
#3 - จากฟังก์ชัน assert หาคำในตารางอันเดิม
#4 - ถ้าเจอตารางแล้วกลับเข้าฟังก์ชันหลักทันที
#5 - ถ้าไม่เจอ error จาก assert รอ 0.5 วินาทีเรื่อยๆ และ จะทำ #6
#6 - ถ้าเวลา start_time - เวลาปัจจุบัน > ตัวแปร MAX_WAIT จะโชว์ error จาก #slide-wait ข้างต้น
แล้วจึงเปลี่ยนฟังก์ชันหาในตารางทั้งหมดมาใช้อันใหม่ wait_for_row_in_list_table()
ลองเทียบเวลา test ทั้งหมดในอันใหม่ กับ functional test แค่อย่างเดียวในอันเก่าไวกว่าประมาณ 0.5 วินาที
ต่อมาลองจงใจ error โดยการ assert คำว่า 'foo' เวลาจะนานเพิ่มขึ้นอย่างที่เรากำหนดหรือไม่ ประกฏว่านานขึ้น
ลองใส่ id ผิดบ้าง ประกฏว่านานขึ้น

Chapter 7


เริ่ม chapter 7 หนังสือบอกจะพัฒนาให้ app ของเราสามารถลองรับ user ได้มากขึ้น โดยแบ่งเป็นหลายๆ list แยกกัน โดยมี url ดังรูป
โดยมี use case ดังนี้
ในขั้นต้นแยก functional test เป็นสำหรับ user เดียว และ multi user (เพิ่มเรื่องราว user story โดย Francis เข้ามาใช้งานด้วยอีกคนนอกจาก Edith)
โดยมีหน้าตาเต็มๆดังนี้
- ในขั้นต้น update test_redirects_after_POST() โดย assert locationว่า ไปที่ '/lists/the-only-list-in-the-world/' หรือไม่
- เมื่อ unit test ก็ fail ตามคาดเพราะยังไม่ได้แก้อะไรเพิ่มตามที่ test
- แก้แบบลวกๆโดยให้ redirect ไปที่ '/lists/the-only-list-in-the-world/'
- unit test ผ่าน
แต่ functional test จะติด error มากมายดังรูปเพราะ page not found (ยังไม่รู้จัก url path '/the-only-list-in-the-world/' )
- จึงมา write minimal code (เขียน unit test) ต่อ โดยเขียน test ว่าแสดง item ครบหรือไม่
- เมื่อ unit test จะ error ที่ self.assertContains(response, 'itemey 1') เพราะยังไม่ได้สร้าง function view_list() ใน views.py และเชื่อม url ใน urls.py

- จึงสร้าง url ของ the-only-list-in-the-world
- เมื่อ test ทั้งหมด จะติด error เพราะยังไม่ได้สร้าง function view_list() ใน views.py
จึงสร้าง view_list() ใน views.py รับ items ทั้งหมดจาก database แล้ว render หน้าเดิม(home.html)
เมื่อ functional test จะติด error 
test สำหรับ user เดียว : หา '2: Use peacock feathers to make a fly' ไม่เจอ
test สำหรับ multi-user :  'Buy peacock feathers' ดันไปรวมอยู่กับ 'Your To-Do list' กลายเป็น 'Your To-Do list\n1: Buy peacock feathers'
- test สำหรับ user เดียวเป็นเพราะไม่เลือก url ที่จะไปไว้เฉพาะเจาะจง ค่าถึงถูก reset จึงเพิ่ม action="/" เข้าไปใน <form> (เดิมผ่านเพราะเรามีหน้าเดียว POST ถึง auto ส่งข้อมูลกลับไปหน้าเดิม)
- เมื่อเรา functional test อีกที error สำหรับ test user เดียวจะหายไป เหลือแต่ error สำหรับ test multi-user
โดยเมื่อเรามี test เยอะขึ้น ทำให้ยากต่อการมอง สามารถใช้คำสั่ง grep -E "class|def" lists/tests.py เพื่อดู class และ function(def) ต่างๆได้และลบอันที่ไม่จำเป็นออกไปได้
- ต่อมาเพิ่ม test_uses_list_template เข้าไปใน class ListViewTest เพื่อ test template สำหรับ function view_list โดยมีชื่อ template ชื่อว่า list.html
- ต่อมาจึงทำการสร้าง list.html
- แล้ว copy ทั้งหมดใน home.html มาใส่
- แล้วจึงไปแก้หน้า home.html ให้ไม่ show data ใดๆอีกต่อไป
- แก้ home_page() ใน views.py ให้ถ้า POST เก็บข้อมูลแล้ว redirect ไปที่ url path '/lists/the-only-list-in-the-world/' เลย
- run unit test จะผ่าน แต่ functional test จะติดไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
ทำการ commit แล้วสร้าง unit test class ใหม่ NewListTest พร้อม 2 test
- test_can_save_a_POST_request ทดสอบ save POST ลงใน database
- test_redirects_after_POST ทดสอบ redirect หลัง POST ว่าไปถูกหน้าหรือไม่
เมื่อ run unit จะโชว์ error ของทั้ง 2 function test ที่เขียนขึ้นมา คือ
1. object count = 0 เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
2. ไม่ได้ redirect '/lists/the-only-list-in-the-world/' เพราะติด 404 page not found เพราะยังไม่ได้สร้าง function ใน views.py และใส่ path ใน urls.py สำหรับ '/lists/new'
ทำการสร้าง function new_list ใน views.py และใส่ path ใน urls.py สำหรับ function
ทำการลบการเอาข้อมูลใส่ data base เพื่อไม่ให้ redundant (ใส่ข้อมูลลง database ทั้งใน home_page() และ new_list() )
แต่เมื่อ functional test ติดของ one user กลับมาเพราะ forms ยังคงชี้ไปที่ URL เก่าอยู่ทั้ง home.html และ lists.html
- จึงแก้ form action ให้ชี้ไปที่ function new_list()
- แต่ functional test ยังคงติด muti-user อันเดิม คือ ไม่เจอ '1: Buy milk' แต่ไปเจอ ['1: Buy peacock feathers', '2: Buy milk'] แทนเพราะ list ของเรายังถูกรวมกันอยู่
จึงทำการปรับ unit test ต่างๆดังนี้ (เพิ่ม list เข้ามาใน ItemModelTest เปลี่ยนชื่อต่างๆให้เหมาะสม)

lists/tests.py
@@ -1,5 +1,5 @@
 from django.test import TestCase
-from lists.models import Item
+from lists.models import Item, List


 class HomePageTest(TestCase):
@@ -44,22 +44,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_)
- ทำการเพิ่ม List เข้ามาใน models.py และ test ไปด้วยคล้าย TDD Chapter 5 จนถึงขั้นตอนดังภาพด้านล่าง(รูปบนสุด)
- unit test จะพบ error message ดังรูป (รูปรองลงมา) เป็นเพราะ Django จะ save string สำหรับ List object ไม่ได้เซฟสำหรับ relationship ระหว่าง object
จึงทำการ update ใน models.py ให้ list เป็น foreinkey ระหว่าง 2 ตาราง

(Foreign key  คือ คีย์นอก  เป็นคีย์ที่เชื่อม Table ที่เกี่ยวข้องหรือมีความสัมพันธ์กัน  เช่น  ใน Table หลักสูตร กำหนดให้รหัสวิชาเป็น Primary Key และทำการเชื่อมโยงไปยัง Table ลงทะเบียนเพื่อต้องการทราบชื่อวิชาและหน่วยกิตที่นักเรียนลงทะเบียน  โดยกำหนดฟิลด์  รหัสวิชา  ใน Table ลงทะเบียนเป็น Foreign Key ในลักษณะความสัมพันธ์ One to Many หมายความว่า รหัสวิชา  1 วิชา สามารถให้นักเรียนลงทะเบียนได้มากกว่า 1 คน  ดังนั้นจึงมีรหัสซ้ำกันได้ใน Table ลงทะเบียน
ข้อมูลจาก : https://sites.google.com/site/rakpheuxnsemxcom/kar-cadkar-than-khxmul/foreign-key )
ต่อมาทำการ makemigration แต่ไม่เป็นไปตามหนังสือ เนื่องจาก django update version ใหม่
จึงทำการเปิด reference ใน website ของ django ดูและ update ตามกรอบแดงในภาพข้างล่างลงไปใน models.py ของเรา
เมื่อแก้แล้วเป็นดังภาพด้านล่างและ makemigration ได้
แต่จะติด test อันเก่าเนื่องจาก foreignkey ทำให้เกิดความสัมพันธ์แบบ one-to-many ระหว่าง 2 ตาราง
จึงทำการแก้ unit test และ function ใน views.py ของเราให้เหมาะสมกับ database
functional test จะติด error เดิม
( AssertionError: '1: Buy milk' not found in ['1: Buy peacock feathers', '2: Buymilk'] )
แต่ลอง unit test จะผ่านทั้งหมด จึงทำการ commit
แล้วจึงทำการปรับ code บางส่วน test_displays_all_items test เปลี่ยนชื่อเป็น test_displays_only_items_for_that_list ให้สื่อความหมาย แล้ว update ให้ตรวจสอบเป็น list ของใครของมัน
แก้ urls.py (ตามหนังสือใช้ url() แต่ computer ผมรันแล้ว error จึงใช้ path repath มาตั้งแต่ต้น)
ใช้ repath() กับ function view_list ให้แสดงแยกว่า list ไหนเป็น list ผ่านตามลำดับ โดยใช้ regular expression ของ django บอก link url
( ข้อมูลเพิ่มเติม regular expression ผมได้ใส่ไว้ใน Django tutorial 3 )
ซึ่งเมื่อ run unit test จะติด error ของ function view_list() เพราะ ไม่ได้ใส่ argument ว่าอยู่ list ไหน
- ทำการแก้โดยใส่ list_id กับตัวแปรรับค่ามาใช้
- เมื่อ unit test จะติด test เก่า ( test_redirects_after_POST() ) เนื่องจาก ยังคงดู url 'the-only-list-in-the-world' อยู่
แก้ test_redirects_after_POST และ new_list ให้ตาม urls.py อันใหม่ที่อัพเดท
จะรัน unit test ผ่าน
แต่ functional test จะติด one user test เพราะ ทุกครั้งที่เราใส่อะไรลงไปจะสร้าง list ใหม่ ยังไม่สามารถเพิ่มค่าลงใน list เดิมได้
update unit test function 2 อันได้แก่ test_can_save_a_POST_request_to_an_existing_list และ test_redirects_to_list_view ก่อนเขียน Add Items ไปยัง List ที่มีอยู่แล้ว
เมื่อ unit test จะได้
ดูจาก wiki list http code ได้ดังนี้



หนังสือบอกเป็นเพราะว่า "we’ve used a very "greedy" regular expression in our URL"
เพราะเมื่อใครก็ตามขอ URL เช่น /lists/1/add_item/  นั้น (.+) ของเราจะหาทั้งประโยคคือ ( 1/add_item/ ) และไปใช้ function view_list() ไม่ใช้ add_item() เพราะ (.+) เป็นตัวอักษรหรืออะไรก็ได้

จึงได้ทำการแก้ให้เป็น numerical digit (ตัวเลข) เท่านั้น
- จึงปรับ urls.py ที่เกี่ยวข้องให้เป็น (.d+) และ ใส่ path ของ add_item()
- สร้าง function add_item()
- run unit test ผ่าน

ต่อมาทำการแก้ action ใน list ให้เรียกใช้ add_item()
update unit test ทดสอบว่า list ที่ถูกส่งไปยัง template ถูก list หรือไม่
เมื่อ run unit test จะได้ KeyError: 'list' เพราะเราไม่ได้ส่ง list ไปยัง template
KeyError: 'list'
- จึงทำการแก้ view_list ให้เก็บค่า list_id ไว้ในตัวแปร list_ และส่งไปยัง template ด้วย
- นำ list_ ที่ได้มา มาชี้ที่ loop
 run test ทั้งหมดจะผ่านทั้งหมด
เหมือนจะเสร็จแล้วแต่ในหนังสือเค้าบอกว่า เอา urls ของ app ไปใส่ไว้ใน urls.py ของ project มันไม่เป็นมืออาชีพ จึงทำการแก้ดังภาพด้านล่าง


ตอนนี้ก็เสร็จ Obey the Testing Goat! "Test-Driven Web Development with Python" ใน Part 1: The Basics of TDD and Django ทั้ง 7 บทเรียบร้อย

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

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