Introducing automated testing
What are automated tests?
การทดสอบ(testing) มีหลายระดับ การทดสอบบางอันอาจนำไปใช้ในส่วนประกอบเล็ก ๆ (method ของ model ที่เกี่ยวข้องได้คืนค่าต่าง ๆ ตามที่คาดหวังไว้หรือไม่) ในขณะที่การทดสอบอื่น ๆ ทดสอบการดำเนินการโดยรวมของซอฟต์แวร์(ลำดับของอินพุตที่ได้จากผู้ใช้ได้ให้ค่าตามที่คิดไว้หรือไม่) ทั้งหมดนั่นไม่ได้แตกต่างไปจากการทดสอบแบบอื่น ๆ ที่คุณเคยได้ทำไปใน Tutorial 1 ที่ได้ใช้ shell ในการทดสอบพฤติกรรมของ method หรือทดสอบการรันโปรแกรมและใส่ข้อมูลเพื่อตรวจสอบว่าจะมีผลเป็นเช่นไร
ส่วนที่แตกต่างใน automated test คือ การทดสอบจะขึ้นอยู่กับระบบ คุณสามารถสร้าง set ของ การทดสอบได้ในครั้งนั้น และจากนั้นคุณจะสร้างความเปลี่ยนแปลงให้ app ของคุณ ซึ่งคุณสามารตรวจสอบได้ว่าโค้ดยังคงใช้งานได้ อย่างที่คุณคิดไว้ โดยไม่มีการทดสอบ time consuming ด้วยตนเอง
ที่ผ่านมาคุณอาจรู้สึกว่าคุณมีความรู้เพียงพอแล้วในการเรียนรู้ Python/Django และยังคงมีสิ่งอื่นๆให้ได้เรียนรู้ครอบงำคุณอยู่ และอาจเป็นสิ่งที่ไม่จำเป็นมากนัก ยังไงก็ตามแต่ โพลของเราก็ทำงานได้ดี ณ ตอนนี้ ส่วนของการนำไปสู่ปัญหาของการสร้าง automate test นั้นจึงไม่ได้มีผลต่อการทำงานของ app นั้นทำงานได้ดีขึ้น ถ้าการสร้าง app poll เป็นสิ่งสุดท้ายของการเขียน Django ที่คุณจะทำ ใช่แล้ว คุณไม่จำเป็นต้องรู้เกียวกับการสร้าง automate test ก็ได้ แต่ในกรณีที่คุณยังนำ app ที่ว่านี้ไปใช้งานต่อ การสร้าง automate test เป็นสิ่งที่คุณควรจะเรียนรู้ไว้ในตอนนี้
Basic testing strategies
กลยุทธ์ในการทดสอบพื้นฐานมีหลายวิธีด้วยกัน
โปรแกรมเมอร์บางคนจะทำตามระเบียบที่เรียกว่า "การพัฒนา test-driven" โดยจะเขียนการทดสอบก่อนที่จะเขียนโค้ด ซึ่งอาจดูเหมือนใช้งานง่าย แต่ในความเป็นจริงแล้ว มันจะคล้าย ๆ กับคนทั่ว ๆ ไปทำ โดยจะอธิบายปัญหา และสร้างโค้ดเพื่อแก้ปัญหานั้น การพัฒนาแบบ Test-Driven มีไว้สำหรับจัดการรูปแบบของปัญหาใน กรณีทดสอบ Python
สำหรับคนที่ยังใหม่สำหรับการทดสอบนั้นจะสร้างโค้ดได้บ้าง และตัดสินใจว่าตัวโปรแกรมควรจะมีการทดสอบทีหลังก็ได้ บางทีคงจะดีกว่าถ้าได้เขียนการทดสอบเร็วขึ้นกว่านี้ แต่ถึงกระนั้นมันก็ยังไม่สายเกินไปสำหรับการเริ่มต้น
บางครั้งมันยากในการที่จะหาว่าจะเริ่มตรงไหนในการเขียนการทดสอบ ถ้าเคยเขียน Python มาหลายพันบรรทัดแล้วนั้น การเลือกที่จะเขียนบางอย่างในการทดสอบอาจยังไม่ง่าย ซึ่งในกรณีนี้ มันมีผลในการเขียนการทดสอบครั้งแรกในครั้งต่อ ๆ ไปเมื่อคุณต้องการจะเปลี่ยนแปลงอะไรซักอย่าง ไม่ว่าเมื่อคุณเพิ่ม feature ใหม่ หรือ แก้บัค
Writing our first test
We identify a bug
จาก Tutorial ที่ได้เคยทำผ่านมาเราจะพบว่าใน app polls นั้นยังมีบัคอยู่ใน function was_published_recently() โดยจะรีเทิร์นค่า true ถ้า poll ถูก published ภายในไม่เกินหนึ่งวัน (ซึ่งจะถูกต้อง) แต่กระนั้นแล้ว was_published_recently() คืนค่า true ถ้าฟิลด์ pub_date ของ poll เกิดขึ้นในอนาคตเช่นกัน (ซึ่งอาจจะไม่แน่นอน)
จะเห็นได้ว่า (ใน Admin) ได้สร้าง poll อันนึงที่กำหนด date หลอกไว้ใน future_poll ก็จะเห็นได้ว่ารายการการเปลี่ยนแปลงของ poll ได้อ้างว่ามันได้ถูก publish ไปเมื่อไม่นาน
คุณสามารถทดลองตามโดยใช้ shell ดังข้อความด้านล่าง
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Poll
>>> # create a Poll instance with pub_date 30 days in the future
>>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_poll.was_published_recently()
True
แต่สิ่งที่เกิดในอนาคตนั้นจะไม่เรียกว่า "ล่าสุด" ได้และสิ่งนี้เป็นสิ่งที่ไม่ถูกต้อง
Create a test to expose the bug
สิ่งที่เราได้ทำไปใน shell เพื่อทดสอบสำหรับปัญหาที่เป็นสิ่งที่เราสามารถทำได้ใน automated test จริง ๆ ดังนั้น มาลองทำ automated test กัน
ส่วนที่สะดวกสำหรับการทดสอบแอปพลิเคชัน tests.py คือการที่ระบบการทดสอบจะค้นหาการทดสอบในไฟล์ต่าง ๆ ที่มีชื่อไฟล์เริ่มต้นด้วยคำว่า test โดยอัตโนมัติ
ทำการพิมพ์โค้ดตามในไฟล์ tests.py ในโฟล์เดอร์ app polls
import datetime
from django.utils import timezone
from django.test import TestCase
from polls.models import Poll
class PollMethodTests(TestCase):
def test_was_published_recently_with_future_poll(self):
"""
was_published_recently() should return False for polls whose
pub_date is in the future
"""
future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
self.assertEqual(future_poll.was_published_recently(), False)
test_was_published_recently_with_future_poll() : จะทดสอบว่าวันในอนาคตนั้นจะต้องไม่เป็น publish ที่ถูกสร้างเมื่อไม่นานมานี้
ใน terminal เราใช้ Command ด้านล่างเพื่อทดสอบเคสดังกล่าว
$ python manage.py test polls
และคุณจะเห็นสิ่งที่ต้องการ
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
self.assertEqual(future_poll.was_published_recently(), False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
จะเกิด FAIL เพราะ future_poll.was_published_recently() นั้นได้รีเทิร์นค่า true แต่สิ่งที่เราต้องการคือ false ทำให้ test case นี้ FAIL ซึ่งจะต้องทำการแก้ไขให้ถูกต้องต่อไป
Fixing the bug
เรารู้อยู่แล้วว่าสิ่งที่เป็นปัญหา คือ Poll.was_published_recently() จะรีเทิร์นค่า False ถ้า pub_date เป็นวันที่ในอนาคต วิธีแก้ไขใน models.py คือ ให้มันรีเทิร์น True ถ้าวันที่ยังอยู่ในวันที่ผ่านมา
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date < now
และทดสอบดูผลการรันอีกครั้ง
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
หลังจากทำการระบุบั๊ก เราได้เขียนการทดสอบและแก้ไขบั๊กในโค้ด ซึ่งนั่นหมายความว่า การทดสอบของเราใช้ได้
หลายสิงหลายอย่างใน application อาจจะผิดพลาดในอนาคต แต่เราสามารถมั่นใจได้ว่าจะไม่มีบั๊กนี้อีก เพราะการรันการทดสอบแบบง่าย ๆ จะคอยเตือนเราได้อย่างทันท่วงที เราสามารถพิจารณา application ในส่วนเล็ก ๆ นี้ถูก pin อย่างนี้ตลอดไป
More comprehensive tests
ขณะที่เรากำลังอยู่ ณ ตรงนี้ เราสามารถ pin เมธอด was_published_recently() ต่อไปได้อีก ในความเป็นจริงแล้ว มันควรจะเกิดปัญหาไปในทิศทางที่ดี ถ้าการแก้บั๊กนั้นเราได้มีผลไปสู่อันถัดไป
เพิ่ม เมธอดทดสอบไปอีก 2 เมธอด ในคลาสเดียวกัน เพื่อทดสอบหากพฤติกรรมของเมธอดดังกล่าวเป็นไปตามที่คาดไว้
def test_was_published_recently_with_old_poll(self):
"""
was_published_recently() should return False for polls whose pub_date
is older than 1 day
"""
old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
self.assertEqual(old_poll.was_published_recently(), False)
def test_was_published_recently_with_recent_poll(self):
"""
was_published_recently() should return True for polls whose pub_date
is within the last day
"""
recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
self.assertEqual(recent_poll.was_published_recently(), True)
ตอนนี้เราจะมี 3 การทดสอบที่รับรอง ว่า Poll.was_published_recently() ที่รีเทิร์นค่าที่เหมาะสมสำหรับ past, recent, และ future polls
A test for a view
The Django test client
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test.client import Client
>>> # create an instance of the client for our use
>>> client = Client()
>>> # get a response from '/'
>>> response = client.get('/')
>>> # we should expect a 404 from that address
>>> response.status_code
404
>>> # on the other hand we should expect to find something at '/polls/'
>>> # we'll use 'reverse()' rather than a harcoded URL
>>> from django.core.urlresolvers import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
'\n\n\n <p>No polls are available.</p>\n\n'
>>> # note - you might get unexpected results if your ``TIME_ZONE``
>>> # in ``settings.py`` is not correct. If you need to change it,
>>> # you will also need to restart your shell session
>>> from polls.models import Poll
>>> from django.utils import timezone
>>> # create a Poll and save it
>>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
>>> p.save()
>>> # check the response once again
>>> response = client.get('/polls/')
>>> response.content
'\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_poll_list']
[<Poll: Who is your favorite Beatle?>]
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_poll_list'
def get_queryset(self):
"""Return the last five published polls."""
return Poll.objects.order_by('-pub_date')[:5]
from django.utils import timezone
def get_queryset(self):
"""
Return the last five published polls (not including those set to be
published in the future).
"""
return Poll.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
from django.core.urlresolvers import reverse
def create_poll(question, days):
"""
Creates a poll with the given `question` published the given number of
`days` offset to now (negative for polls published in the past,
positive for polls that have yet to be published).
"""
return Poll.objects.create(question=question,
pub_date=timezone.now() + datetime.timedelta(days=days))
class PollViewTests(TestCase):
def test_index_view_with_no_polls(self):
"""
If no polls exist, an appropriate message should be displayed.
"""
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_poll_list'], [])
def test_index_view_with_a_past_poll(self):
"""
Polls with a pub_date in the past should be displayed on the index page.
"""
create_poll(question="Past poll.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_poll_list'],
['<Poll: Past poll.>']
)
def test_index_view_with_a_future_poll(self):
"""
Polls with a pub_date in the future should not be displayed on the
index page.
"""
create_poll(question="Future poll.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.", status_code=200)
self.assertQuerysetEqual(response.context['latest_poll_list'], [])
def test_index_view_with_future_poll_and_past_poll(self):
"""
Even if both past and future polls exist, only past polls should be
displayed.
"""
create_poll(question="Past poll.", days=-30)
create_poll(question="Future poll.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_poll_list'],
['<Poll: Past poll.>']
)
def test_index_view_with_two_past_polls(self):
"""
The polls index page may display multiple polls.
"""
create_poll(question="Past poll 1.", days=-30)
create_poll(question="Past poll 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_poll_list'],
['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
)
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any polls that aren't published yet.
"""
return Poll.objects.filter(pub_date__lte=timezone.now())
class PollIndexDetailTests(TestCase):
def test_detail_view_with_a_future_poll(self):
"""
The detail view of a poll with a pub_date in the future should
return a 404 not found.
"""
future_poll = create_poll(question='Future poll.', days=5)
response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
self.assertEqual(response.status_code, 404)
def test_detail_view_with_a_past_poll(self):
"""
The detail view of a poll with a pub_date in the past should display
the poll's question.
"""
past_poll = create_poll(question='Past Poll.', days=-5)
response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
self.assertContains(response, past_poll.question, status_code=200)
ไม่มีความคิดเห็น:
แสดงความคิดเห็น