本教程上接教程第4部分。 我們已經(jīng)建立一個(gè)網(wǎng)頁(yè)投票應(yīng)用,現(xiàn)在我們將為它創(chuàng)建一些自動(dòng)化測(cè)試。
測(cè)試是檢查你的代碼是否正常運(yùn)行的簡(jiǎn)單程序。
測(cè)試可以劃分為不同的級(jí)別。 一些測(cè)試可能專(zhuān)注于小細(xì)節(jié)(某一個(gè)模型的方法是否會(huì)返回預(yù)期的值?), 其他的測(cè)試可能會(huì)檢查軟件的整體運(yùn)行是否正常(用戶(hù)在對(duì)網(wǎng)站進(jìn)行了一系列的操作后,是否返回了正確的結(jié)果?)。這些其實(shí)和你早前在教程 1中做的差不多, 使用shell來(lái)檢測(cè)一個(gè)方法的行為,或者運(yùn)行程序并輸入數(shù)據(jù)來(lái)檢查它的行為方式。
自動(dòng)化測(cè)試的不同之處就在于這些測(cè)試會(huì)由系統(tǒng)來(lái)幫你完成。你創(chuàng)建了一組測(cè)試程序,當(dāng)你修改了你的應(yīng)用,你就可以用這組測(cè)試程序來(lái)檢查你的代碼是否仍然同預(yù)期的那樣運(yùn)行,而無(wú)需執(zhí)行耗時(shí)的手動(dòng)測(cè)試。
那么,為什么要?jiǎng)?chuàng)建測(cè)試?而且為什么是現(xiàn)在?
你可能感覺(jué)學(xué)習(xí)Python/Django已經(jīng)足夠,再去學(xué)習(xí)其他的東西也許需要付出巨大的努力而且沒(méi)有必要。 畢竟,我們的投票應(yīng)用已經(jīng)活蹦亂跳了; 將時(shí)間運(yùn)用在自動(dòng)化測(cè)試上還不如運(yùn)用在改進(jìn)我們的應(yīng)用上。 如果你學(xué)習(xí)Django就是為了創(chuàng)建一個(gè)投票應(yīng)用,那么創(chuàng)建自動(dòng)化測(cè)試顯然沒(méi)有必要。 但如果不是這樣,現(xiàn)在是一個(gè)很好的學(xué)習(xí)機(jī)會(huì)。
在某種程度上, ‘檢查起來(lái)似乎正常工作’將是一種令人滿(mǎn)意的測(cè)試。 在更復(fù)雜的應(yīng)用中,你可能有幾十種組件之間的復(fù)雜的相互作用。
這些組件的任何一個(gè)小的變化,都可能對(duì)應(yīng)用的行為產(chǎn)生意想不到的影響。 檢查起來(lái)‘似乎正常工作’可能意味著你需要運(yùn)用二十種不同的測(cè)試數(shù)據(jù)來(lái)測(cè)試你代碼的功能,僅僅是為了確保你沒(méi)有搞砸某些事 —— 這不是對(duì)時(shí)間的有效利用。
尤其是當(dāng)自動(dòng)化測(cè)試只需要數(shù)秒就可以完成以上的任務(wù)時(shí)。 如果出現(xiàn)了錯(cuò)誤,測(cè)試程序還能夠幫助找出引發(fā)這個(gè)異常行為的代碼。
有時(shí)候你可能會(huì)覺(jué)得編寫(xiě)測(cè)試程序?qū)⒛銖挠袃r(jià)值的、創(chuàng)造性的編程工作里帶出,帶到了單調(diào)乏味、無(wú)趣的編寫(xiě)測(cè)試中,尤其是當(dāng)你的代碼工作正常時(shí)。
然而,比起用幾個(gè)小時(shí)的時(shí)間來(lái)手動(dòng)測(cè)試你的程序,或者試圖找出代碼中一個(gè)新引入的問(wèn)題的原因,編寫(xiě)測(cè)試程序還是令人愜意的。
將測(cè)試看做只是開(kāi)發(fā)過(guò)程中消極的一面是錯(cuò)誤的。
沒(méi)有測(cè)試,應(yīng)用的目的和意圖將會(huì)變得相當(dāng)模糊。 甚至在你查看自己的代碼時(shí),也不會(huì)發(fā)現(xiàn)這些代碼真正干了些什么。
測(cè)試改變了這一切; 它們使你的代碼內(nèi)部變得明晰,當(dāng)錯(cuò)誤出現(xiàn)后,它們會(huì)明確地指出哪部分代碼出了問(wèn)題 —— 甚至你自己都不會(huì)料到問(wèn)題會(huì)出現(xiàn)在那里。
你可能已經(jīng)創(chuàng)建了一個(gè)堪稱(chēng)輝煌的軟件,但是你會(huì)發(fā)現(xiàn)許多其他的開(kāi)發(fā)者會(huì)由于它缺少測(cè)試程序而拒絕查看它一眼;沒(méi)有測(cè)試程序,他們不會(huì)信任它。 Jacob Kaplan-Moss,Django最初的幾個(gè)開(kāi)發(fā)者之一,說(shuō)過(guò)“不具有測(cè)試程序的代碼是設(shè)計(jì)上的錯(cuò)誤?!?/p>
你需要開(kāi)始編寫(xiě)測(cè)試的另一個(gè)原因就是其他的開(kāi)發(fā)者在他們認(rèn)真研讀你的代碼前可能想要查看一下它有沒(méi)有測(cè)試。
之前的觀點(diǎn)是從單個(gè)開(kāi)發(fā)人員來(lái)維護(hù)一個(gè)程序這個(gè)方向來(lái)闡述的。 復(fù)雜的應(yīng)用將會(huì)被一個(gè)團(tuán)隊(duì)來(lái)維護(hù)。 測(cè)試能夠減少同事在無(wú)意間破壞你的代碼的機(jī)會(huì)(和你在不知情的情況下破壞別人的代碼的機(jī)會(huì))。 如果你想在團(tuán)隊(duì)中做一個(gè)好的Django開(kāi)發(fā)者,你必須擅長(zhǎng)測(cè)試!
編寫(xiě)測(cè)試有很多種方法。
一些開(kāi)發(fā)者遵循一種叫做“由測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)”的規(guī)則;他們?cè)诰帉?xiě)代碼前會(huì)先編好測(cè)試。 這似乎與直覺(jué)不符,盡管這種方法與大多數(shù)人經(jīng)常的做法很相似:人們先描述一個(gè)問(wèn)題,然后創(chuàng)建一些代碼來(lái)解決這個(gè)問(wèn)題。 由測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)可以用Python測(cè)試用例將這個(gè)問(wèn)題簡(jiǎn)單地形式化。
更常見(jiàn)的情況是,剛接觸測(cè)試的人會(huì)先編寫(xiě)一些代碼,然后才決定為這些代碼創(chuàng)建一些測(cè)試。 也許在之前就編寫(xiě)一些測(cè)試會(huì)好一點(diǎn),但什么時(shí)候開(kāi)始都不算晚。
有時(shí)候很難解決從什么地方開(kāi)始編寫(xiě)測(cè)試。 如果你已經(jīng)編寫(xiě)了數(shù)千行Python代碼,挑選它們中的一些來(lái)進(jìn)行測(cè)試不會(huì)是太容易的。 這種情況下,在下次你對(duì)代碼進(jìn)行變更,或者添加一個(gè)新功能或者修復(fù)一個(gè)bug時(shí),編寫(xiě)你的第一個(gè)測(cè)試,效果會(huì)非常好。
現(xiàn)在,讓我們馬上來(lái)編寫(xiě)一個(gè)測(cè)試。
幸運(yùn)的是,polls應(yīng)用中有一個(gè)小錯(cuò)誤讓我們可以馬上來(lái)修復(fù)它:如果Question在最后一個(gè)天發(fā)布,Question.was_published_recently() 方法返回True(這是對(duì)的),但是如果Question的pub_date 字段是在未來(lái),它還返回True(這肯定是不對(duì)的)。
你可以在管理站點(diǎn)中看到這一點(diǎn); 創(chuàng)建一個(gè)發(fā)布時(shí)間在未來(lái)的一個(gè)Question; 你可以看到Question 的變更列表聲稱(chēng)它是最近發(fā)布的。
你還可以使用shell看到這點(diǎn):
>>> 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
由于將來(lái)的事情并不能稱(chēng)之為‘最近’,這確實(shí)是一個(gè)錯(cuò)誤。
我們需要在自動(dòng)化測(cè)試?yán)镒龅暮蛣偛旁趕hell里做的差不多,讓我們來(lái)將它轉(zhuǎn)換成一個(gè)自動(dòng)化測(cè)試。
應(yīng)用的測(cè)試用例安裝慣例一般放在該應(yīng)用的tests.py文件中;測(cè)試系統(tǒng)將自動(dòng)在任何以test開(kāi)頭的文件中查找測(cè)試用例。
將下面的代碼放入polls應(yīng)用下的tests.py文件中:
polls/tests.py
import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question
class QuestionMethodTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertEqual(future_question.was_published_recently(), False)
我們?cè)谶@里做的是創(chuàng)建一個(gè)django.test.TestCase子類(lèi),它具有一個(gè)方法可以創(chuàng)建一個(gè)pub_date在未來(lái)的Question實(shí)例。然后我們檢查was_published_recently()的輸出 —— 它應(yīng)該是 False.
在終端中,我們可以運(yùn)行我們的測(cè)試:
$ python manage.py test polls
你將看到類(lèi)似下面的輸出:
Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertEqual(future_question.was_published_recently(), False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
發(fā)生了如下這些事:
這個(gè)測(cè)試通知我們哪個(gè)測(cè)試失敗,甚至是錯(cuò)誤出現(xiàn)在哪一行。
我們已經(jīng)知道問(wèn)題是什么:Question.was_published_recently() 應(yīng)該返回 False,如果它的pub_date是在未來(lái)。在models.py中修復(fù)這個(gè)方法,讓它只有當(dāng)日期是在過(guò)去時(shí)才返回True :
polls/models.py
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
再次運(yùn)行測(cè)試:
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
在找出一個(gè)錯(cuò)誤之后,我們編寫(xiě)一個(gè)測(cè)試來(lái)暴露這個(gè)錯(cuò)誤,然后在代碼中更正這個(gè)錯(cuò)誤讓我們的測(cè)試通過(guò)。
未來(lái),我們的應(yīng)用可能會(huì)出許多其它的錯(cuò)誤,但是我們可以保證我們不會(huì)無(wú)意中再次引入這個(gè)錯(cuò)誤,因?yàn)楹?jiǎn)單地運(yùn)行一下這個(gè)測(cè)試就會(huì)立即提醒我們。 我們可以認(rèn)為這個(gè)應(yīng)用的這一小部分會(huì)永遠(yuǎn)安全了。
在這里,我們可以使was_published_recently() 方法更加穩(wěn)定;事實(shí)上,在修復(fù)一個(gè)錯(cuò)誤的時(shí)候引入一個(gè)新的錯(cuò)誤將是一件很令人尷尬的事。
在同一個(gè)類(lèi)中添加兩個(gè)其它的測(cè)試方法,來(lái)更加綜合地測(cè)試這個(gè)方法:
polls/tests.py
def test_was_published_recently_with_old_question(self):
"""
was_published_recently() should return False for questions whose
pub_date is older than 1 day.
"""
time = timezone.now() - datetime.timedelta(days=30)
old_question = Question(pub_date=time)
self.assertEqual(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
"""
was_published_recently() should return True for questions whose
pub_date is within the last day.
"""
time = timezone.now() - datetime.timedelta(hours=1)
recent_question = Question(pub_date=time)
self.assertEqual(recent_question.was_published_recently(), True)
現(xiàn)在我們有三個(gè)測(cè)試來(lái)保證無(wú)論發(fā)布時(shí)間是在過(guò)去、現(xiàn)在還是未來(lái) Question.was_published_recently()都將返回合理的數(shù)據(jù)。
再說(shuō)一次,polls 應(yīng)用雖然簡(jiǎn)單,但是無(wú)論它今后會(huì)變得多么復(fù)雜以及會(huì)和多少其它的應(yīng)用產(chǎn)生相互作用,我們都能保證我們剛剛為它編寫(xiě)過(guò)測(cè)試的那個(gè)方法會(huì)按照預(yù)期的那樣工作。
這個(gè)投票應(yīng)用沒(méi)有區(qū)分能力:它將會(huì)發(fā)布任何一個(gè)Question,包括 pub_date字段位于未來(lái)。我們應(yīng)該改進(jìn)這一點(diǎn)。 設(shè)定pub_date在未來(lái)應(yīng)該表示Question在此刻發(fā)布,但是直到那個(gè)時(shí)間點(diǎn)才會(huì)變得可見(jiàn)。
當(dāng)我們修復(fù)上面的錯(cuò)誤時(shí),我們先寫(xiě)測(cè)試,然后修改代碼來(lái)修復(fù)它。 事實(shí)上,這是由測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)的一個(gè)簡(jiǎn)單的例子,但做的順序并不真的重要。
在我們的第一個(gè)測(cè)試中,我們專(zhuān)注于代碼內(nèi)部的行為。 在這個(gè)測(cè)試中,我們想要通過(guò)瀏覽器從用戶(hù)的角度來(lái)檢查它的行為。
在我們?cè)囍迯?fù)任何事情之前,讓我們先查看一下我們能用到的工具。
Django提供了一個(gè)測(cè)試客戶(hù)端來(lái)模擬用戶(hù)和代碼的交互。我們可以在tests.py 甚至在shell 中使用它。
我們將再次以shell開(kāi)始,但是我們需要做很多在tests.py中不必做的事。首先是在 shell中設(shè)置測(cè)試環(huán)境:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
setup_test_environment()安裝一個(gè)模板渲染器,可以使我們來(lái)檢查響應(yīng)的一些額外屬性比如response.context,否則是訪問(wèn)不到的。請(qǐng)注意,這種方法不會(huì)建立一個(gè)測(cè)試數(shù)據(jù)庫(kù),所以以下命令將運(yùn)行在現(xiàn)有的數(shù)據(jù)庫(kù)上,輸出的內(nèi)容也會(huì)根據(jù)你已經(jīng)創(chuàng)建的Question不同而稍有不同。
下一步我們需要導(dǎo)入測(cè)試客戶(hù)端類(lèi)(在之后的tests.py 中,我們將使用django.test.TestCase類(lèi),它具有自己的客戶(hù)端,將不需要導(dǎo)入這個(gè)類(lèi)):
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
這些都做完之后,我們可以讓這個(gè)客戶(hù)端來(lái)為我們做一些事:
>>> # 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 hardcoded 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 Question
>>> from django.utils import timezone
>>> # create a Question and save it
>>> q = Question(question_text="Who is your favorite Beatle?", pub_date=timezone.now())
>>> q.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'
>>> # If the following doesn't work, you probably omitted the call to
>>> # setup_test_environment() described above
>>> response.context['latest_question_list']
[<Question: Who is your favorite Beatle?>]
投票的列表顯示還沒(méi)有發(fā)布的投票(即pub_date在未來(lái)的投票)。讓我們來(lái)修復(fù)它。
在教程 4中,我們介紹了一個(gè)繼承ListView的基于類(lèi)的視圖:
polls/views.py
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
response.context_data['latest_question_list'] 取出由視圖放置在context 中的數(shù)據(jù)。
我們需要修改get_queryset方法并讓它將日期與timezone.now()進(jìn)行比較。首先我們需要添加一行導(dǎo)入:
polls/views.py
from django.utils import timezone
然后我們必須像這樣修改get_queryset方法:
polls/views.py
def get_queryset(self):
"""
Return the last five published questions (not including those set to be
published in the future).
"""
return Question.objects.filter(
pub_date__lte=timezone.now()
).order_by('-pub_date')[:5]
Question.objects.filter(pub_date__lte=timezone.now()) 返回一個(gè)查詢(xún)集,包含pub_date小于等于timezone.now的Question。
啟動(dòng)服務(wù)器、在瀏覽器中載入站點(diǎn)、創(chuàng)建一些發(fā)布時(shí)間在過(guò)去和將來(lái)的Questions ,然后檢驗(yàn)只有已經(jīng)發(fā)布的Question會(huì)展示出來(lái),現(xiàn)在你可以對(duì)自己感到滿(mǎn)意了。你不想每次修改可能與這相關(guān)的代碼時(shí)都重復(fù)這樣做 —— 所以讓我們基于以上shell會(huì)話(huà)中的內(nèi)容,再編寫(xiě)一個(gè)測(cè)試。
將下面的代碼添加到polls/tests.py:
polls/tests.py
from django.core.urlresolvers import reverse
我們將創(chuàng)建一個(gè)快捷函數(shù)來(lái)創(chuàng)建Question,同時(shí)我們要?jiǎng)?chuàng)建一個(gè)新的測(cè)試類(lèi):
polls/tests.py
def create_question(question_text, days):
"""
Creates a question with the given `question_text` published the given
number of `days` offset to now (negative for questions published
in the past, positive for questions that have yet to be published).
"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text,
pub_date=time)
class QuestionViewTests(TestCase):
def test_index_view_with_no_questions(self):
"""
If no questions 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_question_list'], [])
def test_index_view_with_a_past_question(self):
"""
Questions with a pub_date in the past should be displayed on the
index page.
"""
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_index_view_with_a_future_question(self):
"""
Questions with a pub_date in the future should not be displayed on
the index page.
"""
create_question(question_text="Future question.", days=30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls are available.",
status_code=200)
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_index_view_with_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
should be displayed.
"""
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_index_view_with_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
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.>']
)
讓我們更詳細(xì)地看下以上這些內(nèi)容。
第一個(gè)是Question的快捷函數(shù)create_question,將重復(fù)創(chuàng)建Question的過(guò)程封裝在一起。
test_index_view_with_no_questions不創(chuàng)建任何Question,但會(huì)檢查消息“No polls are available.” 并驗(yàn)證latest_question_list為空。注意django.test.TestCase類(lèi)提供一些額外的斷言方法。在這些例子中,我們使用assertContains() 和 assertQuerysetEqual()。
在test_index_view_with_a_past_question中,我們創(chuàng)建一個(gè)Question并驗(yàn)證它是否出現(xiàn)在列表中。
在test_index_view_with_a_future_question中,我們創(chuàng)建一個(gè)pub_date 在未來(lái)的Question。數(shù)據(jù)庫(kù)會(huì)為每一個(gè)測(cè)試方法進(jìn)行重置,所以第一個(gè)Question已經(jīng)不在那里,因此首頁(yè)面里不應(yīng)該有任何Question。
等等。 事實(shí)上,我們是在用測(cè)試模擬站點(diǎn)上的管理員輸入和用戶(hù)體驗(yàn),檢查針對(duì)系統(tǒng)每一個(gè)狀態(tài)和狀態(tài)的新變化,發(fā)布的是預(yù)期的結(jié)果。
一切都運(yùn)行得很好; 然而,即使未來(lái)發(fā)布的Question不會(huì)出現(xiàn)在index中,如果用戶(hù)知道或者猜出正確的URL依然可以訪問(wèn)它們。所以我們需要給DetailView添加一個(gè)這樣的約束:
polls/views.py
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
當(dāng)然,我們將增加一些測(cè)試來(lái)檢驗(yàn)pub_date 在過(guò)去的Question 可以顯示出來(lái),而pub_date在未來(lái)的不可以:
polls/tests.py
class QuestionIndexDetailTests(TestCase):
def test_detail_view_with_a_future_question(self):
"""
The detail view of a question with a pub_date in the future should
return a 404 not found.
"""
future_question = create_question(question_text='Future question.',
days=5)
response = self.client.get(reverse('polls:detail',
args=(future_question.id,)))
self.assertEqual(response.status_code, 404)
def test_detail_view_with_a_past_question(self):
"""
The detail view of a question with a pub_date in the past should
display the question's text.
"""
past_question = create_question(question_text='Past Question.',
days=-5)
response = self.client.get(reverse('polls:detail',
args=(past_question.id,)))
self.assertContains(response, past_question.question_text,
status_code=200)
我們應(yīng)該添加一個(gè)類(lèi)似get_queryset的方法到ResultsView并為該視圖創(chuàng)建一個(gè)新的類(lèi)。這將與我們剛剛創(chuàng)建的非常類(lèi)似;實(shí)際上將會(huì)有許多重復(fù)。
我們還可以在其它方面改進(jìn)我們的應(yīng)用,并隨之不斷增加測(cè)試。例如,發(fā)布一個(gè)沒(méi)有Choices的Questions就顯得傻傻的。所以,我們的視圖應(yīng)該檢查這點(diǎn)并排除這些 Questions。我們的測(cè)試應(yīng)該創(chuàng)建一個(gè)不帶Choices 的 Question然后測(cè)試它不會(huì)發(fā)布出來(lái), 同時(shí)創(chuàng)建一個(gè)類(lèi)似的帶有 Choices的Question 并驗(yàn)證它會(huì) 發(fā)布出來(lái)。
也許登陸的用戶(hù)應(yīng)該被允許查看還沒(méi)發(fā)布的 Questions,但普通游客不行。 再說(shuō)一次:無(wú)論添加什么代碼來(lái)完成這個(gè)要求,需要提供相應(yīng)的測(cè)試代碼,無(wú)論你是否是先編寫(xiě)測(cè)試然后讓這些代碼通過(guò)測(cè)試,還是先用代碼解決其中的邏輯然后編寫(xiě)測(cè)試來(lái)證明它。
從某種程度上來(lái)說(shuō),你一定會(huì)查看你的測(cè)試,然后想知道是否你的測(cè)試程序過(guò)于臃腫,這將我們帶向下面的內(nèi)容:
看起來(lái)我們的測(cè)試代碼的增長(zhǎng)正在失去控制。 以這樣的速度,測(cè)試的代碼量將很快超過(guò)我們的應(yīng)用,對(duì)比我們其它優(yōu)美簡(jiǎn)潔的代碼,重復(fù)毫無(wú)美感。
沒(méi)關(guān)系。讓它們繼續(xù)增長(zhǎng)。最重要的是,你可以寫(xiě)一個(gè)測(cè)試一次,然后忘了它。 當(dāng)你繼續(xù)開(kāi)發(fā)你的程序時(shí),它將繼續(xù)執(zhí)行有用的功能。
有時(shí),測(cè)試需要更新。 假設(shè)我們修改我們的視圖使得只有具有Choices的 Questions 才會(huì)發(fā)布。在這種情況下,我們?cè)S多已經(jīng)存在的測(cè)試都將失敗 —— 這會(huì)告訴我們哪些測(cè)試需要被修改來(lái)使得它們保持最新,所以從某種程度上講,測(cè)試可以自己照顧自己。
在最壞的情況下,在你的開(kāi)發(fā)過(guò)程中,你會(huì)發(fā)現(xiàn)許多測(cè)試現(xiàn)在變得冗余。 即使這樣,也不是問(wèn)題;對(duì)測(cè)試來(lái)說(shuō),冗余是一件好 事。
只要你的測(cè)試被合理地組織,它們就不會(huì)變得難以管理。 從經(jīng)驗(yàn)上來(lái)說(shuō),好的做法是:
本教程只介紹了一些基本的測(cè)試。 還有很多你可以做,有許多非常有用的工具可以隨便使用來(lái)你實(shí)現(xiàn)一些非常聰明的做法。
例如,雖然我們的測(cè)試覆蓋了模型的內(nèi)部邏輯和視圖發(fā)布信息的方式,你可以使用一個(gè)“瀏覽器”框架例如Selenium來(lái)測(cè)試你的HTML文件在瀏覽器中真實(shí)渲染的樣子。 這些工具不僅可以讓你檢查你的Django代碼的行為,還能夠檢查你的JavaScript的行為。 它會(huì)啟動(dòng)一個(gè)瀏覽器,并開(kāi)始與你的網(wǎng)站進(jìn)行交互,就像有一個(gè)人在操縱一樣,非常值得一看! Django 包含一個(gè)LiveServerTestCase來(lái)幫助與Selenium 這樣的工具集成。
如果你有一個(gè)復(fù)雜的應(yīng)用,你可能為了實(shí)現(xiàn)continuous integration,想在每次提交代碼后對(duì)代碼進(jìn)行自動(dòng)化測(cè)試,讓代碼自動(dòng) —— 至少是部分自動(dòng) —— 地來(lái)控制它的質(zhì)量。
發(fā)現(xiàn)你應(yīng)用中未經(jīng)測(cè)試的代碼的一個(gè)好方法是檢查測(cè)試代碼的覆蓋率。 這也有助于識(shí)別脆弱的甚至死代碼。 如果你不能測(cè)試一段代碼,這通常意味著這些代碼需要被重構(gòu)或者移除。 Coverage將幫助我們識(shí)別死代碼。 查看與coverage.py 集成來(lái)了解更多細(xì)節(jié)。
Django 中的測(cè)試有關(guān)于測(cè)試更加全面的信息。
關(guān)于測(cè)試的完整細(xì)節(jié),請(qǐng)查看Django 中的測(cè)試。
當(dāng)你對(duì)Django 視圖的測(cè)試感到滿(mǎn)意后,請(qǐng)閱讀本教程的第6部分來(lái) 了解靜態(tài)文件的管理。
譯者:Django 文檔協(xié)作翻譯小組,原文:Part 5: Testing。
本文以 CC BY-NC-SA 3.0 協(xié)議發(fā)布,轉(zhuǎn)載請(qǐng)保留作者署名和文章出處。
Django 文檔協(xié)作翻譯小組人手緊缺,有興趣的朋友可以加入我們,完全公益性質(zhì)。交流群:467338606。