測(cè)試代碼,是用來(lái)檢查你的代碼能否正常運(yùn)行的程序。
測(cè)試在不同的層次中都存在。有些測(cè)試只關(guān)注某個(gè)很小的細(xì)節(jié)(某個(gè)模型的某個(gè)方法的返回值是否滿(mǎn)足預(yù)期?),而另一些測(cè)試可能檢查對(duì)某個(gè)軟件的一系列操作(某一用戶(hù)輸入序列是否造成了預(yù)期的結(jié)果?)。其實(shí)這和我們?cè)?教程第 2 部分里做的并沒(méi)有什么不同,我們使用 ?shell
?來(lái)測(cè)試某一方法的功能,或者運(yùn)行某個(gè)應(yīng)用并輸入數(shù)據(jù)來(lái)檢查它的行為。
真正不同的地方在于,自動(dòng)化 測(cè)試是由某個(gè)系統(tǒng)幫你自動(dòng)完成的。當(dāng)你創(chuàng)建好了一系列測(cè)試,每次修改應(yīng)用代碼后,就可以自動(dòng)檢查出修改后的代碼是否還像你曾經(jīng)預(yù)期的那樣正常工作。你不需要花費(fèi)大量時(shí)間來(lái)進(jìn)行手動(dòng)測(cè)試。
但是,為什么需要測(cè)試呢?又為什么是現(xiàn)在呢?
你可能覺(jué)得學(xué) Python/Django 對(duì)你來(lái)說(shuō)已經(jīng)很滿(mǎn)足了,再學(xué)一些新東西的話(huà)看起來(lái)有點(diǎn)負(fù)擔(dān)過(guò)重并且沒(méi)什么必要。畢竟,我們的投票應(yīng)用看起來(lái)已經(jīng)完美工作了。寫(xiě)一些自動(dòng)測(cè)試并不能讓它工作的更好。如果寫(xiě)一個(gè)投票應(yīng)用是你想用 Django 完成的唯一工作,那你確實(shí)沒(méi)必要學(xué)寫(xiě)測(cè)試。但是如果你還想寫(xiě)更復(fù)雜的項(xiàng)目,現(xiàn)在就是學(xué)習(xí)測(cè)試寫(xiě)法的最好時(shí)機(jī)了。
在某種程度上,能夠「判斷出代碼是否正常工作」的測(cè)試,就稱(chēng)得上是個(gè)令人滿(mǎn)意的了。在更復(fù)雜的應(yīng)用程序中,組件之間可能會(huì)有數(shù)十個(gè)復(fù)雜的交互。
對(duì)其中某一組件的改變,也有可能會(huì)造成意想不到的結(jié)果。判斷「代碼是否正常工作」意味著你需要用大量的數(shù)據(jù)來(lái)完整的測(cè)試全部代碼的功能,以確保你的小修改沒(méi)有對(duì)應(yīng)用整體造成破壞——這太費(fèi)時(shí)間了。
尤其是當(dāng)你發(fā)現(xiàn)自動(dòng)化測(cè)試能在幾秒鐘之內(nèi)幫你完成這件事時(shí),就更會(huì)覺(jué)得手動(dòng)測(cè)試實(shí)在是太浪費(fèi)時(shí)間了。當(dāng)某人寫(xiě)出錯(cuò)誤的代碼時(shí),自動(dòng)化測(cè)試還能幫助你定位錯(cuò)誤代碼的位置。
有時(shí)候你會(huì)覺(jué)得,和富有創(chuàng)造性和生產(chǎn)力的業(yè)務(wù)代碼比起來(lái),編寫(xiě)枯燥的測(cè)試代碼實(shí)在是太無(wú)聊了,特別是當(dāng)你知道你的代碼完全沒(méi)有問(wèn)題的時(shí)候。
然而,編寫(xiě)測(cè)試還是要比花費(fèi)幾個(gè)小時(shí)手動(dòng)測(cè)試你的應(yīng)用,或者為了找到某個(gè)小錯(cuò)誤而胡亂翻看代碼要有意義的多。
「測(cè)試是開(kāi)發(fā)的對(duì)立面」,這種思想是不對(duì)的。
如果沒(méi)有測(cè)試,整個(gè)應(yīng)用的行為意圖會(huì)變得更加的不清晰。甚至當(dāng)你在看自己寫(xiě)的代碼時(shí)也是這樣,有時(shí)候你需要仔細(xì)研讀一段代碼才能搞清楚它有什么用。
而測(cè)試的出現(xiàn)改變了這種情況。測(cè)試就好像是從內(nèi)部仔細(xì)檢查你的代碼,當(dāng)有些地方出錯(cuò)時(shí),這些地方將會(huì)變得很顯眼——就算你自己沒(méi)有意識(shí)到那里寫(xiě)錯(cuò)了。
你也許遇到過(guò)這種情況:你編寫(xiě)了一個(gè)絕贊的軟件,但是其他開(kāi)發(fā)者看都不看它一眼,因?yàn)樗鄙贉y(cè)試。沒(méi)有測(cè)試的代碼不值得信任。 Django 最初開(kāi)發(fā)者之一的 Jacob Kaplan-Moss 說(shuō)過(guò):“項(xiàng)目規(guī)劃時(shí)沒(méi)有包含測(cè)試是不科學(xué)的。”
其他的開(kāi)發(fā)者希望在正式使用你的代碼前看到它通過(guò)了測(cè)試,這是你需要寫(xiě)測(cè)試的另一個(gè)重要原因。
前面的幾點(diǎn)都是從單人開(kāi)發(fā)的角度來(lái)說(shuō)的。復(fù)雜的應(yīng)用可能由團(tuán)隊(duì)維護(hù)。測(cè)試的存在保證了協(xié)作者不會(huì)不小心破壞了了你的代碼(也保證你不會(huì)不小心弄壞他們的)。如果你想作為一個(gè) Django 程序員謀生的話(huà),你必須擅長(zhǎng)編寫(xiě)測(cè)試!
有好幾種不同的方法可以寫(xiě)測(cè)試。
一些開(kāi)發(fā)者遵循 "測(cè)試驅(qū)動(dòng)" 的開(kāi)發(fā)原則,他們?cè)趯?xiě)代碼之前先寫(xiě)測(cè)試。這種方法看起來(lái)有點(diǎn)反直覺(jué),但事實(shí)上,這和大多數(shù)人日常的做法是相吻合的。我們會(huì)先描述一個(gè)問(wèn)題,然后寫(xiě)代碼來(lái)解決它?!笢y(cè)試驅(qū)動(dòng)」的開(kāi)發(fā)方法只是將問(wèn)題的描述抽象為了 Python 的測(cè)試樣例。
更普遍的情況是,一個(gè)剛接觸自動(dòng)化測(cè)試的新手更傾向于先寫(xiě)代碼,然后再寫(xiě)測(cè)試。雖然提前寫(xiě)測(cè)試可能更好,但是晚點(diǎn)寫(xiě)起碼也比沒(méi)有強(qiáng)。
有時(shí)候很難決定從哪里開(kāi)始下手寫(xiě)測(cè)試。如果你才寫(xiě)了幾千行 Python 代碼,選擇從哪里開(kāi)始寫(xiě)測(cè)試確實(shí)不怎么簡(jiǎn)單。如果是這種情況,那么在你下次修改代碼(比如加新功能,或者修復(fù) Bug)之前寫(xiě)個(gè)測(cè)試是比較合理且有效的。
所以,我們現(xiàn)在就開(kāi)始寫(xiě)吧。
首先得有個(gè)bug
幸運(yùn)的是,我們的 ?polls
?應(yīng)用現(xiàn)在就有一個(gè)小 bug 需要被修復(fù):我們的要求是如果 ?Question
?是在一天之內(nèi)發(fā)布的,? Question.was_published_recently()
? 方法將會(huì)返回 ?True
?,然而現(xiàn)在這個(gè)方法在 ?Question
?的 ?pub_date
?字段比當(dāng)前時(shí)間還晚時(shí)也會(huì)返回 ?True
?(這是個(gè) Bug)。
用?djadmin:`shell`
?命令確認(rèn)一下這個(gè)方法的日期bug
...\> py manage.py 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
因?yàn)閷?lái)發(fā)生的是肯定不是最近發(fā)生的,所以代碼明顯是錯(cuò)誤的。
我們剛剛在 ?shell
?里做的測(cè)試也就是自動(dòng)化測(cè)試應(yīng)該做的工作。所以我們來(lái)把它改寫(xiě)成自動(dòng)化的吧。
按照慣例,Django 應(yīng)用的測(cè)試應(yīng)該寫(xiě)在應(yīng)用的 ?tests.py
? 文件里。測(cè)試系統(tǒng)會(huì)自動(dòng)的在所有以 ?tests
?開(kāi)頭的文件里尋找并執(zhí)行測(cè)試代碼。
將下面的代碼寫(xiě)入 ?polls
?應(yīng)用里的 ?tests.py
? 文件內(nèi):
import datetime
from django.test import TestCase
from django.utils import timezone
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)
我們創(chuàng)建了一個(gè) ?django.test.TestCase
? 的子類(lèi),并添加了一個(gè)方法,此方法創(chuàng)建一個(gè) ?pub_date
是未來(lái)某天的 ?Question
?實(shí)例。然后檢查它的 ?was_published_recently()
? 方法的返回值——它應(yīng)該是 ?False
?。
在終端中,我們通過(guò)輸入以下代碼運(yùn)行測(cè)試:
...\> py manage.py test polls
你將會(huì)看到運(yùn)行結(jié)果:
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'...
發(fā)生了什么呢?以下是自動(dòng)化測(cè)試的運(yùn)行過(guò)程:
python manage.py test polls
? 將會(huì)尋找 polls 應(yīng)用里的測(cè)試代碼django.test.TestCase
? 的一個(gè)子類(lèi)test
?開(kāi)頭的方法。test_was_published_recently_with_future_question
? 方法中,它創(chuàng)建了一個(gè) ?pub_date
?值為 30 天后的 ?Question
?實(shí)例。assertls()
? 方法,發(fā)現(xiàn) ?was_published_recently()
? 返回了 ?True
?,而我們期望它返回 ?False
?。測(cè)試系統(tǒng)通知我們哪些測(cè)試樣例失敗了,和造成測(cè)試失敗的代碼所在的行號(hào)。
我們?cè)缫阎溃?dāng) ?pub_date
?為未來(lái)某天時(shí), ?Question.was_published_recently()
? 應(yīng)該返回 ?False
?。我們修改 ?models.py
? 里的方法,讓它只在日期是過(guò)去式的時(shí)候才返回 ?True
?:
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'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
發(fā)現(xiàn) bug 后,我們編寫(xiě)了能夠暴露這個(gè) bug 的自動(dòng)化測(cè)試。在修復(fù) bug 之后,我們的代碼順利的通過(guò)了測(cè)試。
將來(lái),我們的應(yīng)用可能會(huì)出現(xiàn)其他的問(wèn)題,但是我們可以肯定的是,一定不會(huì)再次出現(xiàn)這個(gè) bug,因?yàn)橹灰\(yùn)行一遍測(cè)試,就會(huì)立刻收到警告。我們可以認(rèn)為應(yīng)用的這一小部分代碼永遠(yuǎn)是安全的。
我們已經(jīng)搞定一小部分了,現(xiàn)在可以考慮全面的測(cè)試 ?was_published_recently()
? 這個(gè)方法以確定它的安全性,然后就可以把這個(gè)方法穩(wěn)定下來(lái)了。事實(shí)上,在修復(fù)一個(gè) bug 時(shí)不小心引入另一個(gè) bug 會(huì)是非常令人尷尬的。
我們?cè)谏洗螌?xiě)的類(lèi)里再增加兩個(gè)測(cè)試,來(lái)更全面的測(cè)試這個(gè)方法:
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)
現(xiàn)在,我們有三個(gè)測(cè)試來(lái)確保 ?Question.was_published_recently()
? 方法對(duì)于過(guò)去,最近,和將來(lái)的三種情況都返回正確的值。
再次申明,盡管 ?polls
現(xiàn)在是個(gè)小型的應(yīng)用,但是無(wú)論它以后變得到多么復(fù)雜,無(wú)論他和其他代碼如何交互,我們可以在一定程度上保證我們?yōu)橹帉?xiě)測(cè)試的方法將按照預(yù)期的方式運(yùn)行。
我們的投票應(yīng)用對(duì)所有問(wèn)題都一視同仁:它將會(huì)發(fā)布所有的問(wèn)題,也包括那些 ?pub_date
?字段值是未來(lái)的問(wèn)題。我們應(yīng)該改善這一點(diǎn)。如果 ?pub_date
?設(shè)置為未來(lái)某天,這應(yīng)該被解釋為這個(gè)問(wèn)題將在所填寫(xiě)的時(shí)間點(diǎn)才被發(fā)布,而在之前是不可見(jiàn)的。
為了修復(fù)上述 bug ,我們這次先編寫(xiě)測(cè)試,然后再去改代碼。事實(shí)上,這是一個(gè)「測(cè)試驅(qū)動(dòng)」開(kāi)發(fā)模式的實(shí)例,但其實(shí)這兩者的順序不太重要。
在我們的第一個(gè)測(cè)試中,我們關(guān)注代碼的內(nèi)部行為。我們通過(guò)模擬用戶(hù)使用瀏覽器訪(fǎng)問(wèn)被測(cè)試的應(yīng)用來(lái)檢查代碼行為是否符合預(yù)期。
在我們動(dòng)手之前,先看看需要用到的工具們。
Django 提供了一個(gè)供測(cè)試使用的 ?Client
?來(lái)模擬用戶(hù)和視圖層代碼的交互。我們能在 ?tests.py
? 甚至是 ?shell
?中使用它。
我們依照慣例從 ?shell
?開(kāi)始,首先我們要做一些在 ?tests.py
? 里不是必須的準(zhǔn)備工作。第一步是在 ?shell
?中配置測(cè)試環(huán)境:
...\> py manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
?setup_test_environment()
? 安裝了一個(gè)模板渲染器,這將使我們能夠檢查響應(yīng)上的一些額外屬性,如 ?response.context
?,否則將無(wú)法使用此功能。請(qǐng)注意,這個(gè)方法 不會(huì) 建立一個(gè)測(cè)試數(shù)據(jù)庫(kù),所以下面的內(nèi)容將針對(duì)現(xiàn)有的數(shù)據(jù)庫(kù)運(yùn)行,輸出結(jié)果可能略有不同,這取決于你已經(jīng)創(chuàng)建了哪些問(wèn)題。如果你在 ?settings.py
? 中的 ?TIME_ZONE
?不正確,你可能會(huì)得到意外的結(jié)果。如果你不記得之前的配置,請(qǐng)?jiān)诶^續(xù)之前檢查。
然后我們需要導(dǎo)入 ?django.test.TestCase
? 類(lèi)(在后續(xù) ?tests.py
? 的實(shí)例中我們將會(huì)使用 ?django.test.TestCase
? 類(lèi),這個(gè)類(lèi)里包含了自己的 client 實(shí)例,所以不需要這一步):
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
搞定了之后,我們可以要求 client 為我們工作了:
>>> # get a response from '/'
>>> response = client.get('/')
Not Found: /
>>> # we should expect a 404 from that address; if you instead see an
>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably
>>> # omitted the setup_test_environment() call described earlier.
>>> 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.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/">What's up?</a></li>\n \n </ul>\n\n'
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>
現(xiàn)在的投票列表會(huì)顯示將來(lái)的投票( ?pub_date
?值是未來(lái)的某天)。我們來(lái)修復(fù)這個(gè)問(wèn)題。
在 教程的第 4 部分 里,我們介紹了基于 ?ListView
?的視圖類(lèi):
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]
我們需要改進(jìn) ?get_queryset()
? 方法,讓他它能通過(guò)將 Question 的 pub_data 屬性與 ?timezone.now()
? 相比較來(lái)判斷是否應(yīng)該顯示此 Question。首先我們需要一行 import 語(yǔ)句:
from django.utils import timezone
然后我們把 ?get_queryset
?方法改寫(xiě)成下面這樣:
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
? 的問(wèn)題。
啟動(dòng)服務(wù)器、在瀏覽器中載入站點(diǎn)、創(chuàng)建一些發(fā)布時(shí)間在過(guò)去和將來(lái)的 ?Questions
?,然后檢驗(yàn)只有已經(jīng)發(fā)布的 ?Questions
?會(huì)展示出來(lái),現(xiàn)在你可以對(duì)自己感到滿(mǎn)意了。你不想每次修改可能與這相關(guān)的代碼時(shí)都重復(fù)這樣做 —— 所以讓我們基于以上 ?shell
?會(huì)話(huà)中的內(nèi)容,再編寫(xiě)一個(gè)測(cè)試。
將下面的代碼添加到 ?polls/tests.py
?:
from django.urls import reverse
然后我們寫(xiě)一個(gè)公用的快捷函數(shù)用于創(chuàng)建投票問(wèn)題,再為視圖創(chuàng)建一個(gè)測(cè)試類(lèi):
def create_question(question_text, days):
"""
Create a question with the given `question_text` and 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 QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""
If no questions exist, an appropriate message is 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_past_question(self):
"""
Questions with a pub_date in the past are displayed on the
index page.
"""
question = create_question(question_text="Past question.", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question],
)
def test_future_question(self):
"""
Questions with a pub_date in the future aren't 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.")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_future_question_and_past_question(self):
"""
Even if both past and future questions exist, only past questions
are displayed.
"""
question = 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],
)
def test_two_past_questions(self):
"""
The questions index page may display multiple questions.
"""
question1 = create_question(question_text="Past question 1.", days=-30)
question2 = create_question(question_text="Past question 2.", days=-5)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question2, question1],
)
讓我們更詳細(xì)地看下以上這些內(nèi)容。
首先是一個(gè)快捷函數(shù) ?create_question
?,它封裝了創(chuàng)建投票的流程,減少了重復(fù)代碼。
?test_no_questions
方法里沒(méi)有創(chuàng)建任何投票,它檢查返回的網(wǎng)頁(yè)上有沒(méi)有 "No polls are available." 這段消息和 ?latest_question_list
?是否為空。注意到 ?django.test.TestCase
? 類(lèi)提供了一些額外的 ?assertion
?方法,在這個(gè)例子中,我們使用了 ?assertContains()
? 和 ?assertQuerysetEqual()
?。
在 ?test_past_question
?方法中,我們創(chuàng)建了一個(gè)投票并檢查它是否出現(xiàn)在列表中。
在 ?test_future_question
?中,我們創(chuàng)建 ?pub_date
?在未來(lái)某天的投票。數(shù)據(jù)庫(kù)會(huì)在每次調(diào)用測(cè)試方法前被重置,所以第一個(gè)投票已經(jīng)沒(méi)了,所以主頁(yè)中應(yīng)該沒(méi)有任何投票。
剩下的那些也都差不多。實(shí)際上,測(cè)試就是假裝一些管理員的輸入,然后通過(guò)用戶(hù)端的表現(xiàn)是否符合預(yù)期來(lái)判斷新加入的改變是否破壞了原有的系統(tǒng)狀態(tài)。
我們的工作似乎已經(jīng)很完美了?不,還有一個(gè)問(wèn)題:就算在發(fā)布日期時(shí)未來(lái)的那些投票不會(huì)在目錄頁(yè) index 里出現(xiàn),但是如果用戶(hù)知道或者猜到正確的 URL ,還是可以訪(fǎng)問(wèn)到它們。所以我們得在 ?DetailView
里增加一些約束:
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())
然后,我們應(yīng)該增加一些測(cè)試來(lái)檢驗(yàn) ?pub_date
在過(guò)去的 ?Question
能夠被顯示出來(lái),而 ?pub_date
在未來(lái)的則不可以:
class QuestionDetailViewTests(TestCase):
def test_future_question(self):
"""
The detail view of a question with a pub_date in the future
returns a 404 not found.
"""
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):
"""
The detail view of a question with a pub_date in the past
displays the question's 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)
我們應(yīng)該給 ?ResultsView
?也增加一個(gè)類(lèi)似的 ?get_queryset
方法,并且為它創(chuàng)建測(cè)試。這和我們之前干的差不多,事實(shí)上,基本就是重復(fù)一遍。
我們還可以從各個(gè)方面改進(jìn)投票應(yīng)用,但是測(cè)試會(huì)一直伴隨我們。比方說(shuō),在目錄頁(yè)上顯示一個(gè)沒(méi)有選項(xiàng) ?Choices
的投票問(wèn)題就沒(méi)什么意義。我們可以檢查并排除這樣的投票題。測(cè)試可以創(chuàng)建一個(gè)沒(méi)有選項(xiàng)的投票,然后檢查它是否被顯示在目錄上。當(dāng)然也要?jiǎng)?chuàng)建一個(gè)有選項(xiàng)的投票,然后確認(rèn)它確實(shí)被顯示了。
恩,也許你想讓管理員能在目錄上看見(jiàn)未被發(fā)布的那些投票,但是普通用戶(hù)看不到。不管怎么說(shuō),如果你想要增加一個(gè)新功能,那么同時(shí)一定要為它編寫(xiě)測(cè)試。不過(guò)你是先寫(xiě)代碼還是先寫(xiě)測(cè)試那就隨你了。
在未來(lái)的某個(gè)時(shí)刻,你一定會(huì)去查看測(cè)試代碼,然后開(kāi)始懷疑:「這么多的測(cè)試不會(huì)使代碼越來(lái)越復(fù)雜嗎?」。別著急,我們馬上就會(huì)談到這一點(diǎn)。
貌似我們的測(cè)試多的快要失去控制了。按照這樣發(fā)展下去,測(cè)試代碼就要變得比應(yīng)用的實(shí)際代碼還要多了。而且測(cè)試代碼大多都是重復(fù)且不優(yōu)雅的,特別是在和業(yè)務(wù)代碼比起來(lái)的時(shí)候,這種感覺(jué)更加明顯。
但是這沒(méi)關(guān)系! 就讓測(cè)試代碼繼續(xù)肆意增長(zhǎng)吧。大部分情況下,你寫(xiě)完一個(gè)測(cè)試之后就可以忘掉它了。在你繼續(xù)開(kāi)發(fā)的過(guò)程中,它會(huì)一直默默無(wú)聞地為你做貢獻(xiàn)的。
但有時(shí)測(cè)試也需要更新。想象一下如果我們修改了視圖,只顯示有選項(xiàng)的那些投票,那么只前寫(xiě)的很多測(cè)試就都會(huì)失敗。但這也明確地告訴了我們哪些測(cè)試需要被更新,所以測(cè)試也會(huì)測(cè)試自己。
最壞的情況是,當(dāng)你繼續(xù)開(kāi)發(fā)的時(shí)候,發(fā)現(xiàn)之前的一些測(cè)試現(xiàn)在看來(lái)是多余的。但是這也不是什么問(wèn)題,多做些測(cè)試也不錯(cuò)。
如果你對(duì)測(cè)試有個(gè)整體規(guī)劃,那么它們就幾乎不會(huì)變得混亂。下面有幾條好的建議:
TestClass
?在本教程中,我們僅僅是了解了測(cè)試的基礎(chǔ)知識(shí)。你能做的還有很多,而且世界上有很多有用的工具來(lái)幫你完成這些有意義的事。
舉個(gè)例子,在上述的測(cè)試中,我們已經(jīng)從代碼邏輯和視圖響應(yīng)的角度檢查了應(yīng)用的輸出,現(xiàn)在你可以從一個(gè)更加 "in-browser" 的角度來(lái)檢查最終渲染出的 HTML 是否符合預(yù)期,使用 Selenium 可以很輕松的完成這件事。這個(gè)工具不僅可以測(cè)試 Django 框架里的代碼,還可以檢查其他部分,比如說(shuō)你的 JavaScript。它假裝成是一個(gè)正在和你站點(diǎn)進(jìn)行交互的瀏覽器,就好像有個(gè)真人在訪(fǎng)問(wèn)網(wǎng)站一樣!Django 它提供了 ?LiveServerTestCase
來(lái)和 Selenium 這樣的工具進(jìn)行交互。
如果你在開(kāi)發(fā)一個(gè)很復(fù)雜的應(yīng)用的話(huà),你也許想在每次提交代碼時(shí)自動(dòng)運(yùn)行測(cè)試,也就是我們所說(shuō)的持續(xù)集成 continuous integration ,這樣就能實(shí)現(xiàn)質(zhì)量控制的自動(dòng)化,起碼是部分自動(dòng)化。
一個(gè)找出代碼中未被測(cè)試部分的方法是檢查代碼覆蓋率。它有助于找出代碼中的薄弱部分和無(wú)用部分。如果你無(wú)法測(cè)試一段代碼,通常說(shuō)明這段代碼需要被重構(gòu)或者刪除。
更多建議: