Django Tutorial Part 10: Testing a Django web application

2018-05-15 17:26 更新
先決條件: 完成之前的所有教程主題,包括 Django教程第9部分:使用表單。
目的: 了解如何為基于Django的網(wǎng)站編寫單元測(cè)試。

概述

LocalLibrary 當(dāng)前具有顯示所有圖書和作者列表的頁面, Book 作者項(xiàng)目的詳細(xì)視圖,要更新 BookInstance 的頁面以及創(chuàng)建,更新和刪除 如果您在表單教程中完成了挑戰(zhàn),那么項(xiàng)目(和 )。 即使使用這個(gè)相對(duì)較小的網(wǎng)站,手動(dòng)導(dǎo)航到每個(gè)網(wǎng)頁并表面地檢查一切是否按預(yù)期工作可能需要幾分鐘。 隨著我們進(jìn)行更改和增長(zhǎng)網(wǎng)站,手動(dòng)檢查一切正常工作所需的時(shí)間只會(huì)增長(zhǎng)。 如果我們像我們一樣繼續(xù)下去,最終我們將花大部分時(shí)間進(jìn)行測(cè)試,并且很少有時(shí)間改進(jìn)我們的代碼。

自動(dòng)化測(cè)試真的可以幫助解決這個(gè)問題! 明顯的好處是,它們可以比手動(dòng)測(cè)試運(yùn)行得更快,可以測(cè)試更低的細(xì)節(jié)水平,并且每次都測(cè)試完全相同的功能(人類測(cè)試人員遠(yuǎn)沒有那么可靠!)因?yàn)樗鼈兪强焖俚淖詣?dòng)化測(cè)試 可以更經(jīng)常地執(zhí)行,并且如果測(cè)試失敗,它們指向代碼沒有按預(yù)期執(zhí)行的確切位置。

此外,自動(dòng)化測(cè)試可以充當(dāng)代碼的第一個(gè)真實(shí)世界的"用戶",迫使您嚴(yán)格定義和記錄您的網(wǎng)站的行為方式。 通常它們是您的代碼示例和文檔的基礎(chǔ)。 由于這些原因,一些軟件開發(fā)過程從測(cè)試定義和實(shí)現(xiàn)開始,之后編寫代碼以匹配所需的行為(例如 Test-driven_development">測(cè)試驅(qū)動(dòng)行為驅(qū)動(dòng)的開發(fā))。

本教程介紹了如何為Django編寫自動(dòng)測(cè)試,方法是在 LocalLibrary 網(wǎng)站中添加一些測(cè)試。

測(cè)試類型

有許多類型,級(jí)別和測(cè)試和測(cè)試方法的分類。 最重要的自動(dòng)化測(cè)試是:

Unit tests
Verify functional behavior of individual components, often to class and function level.
Regression tests
Tests that reproduce historic bugs. Each test is initially run to verify that the bug has been fixed, and then re-run to ensure that it has not been reintroduced following later changes to the code.
Integration tests
Verify how groupings of components work when used together. Integration tests are aware of the required interactions between components, but not necessarily of the internal operations of each component. They may cover simple groupings of components through to the whole website.

注意:其他常見類型的測(cè)試包括黑盒,白盒,手動(dòng),自動(dòng),金絲雀,煙霧,一致性,驗(yàn)收,功能,系統(tǒng),性能,負(fù)載和壓力測(cè)試。 查找更多信息。

Django提供了什么用于測(cè)試?

測(cè)試網(wǎng)站是一項(xiàng)復(fù)雜的任務(wù),因?yàn)樗蓭讓舆壿嫿M成 - 從HTTP級(jí)請(qǐng)求處理,查詢模型到表單驗(yàn)證和處理,以及模板呈現(xiàn)。

Django提供了一個(gè)基于Python標(biāo)準(zhǔn)的小層次類的測(cè)試框架 unittest library.\">unittest"> unittest 庫。盡管有這個(gè)名字,這個(gè)測(cè)試框架適用于單元測(cè)試和集成測(cè)試。 Django框架添加了API方法和工具來幫助測(cè)試web和Django特定的行為。這些允許您模擬請(qǐng)求,插入測(cè)試數(shù)據(jù)和檢查應(yīng)用程序的輸出。 LiveServerTestCase) and tools for Django還提供了一個(gè)API( LiveServerTestCase )和工具, using different testing frameworks, for example you can\">a class ="external">使用不同的測(cè)試框架,例如,您可以Selenium framework to simulate a user interacting with a live browser.\">與流行的 Selenium 框架集成,以模擬用戶與實(shí)時(shí)瀏覽器進(jìn)行交互。

