隨著網(wǎng)站的增長(zhǎng),他們?cè)絹碓诫y以手動(dòng)測(cè)試。不僅要進(jìn)行更多的測(cè)試,而且隨著組件之間的交互變得越來越復(fù)雜,一個(gè)區(qū)域的小改變可能會(huì)影響到其他區(qū)域,所以需要做更多的改變來確保一切正常運(yùn)行,并且在進(jìn)行更多更改時(shí)不會(huì)引入錯(cuò)誤。減輕這些問題的一種方法是編寫自動(dòng)化測(cè)試,每當(dāng)您進(jìn)行更改時(shí),都可以輕松可靠地運(yùn)行測(cè)試。本教程演示如何使用Django的測(cè)試框架自動(dòng)化您的網(wǎng)站的單元測(cè)試。 先決條件: | 完成之前的所有教程主題,包括 Django教程 9:使用表單。 |
---|
目標(biāo): | 了解如何為基于 Django 的網(wǎng)站編寫單元測(cè)試。 |
---|
LocalLibrary 目前有頁面顯示所有書本和作者的列表,書本和作者項(xiàng)目的詳細(xì)視圖,續(xù)借BookInstances 的頁面,以及創(chuàng)建,更新和刪除作者項(xiàng)目的頁面(如果您完成了Django 教程 9:使用表單中的自我挑戰(zhàn),也可以創(chuàng)建,更新和刪除書本記錄)。即使使用這個(gè)相對(duì)較小的站點(diǎn),手動(dòng)導(dǎo)航到每個(gè)頁面,并且表面地檢查一切是否按預(yù)期工作,可能需要幾分鐘。當(dāng)我們進(jìn)行更改,并擴(kuò)展網(wǎng)站時(shí),手動(dòng)檢查所有內(nèi)容 “正常” 工作所需的時(shí)間只會(huì)增長(zhǎng)。如果我們繼續(xù)這樣做,最終我們將花費(fèi)大部分時(shí)間進(jìn)行測(cè)試,并且很少有時(shí)間來改進(jìn)我們的代碼。 自動(dòng)化測(cè)試可以真正幫助解決這個(gè)問題!顯而易見的好處,是它們可以比手動(dòng)測(cè)試運(yùn)行得更快,可以測(cè)試更底層級(jí)別的細(xì)節(jié),并且每次都測(cè)試完全相同的功能(人類測(cè)試員遠(yuǎn)遠(yuǎn)沒有這么可靠!)因?yàn)樗鼈兒芸焖?,自?dòng)化的測(cè)試可以更頻繁地執(zhí)行,如果測(cè)試失敗,他們會(huì)指出代碼未按預(yù)期執(zhí)行的位置。 此外,自動(dòng)化測(cè)試可以充當(dāng)代碼的第一個(gè)真實(shí)“用戶”,迫使您嚴(yán)格定義和記錄網(wǎng)站的行為方式。它們通常是您的代碼示例,和文檔的基礎(chǔ)。由于這些原因,一些軟件開發(fā)過程,從測(cè)試定義和實(shí)現(xiàn)開始,之后編寫代碼以匹配所需的行為(例如,測(cè)試驅(qū)動(dòng)test-driven 和行為驅(qū)動(dòng) behaviour-driven的開發(fā))。 本教程通過向 LocalLibrary 網(wǎng)站添加大量測(cè)試,來演示如何為 Django 編寫自動(dòng)化測(cè)試。 測(cè)試和測(cè)試方法有許多類型,級(jí)別和分類。最重要的自動(dòng)化測(cè)試是: 單元測(cè)試Unit tests 驗(yàn)證各個(gè)組件的功能行為,通常是類別和功能級(jí)別。 回歸測(cè)試 測(cè)試重現(xiàn)歷史錯(cuò)誤。最初運(yùn)行每個(gè)測(cè)試,以驗(yàn)證錯(cuò)誤是否已修復(fù),然后重新運(yùn)行,以確保在以后更改代碼之后,未重新引入該錯(cuò)誤。 集成測(cè)試 驗(yàn)證組件分組在一起使用時(shí)的工作方式。集成測(cè)試了解組件之間所需的交互,但不一定了解每個(gè)組件的內(nèi)部操作。它們可能涵蓋整個(gè)網(wǎng)站的簡(jiǎn)單組件分組。
注意: 其他常見類型的測(cè)試,包括黑盒,白盒,手動(dòng),自動(dòng),金絲雀,煙霧,一致性,驗(yàn)收,功能,系統(tǒng),性能,負(fù)載和壓力測(cè)試。查找它們以獲取更多信息。 Django為測(cè)試提供了什么?節(jié)測(cè)試網(wǎng)站是一項(xiàng)復(fù)雜的任務(wù),因?yàn)樗啥鄬舆壿嫿M成 - 從 HTTP 級(jí)請(qǐng)求處理,查詢模型,到表單驗(yàn)證和處理,以及模板呈現(xiàn)。 Django 提供了一個(gè)測(cè)試框架,其中包含基于 Python 標(biāo)準(zhǔn)unittest 庫的小型層次結(jié)構(gòu)。盡管名稱如此,但該測(cè)試框架適用于單元測(cè)試和集成測(cè)試。 Django 框架添加了 API 方法和工具,以幫助測(cè)試 Web 和 Django 特定的行為。這允許您模擬請(qǐng)求,插入測(cè)試數(shù)據(jù)以及檢查應(yīng)用程序的輸出。 Django 還提供了一個(gè)API(LiveServerTestCase)和使用不同測(cè)試框架的工具,例如,您可以與流行的 Selenium 框架集成,以模擬用戶與實(shí)時(shí)瀏覽器交互。 要編寫測(cè)試,您可以從任何 Django(或unittest)測(cè)試基類(SimpleTestCase, TransactionTestCase, TestCase, LiveServerTestCase)派生,然后編寫單獨(dú)的方法,來檢查特定功能,是否按預(yù)期工作(測(cè)試使用 “assert” 方法來測(cè)試表達(dá)式導(dǎo)致 True 或 False 值,或者兩個(gè)值相等,等等。)當(dāng)您開始測(cè)試運(yùn)行時(shí),框架將在派生類中執(zhí)行所選的測(cè)試方法。測(cè)試方法獨(dú)立運(yùn)行,具有在類中定義的常見設(shè)置和/或拆卸行為,如下所示。 class YourTestClass(TestCase):
def setUp(self):
#Setup run before every test method.
pass
def tearDown(self):
#Clean up run after every test method.
pass
def test_something_that_will_pass(self):
self.assertFalse(False)
def test_something_that_will_fail(self):
self.assertTrue(False)
大多數(shù)測(cè)試的最佳基類是 django.test.TestCase。此測(cè)試類在運(yùn)行測(cè)試之前,創(chuàng)建一個(gè)干凈的數(shù)據(jù)庫,并在自己的事務(wù)中,運(yùn)行每個(gè)測(cè)試函數(shù)。該類還擁有一個(gè)測(cè)試客戶端,您可以使用該客戶端,模擬在視圖級(jí)別與代碼交互的用戶。在下面的部分中,我們將集中討論使用此TestCase 基類創(chuàng)建的單元測(cè)試。 注意: django.test.TestCase 類非常方便,但可能會(huì)導(dǎo)致某些測(cè)試,比它們需要的速度慢(并非每個(gè)測(cè)試,都需要設(shè)置自己的數(shù)據(jù)庫,或模擬視圖交互)。一旦熟悉了這個(gè)類可以做什么,您可能希望用可以用更簡(jiǎn)單的測(cè)試類,替換一些測(cè)試。 你應(yīng)該測(cè)試什么?節(jié)您應(yīng)該測(cè)試自己代碼的所有方面,但不要測(cè)試 Python 或 Django 的一部分提供的任何庫或功能。 例如,考慮下面定義的 Author 模型。您不需要顯式測(cè)試 first_name 和 last_name 是否已在數(shù)據(jù)庫中正確儲(chǔ)存為CharField ,因?yàn)檫@是 Django 定義的內(nèi)容(當(dāng)然,在實(shí)踐中,您將不可避免地在開發(fā)期間測(cè)試此功能)。你也不需要測(cè)試date_of_birth 是否已被驗(yàn)證為日期字段,因?yàn)檫@也是 Django 中實(shí)現(xiàn)的東西。 但是,您應(yīng)該檢查用于標(biāo)簽的文本(名字,姓氏,出生日期,死亡),以及為文本分配的字段大小(100個(gè)字符),因?yàn)檫@些是您的設(shè)計(jì)的一部分,可能會(huì)在將來被打破/改變。 class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
date_of_death = models.DateField('Died', null=True, blank=True)
def get_absolute_url(self):
return reverse('author-detail', args=[str(self.id)])
def __str__(self):
return '%s, %s' % (self.last_name, self.first_name)
同樣,您應(yīng)該檢查自定義方法 get_absolute_url() 和 __str__() 是否符合要求,因?yàn)樗鼈兪悄拇a/業(yè)務(wù)邏輯。在get_absolute_url() 的情況下,您可以相信 Django reverse() 方法已經(jīng)正確實(shí)現(xiàn),因此您正在測(cè)試的是實(shí)際上已經(jīng)定義了關(guān)聯(lián)的視圖。 注意: 精明的讀者可能會(huì)注意到,我們也希望將出生和死亡的日期限制在合理的值,并檢查出生后是否死亡。在 Django中,此約束將添加到表單類中(盡管您可以為字段定義驗(yàn)證器,這些字段似乎僅在表單級(jí)別使用,而不是在模型級(jí)別使用)。 考慮到這些,讓我們開始研究如何定義和運(yùn)行測(cè)試。 測(cè)試結(jié)構(gòu)概述節(jié)在我們?cè)敿?xì)討論“測(cè)試內(nèi)容”之前,讓我們先簡(jiǎn)要介紹一下測(cè)試的定位和方式。 Django 使用 unittest 模塊的內(nèi)置測(cè)試查找,它將在任何使用模式test*.py 命名的文件中,查找當(dāng)前工作目錄下的測(cè)試。如果您正確命名文件,則可以使用您喜歡的任何結(jié)構(gòu)。我們建議您為測(cè)試代碼創(chuàng)建一個(gè)模塊,并為模型,視圖,表單和您需要測(cè)試的任何其他類型的代碼,分別創(chuàng)建文件。例如: catalog/
/tests/
__init__.py
test_models.py
test_forms.py
test_views.py
在 LocalLibrary 項(xiàng)目中,創(chuàng)建如上所示的文件結(jié)構(gòu)。__init__.py 應(yīng)該是一個(gè)空文件(這告訴 Python 該目錄是一個(gè)套件包)。您可以通過復(fù)制和重命名框架測(cè)試文件/catalog/tests.py,來創(chuàng)建三個(gè)測(cè)試文件。 注意: 我們構(gòu)建 Django 骨架網(wǎng)站時(shí),會(huì)自動(dòng)創(chuàng)建骨架測(cè)試文件/catalog/tests.py 。將所有測(cè)試放入其中是完全“合法的”,但如果測(cè)試正確,您將很快得到一個(gè)非常龐大且難以管理的測(cè)試文件。 刪除骨架文件,因?yàn)槲覀儾恍枰?/p> 打開 /catalog/tests/test_models.py。 該文件應(yīng)導(dǎo)入django.test.TestCase ,如下所示: from django.test import TestCase
# Create your tests here.
通常,您將為要測(cè)試的每個(gè)模型/視圖/表單添加測(cè)試類別,并使用個(gè)別方法來測(cè)試特定功能。在其他情況下,您可能希望有一個(gè)分開的類別,來測(cè)試特定用例,使用個(gè)別的測(cè)試函數(shù),來測(cè)試該用例的各個(gè)方面(例如,測(cè)試模型字段已正確驗(yàn)證的類,以及測(cè)試每個(gè)可能的失敗案例的函數(shù))。相同地,這樣的結(jié)構(gòu)非常適合您,但最好您能保持一致。 將下面的測(cè)試類別,添加到文件的底部。該類別演示了,如何通過派生TestCase ,構(gòu)建測(cè)試用例類。 class YourTestClass(TestCase):
@classmethod
def setUpTestData(cls):
print("setUpTestData: Run once to set up non-modified data for all class methods.")
pass
def setUp(self):
print("setUp: Run once for every test method to setup clean data.")
pass
def test_false_is_false(self):
print("Method: test_false_is_false.")
self.assertFalse(False)
def test_false_is_true(self):
print("Method: test_false_is_true.")
self.assertTrue(False)
def test_one_plus_one_equals_two(self):
print("Method: test_one_plus_one_equals_two.")
self.assertEqual(1 + 1, 2)
新的類別定義了兩個(gè)可用于測(cè)試之前的配置的方法(例如,創(chuàng)建測(cè)試所需的任何模型或其他對(duì)象): setUpTestData() 用于類級(jí)別設(shè)置,在測(cè)試運(yùn)行開始的時(shí)侯,會(huì)調(diào)用一次。您可以使用它來創(chuàng)建在任何測(cè)試方法中,都不會(huì)修改或更改的對(duì)象。
setUp() 在每個(gè)測(cè)試函數(shù)之前被調(diào)用,以設(shè)置可能被測(cè)試修改的任何對(duì)象(每個(gè)測(cè)試函數(shù),都將獲得這些對(duì)象的 “新” 版本)。
注意:測(cè)試類別還有一個(gè)我們還沒有使用的tearDown() 方法。此方法對(duì)數(shù)據(jù)庫測(cè)試不是特別有用,因?yàn)?code>TestCase基類會(huì)為您處理數(shù)據(jù)庫拆卸。 下面我們有一些測(cè)試方法,它們使用 Assert 函數(shù)來測(cè)試條件是真,假或相等(AssertTrue , AssertFalse , AssertEqual )。如果條件評(píng)估不如預(yù)期,則測(cè)試將失敗,并將錯(cuò)誤報(bào)告給控制臺(tái)。 AssertTrue , AssertFalse , AssertEqual 是 unittest 提供的標(biāo)準(zhǔn)斷言。框架中還有其他標(biāo)準(zhǔn)斷言,還有 Django 特定的斷言,來測(cè)試視圖是否重定向(assertRedirects ),或測(cè)試是否已使用特定模板(assertTemplateUsed )等。
注意:您通常不應(yīng)在測(cè)試中包含print() 函數(shù),如上所示。我們這樣做,只是為了讓您可以看到在控制臺(tái)中,調(diào)用設(shè)置功能的順序(在下一節(jié)中)。 如何運(yùn)行測(cè)試節(jié)要運(yùn)行所有測(cè)試,最簡(jiǎn)單的方法,是使用以下命令: python3 manage.py test
這將查找當(dāng)前目錄下,使用模式 test*.py 命名的所有文件,并運(yùn)行使用適當(dāng)基類定義的所有測(cè)試(這里我們有許多測(cè)試文件,但只有 /catalog/tests/test_models.py 目前包含任何測(cè)試。)。默認(rèn)情況下,測(cè)試將僅單獨(dú)報(bào)告測(cè)試失敗,然后是測(cè)試摘要。 如果您收到類似于以下內(nèi)容的錯(cuò)誤:ValueError: Missing staticfiles manifest entry ... 這可能是因?yàn)槟J(rèn)情況下,測(cè)試不會(huì)運(yùn)行 collectstatic,而您的應(yīng)用程序正在使用需要它的儲(chǔ)存類別(有關(guān)更多信息,請(qǐng)參閱 manifest_strict)。有許多方法可以解決這個(gè)問題 - 最簡(jiǎn)單的方法,是在運(yùn)行測(cè)試之前,簡(jiǎn)單地運(yùn)行collectstatic: python3 manage.py collectstatic
在 LocalLibrary 的根目錄中,運(yùn)行測(cè)試。您應(yīng)該看到如下所示的輸出。 >python3 manage.py test
Creating test database for alias 'default'...
setUpTestData: Run once to set up non-modified data for all class methods.
setUp: Run once for every test method to setup clean data.
Method: test_false_is_false.
.setUp: Run once for every test method to setup clean data.
Method: test_false_is_true.
FsetUp: Run once for every test method to setup clean data.
Method: test_one_plus_one_equals_two.
.
======================================================================
FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\Github\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true
self.assertTrue(False)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 3 tests in 0.075s
FAILED (failures=1)
Destroying test database for alias 'default'...
在這里,我們看到有一個(gè)測(cè)試失敗,我們可以確切地看到哪個(gè)函數(shù)失敗了、為什么失敗(這個(gè)失敗是預(yù)期的,因?yàn)?False 不是 True !)。 提示: 從上面的測(cè)試輸出中,學(xué)到的最重要事情是,如果為對(duì)象和方法使用描述性/信息性名稱,它會(huì)更有價(jià)值。 上面以粗體顯示的文本,通常不會(huì)出現(xiàn)在測(cè)試輸出中(這是由我們的測(cè)試中的print() 函數(shù)生成的)。這顯示了如何為類調(diào)用setUpTestData() 方法,并在每個(gè)方法之前調(diào)用setUp() 。 接下來的部分,將介紹如何運(yùn)行特定測(cè)試,以及如何控制測(cè)試顯示的信息量。 如果您想獲得有關(guān)測(cè)試運(yùn)行的更多信息,可以更改詳細(xì)程度。例如,要列出測(cè)試成功和失?。ㄒ约坝嘘P(guān)如何設(shè)置測(cè)試數(shù)據(jù)庫的大量信息),您可以將詳細(xì)程度設(shè)置為 “2”,如下所示: python3 manage.py test --verbosity 2
允許的詳細(xì)級(jí)別為 0, 1 ,2 和 3,默認(rèn)值為 “1”。 運(yùn)行特定測(cè)試節(jié)如果要運(yùn)行測(cè)試的子集,可以通過指定包,模塊,TestCase 子類或方法的完整路徑(包含點(diǎn))來執(zhí)行此操作: python3 manage.py test catalog.tests # Run the specified module
python3 manage.py test catalog.tests.test_models # Run the specified module
python3 manage.py test catalog.tests.test_models.YourTestClass # Run the specified class
python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two # Run the specified method
LocalLibrary 測(cè)試節(jié)現(xiàn)在我們知道,如何運(yùn)行我們的測(cè)試,以及我們需要測(cè)試哪些東西,讓我們看一些實(shí)際的例子。 注意: 我們不會(huì)編寫所有可能的測(cè)試,但這應(yīng)該可以讓您了解測(cè)試的工作原理,以及您可以做些什么。 如上所述,我們應(yīng)該測(cè)試我們?cè)O(shè)計(jì)的任何內(nèi)容,或由我們編寫的代碼定義的內(nèi)容,而不是已經(jīng)由 Django 或 Python 開發(fā)團(tuán)隊(duì)測(cè)試過的庫/代碼。 例如,請(qǐng)考慮下面的作者模型 Author 。在這里,我們應(yīng)該測(cè)試所有字段的標(biāo)簽,因?yàn)榧词刮覀儧]有明確指定它們中的大部分,我們也有一個(gè)設(shè)計(jì),說明這些值應(yīng)該是什么。如果我們不測(cè)試值,那么我們不知道字段標(biāo)簽,是否具有其預(yù)期值。同樣,雖然我們相信 Django 會(huì)創(chuàng)建一個(gè)指定長(zhǎng)度的字段,但值得為這個(gè)長(zhǎng)度指定一個(gè)測(cè)試,以確保它按計(jì)劃實(shí)現(xiàn)。 class Author(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField(null=True, blank=True)
date_of_death = models.DateField('Died', null=True, blank=True)
def get_absolute_url(self):
return reverse('author-detail', args=[str(self.id)])
def __str__(self):
return '%s, %s' % (self.last_name, self.first_name)
打開我們的 /catalog/tests/test_models.py,并用 Author 模型的以下測(cè)試代碼,替換任何現(xiàn)有代碼。 在這里,您將看到我們首先導(dǎo)入 TestCase ,并使用描述性名稱,從中派生我們的測(cè)試類(AuthorModelTest ),以便我們可以輕松識(shí)別測(cè)試輸出中的任何失敗測(cè)試。然后我們調(diào)用setUpTestData() ,來創(chuàng)建一個(gè)我們將使用,但不在任何測(cè)試中修改的作者對(duì)象。 from django.test import TestCase
# Create your tests here.
from catalog.models import Author
class AuthorModelTest(TestCase):
@classmethod
def setUpTestData(cls):
#Set up non-modified objects used by all test methods
Author.objects.create(first_name='Big', last_name='Bob')
def test_first_name_label(self):
author=Author.objects.get(id=1)
field_label = author._meta.get_field('first_name').verbose_name
self.assertEquals(field_label,'first name')
def test_date_of_death_label(self):
author=Author.objects.get(id=1)
field_label = author._meta.get_field('date_of_death').verbose_name
self.assertEquals(field_label,'died')
def test_first_name_max_length(self):
author=Author.objects.get(id=1)
max_length = author._meta.get_field('first_name').max_length
self.assertEquals(max_length,100)
def test_object_name_is_last_name_comma_first_name(self):
author=Author.objects.get(id=1)
expected_object_name = '%s, %s' % (author.last_name, author.first_name)
self.assertEquals(expected_object_name,str(author))
def test_get_absolute_url(self):
author=Author.objects.get(id=1)
#This will also fail if the urlconf is not defined.
self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
字段測(cè)試檢查字段標(biāo)簽(verbose_name )的值,以及字符字段的大小,是否符合預(yù)期。這些方法都有描述性名稱,并遵循相同的模式: author=Author.objects.get(id=1) # Get an author object to test
field_label = author._meta.get_field('first_name').verbose_name # Get the metadata for the required field and use it to query the required field data
self.assertEquals(field_label,'first name') # Compare the value to the expected result
有趣的事情是: 我們無法使用 author.first_name.verbose_name 直接獲取 verbose_name ,因?yàn)?code>author.first_name 是一個(gè)字符串(不是我們可以用來訪問其屬性的first_name 對(duì)象的句柄)。取而代之的是,我們需要使用作者的 _meta 屬性,來獲取字段的實(shí)例,并使用它來查詢其他信息。
我們選擇使用 assertEquals(field_label,'first name') ,而不是assertTrue(field_label == 'first name') 。這樣做的原因是,如果測(cè)試失敗,前者的輸出,會(huì)告訴您標(biāo)簽實(shí)際上是什么,這使得調(diào)試問題變得更容易一些。
注意: 已省略對(duì)last_name 和 date_of_birth 標(biāo)簽的測(cè)試,以及 last_name 字段長(zhǎng)度的測(cè)試?,F(xiàn)在按照上面顯示的命名約定和方法,添加您自己的版本。 我們還需要測(cè)試我們的自定義方法。這些基本上只是檢查對(duì)象名稱,是否按照我們的預(yù)期,使用“姓氏”,“名字”格式構(gòu)建,并且我們?yōu)?code>Author獲取的 URL,是我們所期望的。 def test_object_name_is_last_name_comma_first_name(self):
author=Author.objects.get(id=1)
expected_object_name = '%s, %s' % (author.last_name, author.first_name)
self.assertEquals(expected_object_name,str(author))
def test_get_absolute_url(self):
author=Author.objects.get(id=1)
#This will also fail if the urlconf is not defined.
self.assertEquals(author.get_absolute_url(),'/catalog/author/1')
立即運(yùn)行測(cè)試。如果您按照模型教程中的描述,創(chuàng)建了作者模型,則很可能會(huì)出現(xiàn)date_of_death 標(biāo)簽的錯(cuò)誤,如下所示。測(cè)試失敗,是因?yàn)樗鼘懙氖瞧谕麡?biāo)簽定義遵循 Django 的約定,即沒有大寫標(biāo)簽的第一個(gè)字母(Django 會(huì)為你做這個(gè))。 ======================================================================
FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\...\locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label
self.assertEquals(field_label,'died')
AssertionError: 'Died' != 'died'
- Died
? ^
+ died
? ^
這是一個(gè)非常小的錯(cuò)誤,但它確實(shí)強(qiáng)調(diào)了,編寫測(cè)試如何能夠更徹底地檢查,您可能做出的任何假設(shè)。 注意: 將 date_of_death字段(/catalog/models.py)的標(biāo)簽更改為“death”并重新運(yùn)行測(cè)試。 用于測(cè)試其他模型的模式,也類似于此,因此我們不會(huì)繼續(xù)進(jìn)一步討論這些模式。請(qǐng)隨意為其他模型,創(chuàng)建您自己的測(cè)試。 測(cè)試表單的理念,與測(cè)試模型的理念相同;您需要測(cè)試您編碼、或設(shè)計(jì)指定的任何內(nèi)容,但不測(cè)試底層框架,和其他第三方庫的行為。 通常,這意味著您應(yīng)該測(cè)試表單,是否包含您想要的字段,并使用適當(dāng)?shù)臉?biāo)簽和幫助文本,顯示這些字段。您無需驗(yàn)證 Django 是否正確驗(yàn)證了字段類型(除非您創(chuàng)建了自己的自定義字段和驗(yàn)證) - 即您不需要測(cè)試電子郵件字段,是否只接受電子郵件。但是,您需要測(cè)試,您希望在字段上執(zhí)行的任何其他驗(yàn)證,以及您的代碼將為錯(cuò)誤生成的任何消息。 考慮我們更新書本的表格。這只有一個(gè)繼續(xù)借閱的日期字段,它將包含我們需要驗(yàn)證的標(biāo)簽,和幫助文本。 class RenewBookForm(forms.Form):
"""
Form for a librarian to renew books.
"""
renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")
def clean_renewal_date(self):
data = self.cleaned_data['renewal_date']
#Check date is not in past.
if data < datetime.date.today():
raise ValidationError(_('Invalid date - renewal in past'))
#Check date is in range librarian allowed to change (+4 weeks)
if data > datetime.date.today() + datetime.timedelta(weeks=4):
raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))
# Remember to always return the cleaned data.
return data
打開我們的 /catalog/tests/test_forms.py 文件,并用RenewBookForm 表單的以下測(cè)試代碼,替換任何現(xiàn)有代碼。我們首先導(dǎo)入我們的表單,和一些 Python 和 Django 庫,以幫助測(cè)試與時(shí)間相關(guān)的功能。然后,我們以與模型相同的方式,聲明我們的表單測(cè)試類,使用我們的 TestCase 派生測(cè)試類的描述性名稱。 from django.test import TestCase
# Create your tests here.
import datetime
from django.utils import timezone
from catalog.forms import RenewBookForm
class RenewBookFormTest(TestCase):
def test_renew_form_date_field_label(self):
form = RenewBookForm()
self.assertTrue(form.fields['renewal_date'].label == None or form.fields['renewal_date'].label == 'renewal date')
def test_renew_form_date_field_help_text(self):
form = RenewBookForm()
self.assertEqual(form.fields['renewal_date'].help_text,'Enter a date between now and 4 weeks (default 3).')
def test_renew_form_date_in_past(self):
date = datetime.date.today() - datetime.timedelta(days=1)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertFalse(form.is_valid())
def test_renew_form_date_too_far_in_future(self):
date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertFalse(form.is_valid())
def test_renew_form_date_today(self):
date = datetime.date.today()
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertTrue(form.is_valid())
def test_renew_form_date_max(self):
date = timezone.now() + datetime.timedelta(weeks=4)
form_data = {'renewal_date': date}
form = RenewBookForm(data=form_data)
self.assertTrue(form.is_valid())
前兩個(gè)函數(shù),測(cè)試字段的label 和 help_text ,是否符合預(yù)期。我們必須使用字段字典訪問該字段(例如form.fields['renewal_date'] )。請(qǐng)注意,我們還必須測(cè)試標(biāo)簽值,是否為None ,因?yàn)榧词?Django 將呈現(xiàn)正確的標(biāo)簽,如果未明確設(shè)置該值,它也會(huì)返回None 。 其余函數(shù),測(cè)試表單對(duì)于續(xù)借日期,在可接受范圍內(nèi)是否有效,對(duì)于范圍外的值,是否無效。請(qǐng)注意我們?nèi)绾问褂?code>datetime.timedelta(),在當(dāng)前日期(datetime.date.today() )周圍構(gòu)建測(cè)試日期值(在這種情況下指定天數(shù)或周數(shù))。然后我們只需創(chuàng)建表單,傳入我們的數(shù)據(jù),并測(cè)試它是否有效。 注意: 這里我們實(shí)際上并沒有使用數(shù)據(jù)庫,或測(cè)試客戶端。考慮修改這些測(cè)試,以使用SimpleTestCase。 如果表單無效,我們還需要驗(yàn)證是否引發(fā)了正確的錯(cuò)誤,但這通常是作為視圖處理的一部分完成的,因此我們將在下一節(jié)中處理。 這就是表單的全部;我們確實(shí)有其他一些的東西,但它們是由基于類的通用編輯視圖自動(dòng)創(chuàng)建的,應(yīng)該在那里進(jìn)行測(cè)試!運(yùn)行測(cè)試,并確認(rèn)我們的代碼仍然通過! 為了驗(yàn)證我們的視圖行為,我們使用 Django 的測(cè)試客戶端。這個(gè)類,就像一個(gè)虛擬的Web瀏覽器,我們可以使用它,來模擬URL上的GET 和POST 請(qǐng)求,并觀察響應(yīng)。我們幾乎可以看到,關(guān)于響應(yīng)的所有內(nèi)容,從低層級(jí)的 HTTP(結(jié)果標(biāo)頭和狀態(tài)代碼),到我們用來呈現(xiàn)HTML的模板,以及我們傳遞給它的上下文數(shù)據(jù)。我們還可以看到重定向鏈(如果有的話),并在每一步檢查URL,和狀態(tài)代碼。這允許我們驗(yàn)證每個(gè)視圖,是否正在執(zhí)行預(yù)期的操作。 讓我們從最簡(jiǎn)單的視圖開始,它提供了所有作者的列表。它顯示在 URL /catalog/authors/ 當(dāng)中(URL 配置中,名為 “authors” 的 URL)。 class AuthorListView(generic.ListView):
model = Author
paginate_by = 10
由于這是一個(gè)通用列表視圖,幾乎所有內(nèi)容,都由 Django 為我們完成??梢哉f,如果您信任 Django,那么您唯一需要測(cè)試的,是視圖可以通過正確的 URL 訪問,并且可以使用其名稱進(jìn)行訪問。但是,如果您使用的是測(cè)試驅(qū)動(dòng)的開發(fā)過程,則首先編寫測(cè)試,確認(rèn)視圖顯示所有作者,并將其分成10個(gè)。 打開 /catalog/tests/test_views.py 文件,并用AuthorListView 的以下測(cè)試代碼,替換任何現(xiàn)有文本。和以前一樣,我們導(dǎo)入模型,和一些有用的類。在setUpTestData() 方法中,我們?cè)O(shè)置了許多Author 對(duì)象,以便我們可以測(cè)試我們的分頁。 from django.test import TestCase
# Create your tests here.
from catalog.models import Author
from django.urls import reverse
class AuthorListViewTest(TestCase):
@classmethod
def setUpTestData(cls):
#Create 13 authors for pagination tests
number_of_authors = 13
for author_num in range(number_of_authors):
Author.objects.create(first_name='Christian %s' % author_num, last_name = 'Surname %s' % author_num,)
def test_view_url_exists_at_desired_location(self):
resp = self.client.get('/catalog/authors/')
self.assertEqual(resp.status_code, 200)
def test_view_url_accessible_by_name(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
def test_view_uses_correct_template(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, 'catalog/author_list.html')
def test_pagination_is_ten(self):
resp = self.client.get(reverse('authors'))
self.assertEqual(resp.status_code, 200)
self.assertTrue('is_paginated' in resp.context)
self.assertTrue(resp.context['is_paginated'] == True)
self.assertTrue( len(resp.context['author_list']) == 10)
def test_lists_all_authors(self):
#Get second page and confirm it has (exactly) remaining 3 items
resp = self.client.get(reverse('authors')+'?page=2')
self.assertEqual(resp.status_code, 200)
self.assertTrue('is_paginated' in resp.context)
self.assertTrue(resp.context['is_paginated'] == True)
self.assertTrue( len(resp.context['author_list']) == 3)
所有測(cè)試,都使用客戶端(屬于我們的TestCase 的派生類)來模擬GET 請(qǐng)求,并獲得響應(yīng)(resp )。第一個(gè)版本檢查特定 URL(注意,只是沒有域名的特定路徑),而第二個(gè)版本從 URL配置中的名稱生成 URL。 resp = self.client.get('/catalog/authors/')
resp = self.client.get(reverse('authors'))
獲得響應(yīng)后,我們會(huì)查詢其狀態(tài)代碼,使用的模板,響應(yīng)是否已分頁,返回的項(xiàng)目數(shù)以及項(xiàng)目總數(shù)。
我們?cè)谏厦嫜菔镜淖钣腥さ淖兞渴?code>resp.context,它是視圖傳遞給模板的上下文變量。這對(duì)測(cè)試非常有用,因?yàn)樗试S我們確認(rèn)模板正在獲取所需的所有數(shù)據(jù)。換句話說,我們可以檢查是否正在使用預(yù)期的模板,以及模板獲得的數(shù)據(jù),這對(duì)于驗(yàn)證任何渲染問題,是否真的僅僅歸因于模板有很大幫助。
僅限登錄用戶的視圖在某些情況下,您需要測(cè)試僅限登錄用戶的視圖。例如,我們的LoanedBooksByUserListView 與我們之前的視圖非常相似,但僅供登錄用戶使用,并且僅顯示當(dāng)前用戶借用的BookInstance 記錄,具有出借中“on loan”狀態(tài),并且排序方式為“舊的優(yōu)先”。 from django.contrib.auth.mixins import LoginRequiredMixin
class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
"""
Generic class-based view listing books on loan to current user.
"""
model = BookInstance
template_name ='catalog/bookinstance_list_borrowed_user.html'
paginate_by = 10
def get_queryset(self):
return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')
將以下測(cè)試代碼,添加到 /catalog/tests/test_views.py。這里我們首先使用SetUp() 創(chuàng)建一些用戶登錄帳戶,和BookInstance 對(duì)象(以及它們的相關(guān)書本,和其他記錄),我們稍后將在測(cè)試中使用它們。每個(gè)測(cè)試用戶都借用了一半的書本,但我們最初,將所有書本的狀態(tài)設(shè)置為“維護(hù)”。我們使用了SetUp() 而不是setUpTestData() ,因?yàn)槲覀兩院髸?huì)修改其中的一些對(duì)象。 注意: 下面的setUp() 代碼,會(huì)創(chuàng)建一個(gè)具有指定語言Language 的書本,但您的代碼可能不包含語言模型Language ,因?yàn)樗亲鳛樘魬?zhàn)創(chuàng)建的。如果是這種情況,只需注釋掉創(chuàng)建或?qū)胝Z言對(duì)象的代碼部分。您還應(yīng)該在隨后的RenewBookInstancesViewTest 部分中,執(zhí)行此操作。 import datetime
from django.utils import timezone
from catalog.models import BookInstance, Book, Genre, Language
from django.contrib.auth.models import User #Required to assign User as a borrower
class LoanedBookInstancesByUserListViewTest(TestCase):
def setUp(self):
#Create two users
test_user1 = User.objects.create_user(username='testuser1', password='12345')
test_user1.save()
test_user2 = User.objects.create_user(username='testuser2', password='12345')
test_user2.save()
#Create a book
test_author = Author.objects.create(first_name='John', last_name='Smith')
test_genre = Genre.objects.create(name='Fantasy')
test_language = Language.objects.create(name='English')
test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language)
# Create genre as a post-step
genre_objects_for_book = Genre.objects.all()
test_book.genre.set(genre_objects_for_book) #Direct assignment of many-to-many types not allowed.
test_book.save()
#Create 30 BookInstance objects
number_of_book_copies = 30
for book_copy in range(number_of_book_copies):
return_date= timezone.now() + datetime.timedelta(days=book_copy%5)
if book_copy % 2:
the_borrower=test_user1
else:
the_borrower=test_user2
status='m'
BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status)
def test_redirect_if_not_logged_in(self):
resp = self.client.get(reverse('my-borrowed'))
self.assertRedirects(resp, '/accounts/login/?next=/catalog/mybooks/')
def test_logged_in_uses_correct_template(self):
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('my-borrowed'))
#Check our user is logged in
self.assertEqual(str(resp.context['user']), 'testuser1')
#Check that we got a response "success"
self.assertEqual(resp.status_code, 200)
#Check we used correct template
self.assertTemplateUsed(resp, 'catalog/bookinstance_list_borrowed_user.html')
要驗(yàn)證如果用戶未登錄,視圖將重定向到登錄頁面,我們使用assertRedirects ,如test_redirect_if_not_logged_in() 中所示。要驗(yàn)證是否已為登錄用戶顯示該頁面,我們首先登錄我們的測(cè)試用戶,然后再次訪問該頁面,并檢查我們獲得的status_code 為200(成功)。 測(cè)試的其余部分,驗(yàn)證我們的觀點(diǎn),僅返回借給當(dāng)前借用人的書本。復(fù)制上面測(cè)試類末尾的(自解釋)代碼。 def test_only_borrowed_books_in_list(self):
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('my-borrowed'))
#Check our user is logged in
self.assertEqual(str(resp.context['user']), 'testuser1')
#Check that we got a response "success"
self.assertEqual(resp.status_code, 200)
#Check that initially we don't have any books in list (none on loan)
self.assertTrue('bookinstance_list' in resp.context)
self.assertEqual( len(resp.context['bookinstance_list']),0)
#Now change all books to be on loan
get_ten_books = BookInstance.objects.all()[:10]
for copy in get_ten_books:
copy.status='o'
copy.save()
#Check that now we have borrowed books in the list
resp = self.client.get(reverse('my-borrowed'))
#Check our user is logged in
self.assertEqual(str(resp.context['user']), 'testuser1')
#Check that we got a response "success"
self.assertEqual(resp.status_code, 200)
self.assertTrue('bookinstance_list' in resp.context)
#Confirm all books belong to testuser1 and are on loan
for bookitem in resp.context['bookinstance_list']:
self.assertEqual(resp.context['user'], bookitem.borrower)
self.assertEqual('o', bookitem.status)
def test_pages_ordered_by_due_date(self):
#Change all books to be on loan
for copy in BookInstance.objects.all():
copy.status='o'
copy.save()
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('my-borrowed'))
#Check our user is logged in
self.assertEqual(str(resp.context['user']), 'testuser1')
#Check that we got a response "success"
self.assertEqual(resp.status_code, 200)
#Confirm that of the items, only 10 are displayed due to pagination.
self.assertEqual( len(resp.context['bookinstance_list']),10)
last_date=0
for copy in resp.context['bookinstance_list']:
if last_date==0:
last_date=copy.due_back
else:
self.assertTrue(last_date <= copy.due_back)
你也可以添加分頁測(cè)試,如果你愿意的話! 使用表單測(cè)試視圖使用表單測(cè)試視圖,比上面的情況稍微復(fù)雜一些,因?yàn)槟枰獪y(cè)試更多代碼路徑:初始顯示,數(shù)據(jù)驗(yàn)證失敗后顯示,以及驗(yàn)證成功后顯示。好消息是,我們使用客戶端進(jìn)行測(cè)試的方式,與我們對(duì)僅顯示視圖的方式,幾乎完全相同。 為了演示,讓我們?yōu)橛糜诶m(xù)借書本的視圖,編寫一些測(cè)試(renew_book_librarian() ): from .forms import RenewBookForm
@permission_required('catalog.can_mark_returned')
def renew_book_librarian(request, pk):
"""
View function for renewing a specific BookInstance by librarian
"""
book_inst=get_object_or_404(BookInstance, pk = pk)
# If this is a POST request then process the Form data
if request.method == 'POST':
# Create a form instance and populate it with data from the request (binding):
form = RenewBookForm(request.POST)
# Check if the form is valid:
if form.is_valid():
# process the data in form.cleaned_data as required (here we just write it to the model due_back field)
book_inst.due_back = form.cleaned_data['renewal_date']
book_inst.save()
# redirect to a new URL:
return HttpResponseRedirect(reverse('all-borrowed') )
# If this is a GET (or any other method) create the default form
else:
proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,})
return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})
我們需要測(cè)試該視圖,僅供具有can_mark_returned 權(quán)限的用戶使用,并且如果用戶嘗試?yán)m(xù)借不存在的BookInstance ,則會(huì)將用戶重定向到HTTP 404錯(cuò)誤頁面。我們應(yīng)該檢查表單的初始值,是否以未來三周的日期為參考值,如果驗(yàn)證成功,我們將被重定向到 “所有借閱的書本” 視圖。作為驗(yàn)證 - 失敗測(cè)試的一部分,我們還將檢查我們的表單,是否發(fā)送了相應(yīng)的錯(cuò)誤消息。 將測(cè)試類的第一部分(如下所示),添加到 /catalog/tests/test_views.py 的底部。這將創(chuàng)建兩個(gè)用戶和兩個(gè)書本實(shí)例,但只為一個(gè)用戶提供訪問該視圖所需的權(quán)限。在測(cè)試期間,授予權(quán)限的代碼以粗體顯示: from django.contrib.auth.models import Permission # Required to grant the permission needed to set a book as returned.
class RenewBookInstancesViewTest(TestCase):
def setUp(self):
#Create a user
test_user1 = User.objects.create_user(username='testuser1', password='12345')
test_user1.save()
test_user2 = User.objects.create_user(username='testuser2', password='12345')
test_user2.save() permission = Permission.objects.get(name='Set book as returned')
test_user2.user_permissions.add(permission)
test_user2.save()
#Create a book
test_author = Author.objects.create(first_name='John', last_name='Smith')
test_genre = Genre.objects.create(name='Fantasy')
test_language = Language.objects.create(name='English')
test_book = Book.objects.create(title='Book Title', summary = 'My book summary', isbn='ABCDEFG', author=test_author, language=test_language,)
# Create genre as a post-step
genre_objects_for_book = Genre.objects.all()
test_book.genre.set(genre_objects_for_book) # Direct assignment of many-to-many types not allowed.
test_book.save()
#Create a BookInstance object for test_user1
return_date= datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance1=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o')
#Create a BookInstance object for test_user2
return_date= datetime.date.today() + datetime.timedelta(days=5)
self.test_bookinstance2=BookInstance.objects.create(book=test_book,imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o')
將以下測(cè)試添加到測(cè)試類的底部。這些檢查只有具有正確權(quán)限的用戶(testuser2)才能訪問該視圖。我們檢查所有情況:當(dāng)用戶沒有登錄時(shí)、當(dāng)用戶登錄但沒有正確的權(quán)限,當(dāng)用戶有權(quán)限但不是借用人(應(yīng)該成功),以及當(dāng)他們嘗試訪問不存在的BookInstance ,會(huì)發(fā)生什么。我們還檢查是否使用了正確的模板。 def test_redirect_if_not_logged_in(self):
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
self.assertEqual( resp.status_code,302)
self.assertTrue( resp.url.startswith('/accounts/login/') )
def test_redirect_if_logged_in_but_not_correct_permission(self):
login = self.client.login(username='testuser1', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Manually check redirect (Can't use assertRedirect, because the redirect URL is unpredictable)
self.assertEqual( resp.status_code,302)
self.assertTrue( resp.url.startswith('/accounts/login/') )
def test_logged_in_with_permission_borrowed_book(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance2.pk,}) )
#Check that it lets us login - this is our book and we have the right permissions.
self.assertEqual( resp.status_code,200)
def test_logged_in_with_permission_another_users_borrowed_book(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
#Check that it lets us login. We're a librarian, so we can view any users book
self.assertEqual( resp.status_code,200)
def test_HTTP404_for_invalid_book_if_logged_in(self):
import uuid
test_uid = uuid.uuid4() #unlikely UID to match our bookinstance!
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid,}) )
self.assertEqual( resp.status_code,404)
def test_uses_correct_template(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
self.assertEqual( resp.status_code,200)
#Check we used correct template
self.assertTemplateUsed(resp, 'catalog/book_renew_librarian.html')
添加下一個(gè)測(cè)試方法,如下所示。這將檢查表單的初始日期,是將來三周。請(qǐng)注意我們?nèi)绾文軌蛟L問表單字段的初始值內(nèi)的值(以粗體顯示)。 def test_form_renewal_date_initially_has_date_three_weeks_in_future(self):
login = self.client.login(username='testuser2', password='12345')
resp = self.client.get(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}) )
self.assertEqual( resp.status_code,200)
date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3)
self.assertEqual(resp.context['form'].initial['renewal_date'], date_3_weeks_in_future )
下一個(gè)測(cè)試(將其添加到類中)會(huì)檢查如果續(xù)借成功,視圖會(huì)重定向到所有借書的列表。這里的不同之處在于,我們首次展示了,如何使用客戶端發(fā)布(POST )數(shù)據(jù)。 post數(shù)據(jù)是post函數(shù)的第二個(gè)參數(shù),并被指定為鍵/值的字典。 def test_redirects_to_all_borrowed_book_list_on_success(self):
login = self.client.login(username='testuser2', password='12345')
valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future} ) self.assertRedirects(resp, reverse('all-borrowed') )
重要:全部借用的視圖作為額外挑戰(zhàn),您的代碼可能會(huì)改為重定向到主頁'/'。如果是這樣,請(qǐng)將測(cè)試代碼的最后兩行,修改為與下面的代碼類似。請(qǐng)求中的follow=True ,確保請(qǐng)求返回最終目標(biāo)URL(因此檢查/catalog/ 而不是/ )。 resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future},follow=True ) self.assertRedirects(resp, '/catalog/')
將最后兩個(gè)函數(shù),復(fù)制到類中,如下所示。這些再次測(cè)試POST 請(qǐng)求,但在這種情況下具有無效的續(xù)借日期。我們使用assertFormError() ,來驗(yàn)證錯(cuò)誤消息是否符合預(yù)期。 def test_form_invalid_renewal_date_past(self):
login = self.client.login(username='testuser2', password='12345')
date_in_past = datetime.date.today() - datetime.timedelta(weeks=1)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':date_in_past} )
self.assertEqual( resp.status_code,200)
self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal in past')
def test_form_invalid_renewal_date_future(self):
login = self.client.login(username='testuser2', password='12345')
invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5)
resp = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':invalid_date_in_future} )
self.assertEqual( resp.status_code,200)
self.assertFormError(resp, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')
可以使用相似的技術(shù),來測(cè)試其他視圖。 Django 提供測(cè)試 API 來檢查您的視圖,是否正在調(diào)用正確的模板,并允許您驗(yàn)證,是否正在發(fā)送正確的信息。但是,沒有特定的 API,支持在 Django中測(cè)試 HTML輸出,是否按預(yù)期呈現(xiàn)。 其他推薦的測(cè)試工具節(jié)Django 的測(cè)試框架,可以幫助您編寫有效的單元和集成測(cè)試 - 我們只涉及底層單元測(cè)試框架unittest可以做什么,而不去談 Django 的其他部分(例如,查看如何使用unittest.mock 修補(bǔ)第三方庫,以便您可以更徹底地測(cè)試自己的代碼)。 雖然您可以使用許多其他測(cè)試工具,但我們只重點(diǎn)介紹兩個(gè): Coverage: 此Python工具報(bào)告您的測(cè)試,實(shí)際執(zhí)行了多少代碼。當(dāng)開始使用時(shí),你正試圖找出你應(yīng)該測(cè)試的確切內(nèi)容,它會(huì)特別有用。 Selenium 是一個(gè)在真實(shí)瀏覽器中,自動(dòng)化測(cè)試的框架。它允許您模擬與站點(diǎn)交互的真實(shí)用戶,并為系統(tǒng)測(cè)試您的站點(diǎn),提供了一個(gè)很好的框架(從集成測(cè)試開始的下一步)。
有許多模型與視圖,我們可以用來測(cè)試。比如一個(gè)簡(jiǎn)單的任務(wù),試著為AuthorCreate 視圖,創(chuàng)造一個(gè)測(cè)試案例。 class AuthorCreate(PermissionRequiredMixin, CreateView):
model = Author
fields = '__all__'
initial={'date_of_death':'12/10/2016',}
permission_required = 'catalog.can_mark_returned'
請(qǐng)記住,您需要檢查您指定的任何內(nèi)容、或設(shè)計(jì)的一部分。這將包括誰有權(quán)訪問,初始日期,使用的模板,以及視圖在成功時(shí),重定向的位置。 撰寫測(cè)試代碼既不有趣也不吸引人,因此在創(chuàng)造一個(gè)網(wǎng)站時(shí),經(jīng)常被留到最后才處理(或者完全不處理)。然而,它是一個(gè)基礎(chǔ)的部分,以保證你的程式碼,在更改之后是安全、可發(fā)布的,并且維護(hù)起來不會(huì)花費(fèi)太多成本。 本教程中,我們演示了如何為模型、表單和視圖,編寫并運(yùn)行測(cè)試。最重要的是,我們已經(jīng)提供給您,應(yīng)該測(cè)試的內(nèi)容的簡(jiǎn)短摘要,這通常是您開始時(shí),最難解決的問題。還有很多東西要知道,但即使你已經(jīng)學(xué)到了什么,你也應(yīng)該能夠?yàn)槟愕木W(wǎng)站創(chuàng)建有效的單元測(cè)試。 下一個(gè)、也是最后一個(gè)教程,展示了如何部署精彩的(并經(jīng)過全面測(cè)試的?。〥jango網(wǎng)站。
|