七、自動化測試
在開發中,我們都需要測試代碼的正确性。前面的例子都是我們寫好代碼後,運作開發伺服器,在浏覽器上自己點選測試,看寫的代碼是否正常,但是這樣做很麻煩,因為以後如果有改動,可能會影響以前本來正常的功能,這樣以前的功能又得測試一遍,非常不友善,Django中有完善的單元測試,我們可以對開發的每一個功能進行單元測試,這樣隻要運作一個指令 python manage.py test,就可以測試功能是否正常。
測試就是檢查代碼是否按照自己的預期那樣運作。自動化測試的不同之處在于測試工作是由系統為您完成的。您隻需建立一組測試,然後在對應用程式進行更改時,可以檢查代碼是否仍然按照您的初始設計工作,而不必執行耗時的手動測試。
測試驅動開發: 有時候,我們知道自己需要的功能(結果),并不知道代碼如何書寫,這時候就可以利用測試驅動開發(Test Driven Development),先寫出我們期待得到的結果(把測試代碼先寫出來),再去完善代碼,直到不報錯,我們就完成了。
1、shell測試
在我們的投票應用程式中,有一個小錯誤,在polls/models.py中,Question.was_published_recently() 函數是用于判斷是否是過去最近一天内發表的,當實際上,并沒有完全考慮到。對于在将來發表的,還是傳回True。我們可以在項目環境終端shell上測試,代碼如下:
>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # create a Question instance with pub_date 30 days in the future
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # was it published recently?
>>> future_question.was_published_recently()
True
2、開始自動化測試
我們在shell測試這個問題時做了什麼,這正是我們在自動化測試中所能做的,是以讓我們把它變成一個自動化的測試。
應用程式測試的正常位置是在應用程式的tests.py檔案中;測試系統會自動找到以test開頭的任何名字的測試檔案。在polls/tests.py加上如下代碼:
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
在cmd中,進入到項目目錄,輸入下面語句開始測試:
python manage.py test polls
//python manage.py test polls.tests.QuestionModelTests
可以看到:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
可以看到是File “/path/to/mysite/polls/tests.py”, line 16, in test_was_published_recently_with_future_question這裡出了問題,檢視上下文,發現return值那裡判斷不完整,修改polls/models.py代碼如下:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
再次運作,可以看到:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
我們可以進行更多的測試,在polls/tests.py輸入以下代碼進行測試:
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() returns True for questions whose pub_date
is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
3、使用測試用戶端
Django提供了一個測試Client模拟使用者在視圖級别與代碼進行互動。我們可以在tests.py或者shell中使用它。我們先在shell中使用它,在那裡我們需要做一些在tests.py中不必要的事情。首先是建立測試環境,代碼如下:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
接着我們需要導入Client類,但在tests.py中我們使用django.test.TestCase替代,代碼如下:
>>> from django.test import Client
>>> #為用戶端建立一個執行個體供我們使用
>>> client = Client()
開始測試:
>>> # 從'/'擷取響應
>>> response = client.get('/')
Not Found: /
>>> # 我們應該從這個位址得到一個404; 如果你看到一個
>>> # "Invalid HTTP_HOST header" 錯誤和一個400響應,你可能
>>> # 省略了前面描述的setup_test_environment()調用。
>>> response.status_code
404
>>> # 另一方面,我們應該期望找到'/polls/'
>>> # 我們将使用'reverse()'
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/" target="_blank" rel="external nofollow" >What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
可以發現,這裡跟前面一樣,都會獲得将來發表的questions。
4、測試ListView
修改polls/views.py的IndexView,代碼如下:
from django.utils import timezone
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""
傳回最近釋出的五個問題(不包括将來釋出的問題)。
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
根據上面的shell會話建立一個測試,在polls/tests.py增加代碼,如下:
from django.urls import reverse
def create_question(question_text, days):
"""
用給定的‘question_text’建立一個問題,并釋出給定數量的‘days’到現在的偏移量(對于
過去釋出的問題是否定的,對于尚未釋出的問題是肯定的)。
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
如果不存在問題,則顯示适當的消息。
"""
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_question_list'], [])
def test_past_question(self):
""
過去釋出的問題,顯示在索引頁上。
"""
create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
)
def test_future_question(self):
"""
将來釋出問題,不顯示在索引頁上
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
"""
過去和将來釋出的問題都存在,隻顯示過去的問題。
"""
create_question(question_text="Past question.", days=-30)
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question.>']
)
def test_two_past_questions(self):
"""
有多個過去問題,索引頁都顯示。
"""
create_question(question_text="Past question 1.", days=-30)
create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
['<Question: Past question 2.>', '<Question: Past question 1.>']
)
5、測試DetailView
同樣,先修改polls/views.py的DetailView,代碼如下:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
排除尚未釋出的問題。
"""
return Question.objects.filter(pub_date__lte=timezone.now())
在polls/tests.py增加代碼,如下:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
如果問題尚未釋出,傳回404未找到。
"""
future_question = create_question(question_text='Future question.', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question(self):
"""
如果是過去釋出的問題,顯示question_text。
"""
past_question = create_question(question_text='Past Question.', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
想了解更多關于自動化測試,請查閱Testing in Django。
參考資料
- Django官方文檔
- 自強學堂
- 被解放的姜戈
- The Django Book