unittest)?test base classes (要寫一個(gè)測(cè)試,你從任何Django(或 unittest )測(cè)試基類(SimpleTestCase,?\">topic / testing / tools /#simpletestcase"> SimpleTestCase , ,?TestCase,?TransactionTestCase , TestCase ,LiveServerTestCase) and then write separate methods to check that specific functionality works as expected (\">"external"> LiveServerTestCase ),然后編寫單獨(dú)的方法來檢查特定功能是否按預(yù)期工作True or False?values,?or that two values are equal, etc.) When you start a test run, the framework\">測(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ù)。 該類還擁有測(cè)試客戶端 您可以使用模擬用戶在視圖級(jí)別與代碼交互。 在下面的章節(jié)中,我們將集中在使用 > TestCase 基類。

你應(yīng)該測(cè)試什么?

您應(yīng)該測(cè)試自己的代碼的所有方面,但不是作為Python或Django的一部分提供的任何庫或功能。

例如,考慮下面定義的 Author 模型。 您不需要明確測(cè)試 first_name last_name 已正確存儲(chǔ)為 CharField 在數(shù)據(jù)庫中,因?yàn)檫@是由Django定義的 雖然當(dāng)然在實(shí)踐中,你將不可避免地在開發(fā)期間測(cè)試這個(gè)功能)。 也不需要測(cè)試 date_of_birth 已經(jīng)驗(yàn)證為日期字段,因?yàn)檫@也是在Django中實(shí)現(xiàn)的。

但是,您應(yīng)該檢查用于標(biāo)簽的文本(名字,姓氏,出生日期,已過帳)以及分配給文本的字段大小( 100個(gè)字符 ),因?yàn)檫@些是你的設(shè)計(jì)的一部分,可以在將來打破/改變的東西。

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() :normal;"> __ str __()按需運(yùn)行,因?yàn)樗鼈兪悄拇a/業(yè)務(wù)邏輯。 在 get_absolute_url()的情況下,你可以相信Django reverse() 正確,所以你測(cè)試的是關(guān)聯(lián)的視圖實(shí)際上已經(jīng)被定義。

請(qǐng)注意:精明的讀者可能會(huì)注意到,我們還希望將出生日期和死亡日期限制為合理的價(jià)值,并檢查死亡是否在出生后出現(xiàn)。 在Django中,這個(gè)約束將被添加到你的表單類中(雖然你可以為這些字段定義驗(yàn)證器,它們只能在表單級(jí)別使用,而不能在模型級(jí)別使用)。

考慮到這一點(diǎn),我們開始考慮如何定義和運(yùn)行測(cè)試。

測(cè)試結(jié)構(gòu)概述

在我們進(jìn)入"要測(cè)試什么"的詳細(xì)信息之前,讓我們先簡(jiǎn)單看一下 測(cè)試的定義。

Django使用unittest模塊的內(nèi)置測(cè)試發(fā)現(xiàn), 它將在以 test * .py 模式命名的任何文件中的當(dāng)前工作目錄下發(fā)現(xiàn)測(cè)試。 如果您適當(dāng)?shù)孛募?,您可以使用任何您喜歡的結(jié)構(gòu)。 我們建議您為測(cè)試代碼創(chuàng)建一個(gè)模塊,并為模型,視圖,表單和您需要測(cè)試的任何其他類型的代碼分別建立文件。 例如:

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è)試文件。

注意:當(dāng)我們構(gòu)建Django框架網(wǎng)站時(shí),會(huì)自動(dòng)創(chuàng)建骨架測(cè)試文件 /catalog/tests.py / a>。 將所有測(cè)試放在其中是完全"合法的",但是如果你正確測(cè)試,你會(huì)很快結(jié)束一個(gè)非常大和難以管理的測(cè)試文件。

刪除骨架文件,因?yàn)槲覀儾恍枰?/p>

打開 /catalog/tests/test_models.py 。 該文件已經(jīng)導(dǎo)入 django.test.TestCase ,如下所示:

from django.test import TestCase

# Create your tests here.

通常,您將為要測(cè)試的每個(gè)模型/視圖/表單添加一個(gè)測(cè)試類,并使用單個(gè)方法來測(cè)試特定功能。 在其他情況下,您可能希望有一個(gè)單獨(dú)的類來測(cè)試特定的用例,使用單獨(dú)的測(cè)試函數(shù)來測(cè)試該用例的各個(gè)方面(例如,一個(gè)類,用于測(cè)試模型字段是否經(jīng)過正確驗(yàn)證, 每個(gè)可能的故障情況)。 再次,結(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)

新類定義了兩種可用于預(yù)測(cè)試配置的方法(例如,創(chuàng)建測(cè)試所需的任何模型或其他對(duì)象):

  • setUpTestData() is called once at the beginning of the test run for class-level setup. You'd use this to create objects that aren't going to be modified or changed in any of the test methods.
  • setUp() is called before every test function to set up any objects that may be modified by the test (every test function will get a "fresh" version of these objects).

測(cè)試類也有一個(gè) tearDown()方法,我們沒有使用。 此方法對(duì)數(shù)據(jù)庫測(cè)試不是特別有用,因?yàn)?code> TestCase 基類為您處理數(shù)據(jù)庫拆卸。

下面我們有一些測(cè)試方法,使用 Assert 函數(shù)來測(cè)試條件是否為真,假或等于( AssertTrue AssertFalse AssertEqual )。 如果條件未按預(yù)期進(jìn)行評(píng)估,則測(cè)試將失敗,并將錯(cuò)誤報(bào)告給控制臺(tái)。

AssertTrue , AssertFalse , AssertEqual 是由 unittest 提供的標(biāo)準(zhǔn)斷言。 框架中還有其他標(biāo)準(zhǔn)斷言,還有 Django特定的斷言 / a>來測(cè)試視圖是否重定向( assertRedirects ),以測(cè)試特定模板是否已被使用( assertTemplateUsed )等。

您應(yīng)該通常在測(cè)試中包括 print()功能,如上所示。 我們?cè)谶@里只做,以便您可以看到在控制臺(tái)中調(diào)用安裝函數(shù)的順序(在下一節(jié))。

如何運(yùn)行測(cè)試

運(yùn)行所有測(cè)試的最簡(jiǎn)單的方法是使用命令:

python3 manage.py test

這將發(fā)現(xiàn)以當(dāng)前目錄下的 test * .py 模式命名的所有文件,并運(yùn)行使用適當(dāng)?shù)幕惗x的所有測(cè)試(這里我們有一些測(cè)試文件,但只有 / catalog /tests/test_models.py 目前包含任何測(cè)試。)默認(rèn)情況下,測(cè)試將單獨(dú)報(bào)告測(cè)試失敗,然后是測(cè)試摘要。

LocalLibrary 的根目錄中運(yùn)行測(cè)試。 你應(yīng)該看到一個(gè)類似下面的輸出。

>python 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è)試失敗,我們可以看到什么函數(shù)失敗和原因(這個(gè)失敗是期望的,因?yàn)?code> False 不是 True !

提示:從上面的測(cè)試輸出中學(xué)習(xí)的最重要的事情是,如果您為對(duì)象和方法使用描述性/信息性名稱,它更有價(jià)值。

上述粗體中顯示的文本通常不會(huì)顯示在測(cè)試輸出中(這是由測(cè)試中的 print()函數(shù)生成的)。 這顯示了 setUpTestData()方法如何調(diào)用一次,而在每個(gè)方法之前調(diào)用 setUp()。

接下來的部分顯示了如何運(yùn)行特定的測(cè)試,以及如何控制測(cè)試顯示多少信息。

顯示更多測(cè)試信息

如果您想獲得有關(guān)測(cè)試運(yùn)行的更多信息,可以更改詳細(xì)信息。 例如,要列出測(cè)試成功以及失敗(以及有關(guān)如何設(shè)置測(cè)試數(shù)據(jù)庫的一大堆信息),您可以將verbosity設(shè)置為"2",如下所示:

python3 manage.py test --verbosity 2

允許的詳細(xì)程度級(jí)別為0,1,2和3,默認(rèn)值為"1"。

運(yùn)行特定測(cè)試

如果要運(yùn)行測(cè)試的一個(gè)子集,可以通過指定package(s),module, TestCase 子類或方法的完整的點(diǎn)路徑來實(shí)現(xiàn):

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è)試

現(xiàn)在我們知道如何運(yùn)行我們的測(cè)試和什么樣的東西我們需要測(cè)試,讓我們看看一些實(shí)際的例子。

請(qǐng)注意:我們不會(huì)編寫每一個(gè)可能的測(cè)試,但這應(yīng)該給你和測(cè)試如何工作的想法,以及你能做什么。

楷模

如上所述,我們應(yīng)該測(cè)試任何作為我們?cè)O(shè)計(jì)的一部分或由我們編寫的代碼定義的東西,而不是測(cè)試已經(jīng)由Django或Python開發(fā)團(tuán)隊(duì)測(cè)試的庫/代碼。

例如,考慮下面的 Author 模型。 這里我們應(yīng)該測(cè)試所有字段的標(biāo)簽,因?yàn)榧词刮覀儧]有明確指定大多數(shù)字段,我們有一個(gè)設(shè)計(jì),說明這些值應(yīng)該是什么。 如果我們不測(cè)試值,那么我們不知道字段標(biāo)簽有其預(yù)期的值。 同樣,當(dāng)我們相信Django將創(chuàng)建一個(gè)指定長(zhǎng)度的字段時(shí),值得為此長(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 ,然后使用作者模型的以下測(cè)試代碼替換任何現(xiàn)有代碼。

在這里,您將看到我們首先導(dǎo)入 TestCase ,并使用描述性名稱從中導(dǎo)出我們的測(cè)試類( AuthorModelTest ),以便我們可以輕松識(shí)別測(cè)試中的任何失敗的測(cè)試 輸出。 然后我們調(diào)用 setUpTestData()創(chuàng)建一個(gè)作者對(duì)象,我們將在任何測(cè)試中使用但不修改。

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

有趣的事情要注意:

  • We can't get the verbose_name directly using author.first_name.verbose_name, because author.first_name is a string (not a handle to the first_name object that we can use to access its properties). Instead we need to use the author's _meta attribute to get an instance of the field and use that to query for the additional information.
  • We chose to use assertEquals(field_label,'first name') rather than assertTrue(field_label == 'first name'). The reason for this is that if the test fails the output for the former tells you what the label actually was, which makes debugging the problem just a little easier.

注意:測(cè)試 last_name date_of_birth 標(biāo)簽,以及測(cè)試 last_name 字段的長(zhǎng)度 被省略。 現(xiàn)在添加您自己的版本,遵循上面所示的命名約定和方法。

我們還需要測(cè)試我們的自定義方法。 這些基本上只是檢查對(duì)象名稱是否按照我們的預(yù)期使用"Surname,"Christian name"格式構(gòu)造,并且我們獲得的 Author 項(xiàng)目的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ì)得到 date_of_birth 標(biāo)簽的錯(cuò)誤,如下所示。 測(cè)試失敗,因?yàn)樗菍懫谕麡?biāo)簽定義遵循Django的慣例不大寫標(biāo)簽的第一個(gè)字母(Django為您做這個(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ò)誤,但它突出了寫測(cè)試如何更徹底地檢查您可能已經(jīng)做出的任何假設(shè)。

請(qǐng)注意:將date_of_death字段(/catalog/models.py)的標(biāo)簽更改為"已死亡",然后重新運(yùn)行測(cè)試。

用于測(cè)試其他模型的模式是類似的,因此我們不會(huì)繼續(xù)進(jìn)一步討論這些模式。 隨時(shí)為我們的其他模型創(chuàng)建自己的測(cè)試。

形式

測(cè)試你的表單的哲學(xué)和測(cè)試你的模型是一樣的; 你需要測(cè)試任何你編碼的或你的設(shè)計(jì)指定,但不是底層框架和其他第三方庫的行為。

一般來說,這意味著您應(yīng)該測(cè)試表單是否具有您想要的字段,并且這些字段顯示有適當(dāng)?shù)臉?biāo)簽和幫助文本。 您不需要驗(yàn)證Django是否正確驗(yàn)證字段類型(除非您創(chuàng)建了自己的自定義字段和驗(yàn)證),即您不需要測(cè)試電子郵件字段只接受電子郵件。 但是,您需要測(cè)試希望對(duì)字段執(zhí)行的任何其他驗(yàn)證以及代碼將為錯(cuò)誤生成的任何消息。

考慮我們的圖書更新形式。 這只有一個(gè)字段用于續(xù)訂日期,該字段將有一個(gè)標(biāo)簽和幫助文本,我們需要驗(yàn)證。

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è)試與測(cè)試時(shí)間相關(guān)的功能。 然后,我們以與我們對(duì)模型相同的方式聲明我們的形式測(cè)試類,為我們的 TestCase -derived測(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è)試字段的標(biāo)簽 help_text 符合預(yù)期。 我們必須使用字段字典訪問字段(例如 form.fields [\'renewal_date\'] )。 注意這里我們還要測(cè)試標(biāo)簽值是否是 None ,因?yàn)榧词笵jango會(huì)渲染正確的標(biāo)簽,如果值不是明確的,它返回 None 設(shè)置。

其余的函數(shù)測(cè)試表單是否在可接受范圍內(nèi)的續(xù)訂日期有效,并且對(duì)范圍之外的值無效。 請(qǐng)注意我們?nèi)绾问褂?code> datetime.timedelta()(在這種情況下指定天數(shù)或 周)。 然后我們創(chuàng)建表單,傳入我們的數(shù)據(jù),并測(cè)試它是否有效。

注意:這里我們實(shí)際上并不使用數(shù)據(jù)庫或測(cè)試客戶端。 請(qǐng)考慮修改這些測(cè)試,以使用 SimpleTestCase 。

我們還需要驗(yàn)證在表單無效時(shí)引發(fā)正確的錯(cuò)誤,但這通常是作為視圖處理的一部分進(jìn)行的,所以我們將在下一節(jié)中介紹。

這是所有的形式; 我們有一些其他的,但它們是由我們的基于類的基于編輯的視圖自動(dòng)創(chuàng)建的,應(yīng)該在那里測(cè)試! 運(yùn)行測(cè)試并確認(rèn)我們的代碼仍然通過!

視圖

要驗(yàn)證我們的觀看行為,我們使用Django測(cè)試客戶端 / a>。 這個(gè)類的行為就像一個(gè)虛擬的網(wǎng)絡(luò)瀏覽器,我們可以使用它來模擬對(duì)網(wǎng)址的 GET POST 請(qǐng)求并觀察響應(yīng)。 我們可以看到幾乎所有的響應(yīng),從低級(jí)HTTP(結(jié)果頭和狀態(tài)代碼)到我們用來渲染HTML和我們傳遞給它的上下文數(shù)據(jù)的模板。 我們還可以看到重定向鏈(如果有的話),并在每個(gè)步驟檢查URL和狀態(tài)代碼。 這允許我們驗(yàn)證每個(gè)視圖是在做什么預(yù)期。

讓我們從我們最簡(jiǎn)單的視圖開始,它提供了所有作者的列表。 這會(huì)顯示在網(wǎng)址 / catalog / authors / (網(wǎng)址配置中名為"authors"的網(wǎng)址)中。

class AuthorListView(generic.ListView):
    model = Author
    paginate_by = 10

因?yàn)檫@是一個(gè)通用的列表視圖,幾乎一切都由我們?yōu)镈jango做。 可以說,如果你信任Django,那么你唯一需要測(cè)試的是視圖是可以訪問的正確的URL,可以使用它的名稱訪問。 然而,如果你使用測(cè)試驅(qū)動(dòng)開發(fā)過程,你將開始編寫測(cè)試,確認(rèn)視圖顯示所有作者,分頁他們?cè)?0。

打開 /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.core.urlresolvers 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 的派生類)來模擬一個(gè) 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),我們查詢它的狀態(tài)代碼,使用的模板,響應(yīng)是否分頁,返回的項(xiàng)目數(shù)和項(xiàng)目總數(shù)。

我們上面演示的最有趣的變量是 resp.context ,它是視圖傳遞給模板的上下文變量。 這對(duì)于測(cè)試非常有用,因?yàn)樗试S我們確認(rèn)我們的模板獲得所需的所有數(shù)據(jù)。 換句話說,我們可以檢查我們是否使用了預(yù)期的模板和模板獲得的數(shù)據(jù),這很大程度上要驗(yàn)證任何渲染問題都是由于模板。

Views that are restricted to logged in users

在某些情況下,您需要測(cè)試僅限登錄用戶的視圖。 例如,我們的 LoanedBooksByUserListView 與我們以前的視圖非常相似,但只適用于已登錄的用戶,并且只顯示當(dāng)前用戶借用的 BookInstance 記錄, 貸款"狀態(tài),并命令"最早的第一"。

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)槲覀兩院髮⑿薷囊恍?duì)象。

注意:以下 setUp()代碼會(huì)創(chuàng)建指定語言的圖書,但 Language 模型,因?yàn)樗亲鳛橐粋€(gè)挑戰(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=genre_objects_for_book
? ? ? ? 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)證如果用戶未登錄,視圖將重定向到登錄頁面,請(qǐng)使用 assertRedirects ,如 test_redirect_if_not_logged_in()中所示。 要驗(yàn)證是否為登錄用戶顯示該頁面,我們首先登錄我們的測(cè)試用戶,然后再次訪問該頁面,并檢查我們是否獲得了一個(gè) 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è)試,如果你愿意!

Testing views with forms

使用表單測(cè)試視圖比上面的情況更復(fù)雜一些,因?yàn)槟枰獪y(cè)試更多的代碼路徑:初始顯示,數(shù)據(jù)驗(yàn)證失敗后顯示,驗(yàn)證成功后顯示。 好消息是,我們使用客戶端進(jìn)行測(cè)試,幾乎與我們對(duì)僅顯示視圖的方式一樣。

為了演示,讓我們?yōu)橛糜诟聢D書的視圖編寫一些測(cè)試(r enew_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è)試該視圖是否只對(duì)具有 can_mark_returned 權(quán)限的用戶可用,并且如果用戶嘗試?yán)m(xù)訂 BookInstance ,則會(huì)將用戶重定向到HTTP 404錯(cuò)誤頁面, 代碼>不存在。 我們應(yīng)該檢查表單的初始值是否為未來3周的日期播種,如果驗(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=genre_objects_for_book
        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 。 我們還檢查是否使用了正確的模板。

  ? 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è)試方法,如下所示。 這將檢查表單的初始日期是三個(gè)星期。 注意我們?nèi)绾文軌蛟L問表單字段的初始值的值(以粗體顯示)。

? ? 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ì)檢查如果更新成功,則視圖重定向到所有借用圖書的列表。 這里不同的是,我們第一次展示如何使用客戶端 POST 數(shù)據(jù)。 后數(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ì)重定向到主頁"/"。 如果是這樣,修改測(cè)試代碼的最后兩行就像下面的代碼。 請(qǐng)求中的 follow = True 可確保請(qǐng)求返回最終目標(biāo)網(wǎng)址(因此檢查 / 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ù)制到類中,如下所示。 這些會(huì)再次測(cè)試 POST 請(qǐng)求,但在這種情況下,請(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è)試另一個(gè)視圖。

模板

Django提供測(cè)試API來檢查視圖是否調(diào)用了正確的模板,并允許您驗(yàn)證是否正在發(fā)送正確的信息。 但是沒有特定的API支持在Django中測(cè)試您的HTML輸出是否按預(yù)期呈現(xiàn)。

Django的測(cè)試框架可以幫助你編寫有效的單元和集成測(cè)試 - 我們只是劃破了底層單元測(cè)試框架的表面,更不用說Django的添加了(例如,查看如何使用 unittest.mock 修補(bǔ)第三方庫,以便更徹底地 測(cè)試你自己的代碼)。

雖然有許多其他測(cè)試工具,您可以使用,我們將突出兩個(gè):

  • Coverage: This Python tool reports on how much of your code is actually executed by your tests. It is particularly useful when you're getting started, and you are trying to work out exactly what you should test.
  • Selenium is a framework to automate testing in a real browser. It allows you to simulate a real user interacting with the site, and provides a great framework for system testing your site (the next step up from integration testing.

挑戰(zhàn)自己

有更多的模型和意見,我們可以測(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)記住,您需要檢查您指定的或設(shè)計(jì)的一部分。 這將包括誰有訪問權(quán)限,初始日期,使用的模板以及視圖在成功時(shí)重定向的位置。

概要

編寫測(cè)試代碼既不是有趣也不迷人,因此通常留在最后(或根本不)在創(chuàng)建網(wǎng)站。 然而,它是確保您的代碼在進(jìn)行更改后可以安全釋放并且具有成本效益的維護(hù)的重要部分。

在本教程中,我們向您展示了如何為模型,表單和視圖編寫和運(yùn)行測(cè)試。 最重要的是,我們簡(jiǎn)要總結(jié)了你應(yīng)該測(cè)試什么,這在開始使用時(shí)通常是最難的。 還有很多要知道的,但即使你已經(jīng)學(xué)到了,你應(yīng)該能夠?yàn)槟木W(wǎng)站創(chuàng)建有效的單元測(cè)試。

下一個(gè)和最后一個(gè)教程將展示如何部署您的精彩(和完全測(cè)試!)Dango網(wǎng)站。

也可以看看

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)