Django Tutorial Part 10: Testing a Django web application

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

概述

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

自動化測試真的可以幫助解決這個問題! 明顯的好處是,它們可以比手動測試運行得更快,可以測試更低的細節(jié)水平,并且每次都測試完全相同的功能(人類測試人員遠沒有那么可靠!)因為它們是快速的自動化測試 可以更經常地執(zhí)行,并且如果測試失敗,它們指向代碼沒有按預期執(zhí)行的確切位置。

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

本教程介紹了如何為Django編寫自動測試,方法是在 LocalLibrary 網站中添加一些測試。

測試類型

有許多類型,級別和測試和測試方法的分類。 最重要的自動化測試是:

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.

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

Django提供了什么用于測試?

測試網站是一項復雜的任務,因為它由幾層邏輯組成 - 從HTTP級請求處理,查詢模型到表單驗證和處理,以及模板呈現。

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

unittest)?test base classes (要寫一個測試,你從任何Django(或 unittest )測試基類(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 ),然后編寫單獨的方法來檢查特定功能是否按預期工作True or False?values,?or that two values are equal, etc.) When you start a test run, the framework\">測試使用"assert"方法來測試表達式是否導致 True False 值,或兩個值相等等)。當您開始測試運行時,在您的派生類中執(zhí)行所選的測試方法。測試方法獨立運行,在類中定義的常見設置和/或拆除行為,如下所示。

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)

大多數測試的最佳基本類型是 django.test.TestCase >。 此測試類在運行測試之前創(chuàng)建一個干凈的數據庫,并在其自己的事務中運行每個測試函數。 該類還擁有測試客戶端 您可以使用模擬用戶在視圖級別與代碼交互。 在下面的章節(jié)中,我們將集中在使用 > TestCase 基類。

你應該測試什么?

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

例如,考慮下面定義的 Author 模型。 您不需要明確測試 first_name last_name 已正確存儲為 CharField 在數據庫中,因為這是由Django定義的 雖然當然在實踐中,你將不可避免地在開發(fā)期間測試這個功能)。 也不需要測試 date_of_birth 已經驗證為日期字段,因為這也是在Django中實現的。

但是,您應該檢查用于標簽的文本(名字,姓氏,出生日期,已過帳)以及分配給文本的字段大小( 100個字符 ),因為這些是你的設計的一部分,可以在將來打破/改變的東西。

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)

同樣,您應該檢查自定義方法 get_absolute_url() :normal;"> __ str __()按需運行,因為它們是您的代碼/業(yè)務邏輯。 在 get_absolute_url()的情況下,你可以相信Django reverse() 正確,所以你測試的是關聯的視圖實際上已經被定義。

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

考慮到這一點,我們開始考慮如何定義和運行測試。

測試結構概述

在我們進入"要測試什么"的詳細信息之前,讓我們先簡單看一下 測試的定義。

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

catalog/
? /tests/
?   __init__.py
?   test_models.py
?   test_forms.py
?   test_views.py

在您的 LocalLibrary 項目中創(chuàng)建如上所示的文件結構。 __ init __。py 應該是一個空文件(這告訴Python該目錄是一個包)。 您可以通過復制和重命名骨架測試文件 /catalog/tests.py 來創(chuàng)建三個測試文件。

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

刪除骨架文件,因為我們不需要它。

打開 /catalog/tests/test_models.py 。 該文件已經導入 django.test.TestCase ,如下所示:

from django.test import TestCase

# Create your tests here.

通常,您將為要測試的每個模型/視圖/表單添加一個測試類,并使用單個方法來測試特定功能。 在其他情況下,您可能希望有一個單獨的類來測試特定的用例,使用單獨的測試函數來測試該用例的各個方面(例如,一個類,用于測試模型字段是否經過正確驗證, 每個可能的故障情況)。 再次,結構是非常取決于你,但最好是如果你是一致的。

將下面的測試類添加到文件的底部。 該類演示了如何通過從 TestCase 派生來構造測試用例類。

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)

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

  • 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).

測試類也有一個 tearDown()方法,我們沒有使用。 此方法對數據庫測試不是特別有用,因為 TestCase 基類為您處理數據庫拆卸。

下面我們有一些測試方法,使用 Assert 函數來測試條件是否為真,假或等于( AssertTrue , AssertFalse AssertEqual )。 如果條件未按預期進行評估,則測試將失敗,并將錯誤報告給控制臺。

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

您應該通常在測試中包括 print()功能,如上所示。 我們在這里只做,以便您可以看到在控制臺中調用安裝函數的順序(在下一節(jié))。

如何運行測試

運行所有測試的最簡單的方法是使用命令:

python3 manage.py test

這將發(fā)現以當前目錄下的 test * .py 模式命名的所有文件,并運行使用適當的基類定義的所有測試(這里我們有一些測試文件,但只有 / catalog /tests/test_models.py 目前包含任何測試。)默認情況下,測試將單獨報告測試失敗,然后是測試摘要。

LocalLibrary 的根目錄中運行測試。 你應該看到一個類似下面的輸出。

>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'...

這里我們看到我們有一個測試失敗,我們可以看到什么函數失敗和原因(這個失敗是期望的,因為 False 不是 True !

提示:從上面的測試輸出中學習的最重要的事情是,如果您為對象和方法使用描述性/信息性名稱,它更有價值。

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

接下來的部分顯示了如何運行特定的測試,以及如何控制測試顯示多少信息。

顯示更多測試信息

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

python3 manage.py test --verbosity 2

允許的詳細程度級別為0,1,2和3,默認值為"1"。

運行特定測試

如果要運行測試的一個子集,可以通過指定package(s),module, TestCase 子類或方法的完整的點路徑來實現:

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測試

現在我們知道如何運行我們的測試和什么樣的東西我們需要測試,讓我們看看一些實際的例子。

請注意:我們不會編寫每一個可能的測試,但這應該給你和測試如何工作的想法,以及你能做什么。

楷模

如上所述,我們應該測試任何作為我們設計的一部分或由我們編寫的代碼定義的東西,而不是測試已經由Django或Python開發(fā)團隊測試的庫/代碼。

例如,考慮下面的 Author 模型。 這里我們應該測試所有字段的標簽,因為即使我們沒有明確指定大多數字段,我們有一個設計,說明這些值應該是什么。 如果我們不測試值,那么我們不知道字段標簽有其預期的值。 同樣,當我們相信Django將創(chuàng)建一個指定長度的字段時,值得為此長度指定一個測試,以確保它按計劃實現。

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 ,然后使用作者模型的以下測試代碼替換任何現有代碼。

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

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')

字段測試檢查字段標簽( verbose_name )的值以及字符字段的大小是否符合預期。 這些方法都有描述性名稱,并遵循相同的模式:

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.

注意:測試 last_name date_of_birth 標簽,以及測試 last_name 字段的長度 被省略。 現在添加您自己的版本,遵循上面所示的命名約定和方法。

我們還需要測試我們的自定義方法。 這些基本上只是檢查對象名稱是否按照我們的預期使用"Surname,"Christian name"格式構造,并且我們獲得的 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')

立即運行測試。 如果您按照模型教程中的描述創(chuàng)建了作者模型,那么很可能您會得到 date_of_birth 標簽的錯誤,如下所示。 測試失敗,因為它是寫期望標簽定義遵循Django的慣例不大寫標簽的第一個字母(Django為您做這個)。

======================================================================
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
? ^

這是一個非常小的錯誤,但它突出了寫測試如何更徹底地檢查您可能已經做出的任何假設。

請注意:將date_of_death字段(/catalog/models.py)的標簽更改為"已死亡",然后重新運行測試。

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

形式

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

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

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

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 表單的以下測試代碼替換任何現有代碼。 我們首先導入我們的表單和一些Python和Django庫,以幫助測試與測試時間相關的功能。 然后,我們以與我們對模型相同的方式聲明我們的形式測試類,為我們的 TestCase -derived測試類使用描述性名稱。

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())

前兩個函數測試字段的標簽 help_text 符合預期。 我們必須使用字段字典訪問字段(例如 form.fields [\'renewal_date\'] )。 注意這里我們還要測試標簽值是否是 None ,因為即使Django會渲染正確的標簽,如果值不是明確的,它返回 None 設置。

其余的函數測試表單是否在可接受范圍內的續(xù)訂日期有效,并且對范圍之外的值無效。 請注意我們如何使用 datetime.timedelta()(在這種情況下指定天數或 周)。 然后我們創(chuàng)建表單,傳入我們的數據,并測試它是否有效。

注意:這里我們實際上并不使用數據庫或測試客戶端。 請考慮修改這些測試,以使用 SimpleTestCase

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

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

視圖

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

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

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

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

打開 /catalog/tests/test_views.py 文件,并使用 AuthorListView 的以下測試代碼替換任何現有文本。 和以前一樣,我們導入我們的模型和一些有用的類。 在 setUpTestData()方法中,我們設置了一些 Author 對象,以便我們可以測試我們的分頁。

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)

所有測試使用客戶端(屬于我們的 TestCase 的派生類)來模擬一個 GET 請求并獲得響應( resp )。 第一個版本檢查特定的URL(注意,只是沒有域的特定路徑),第二個版本從URL配置中的名稱生成URL。

resp = self.client.get('/catalog/authors/')
resp = self.client.get(reverse('authors'))

一旦我們有響應,我們查詢它的狀態(tài)代碼,使用的模板,響應是否分頁,返回的項目數和項目總數。

我們上面演示的最有趣的變量是 resp.context ,它是視圖傳遞給模板的上下文變量。 這對于測試非常有用,因為它允許我們確認我們的模板獲得所需的所有數據。 換句話說,我們可以檢查我們是否使用了預期的模板和模板獲得的數據,這很大程度上要驗證任何渲染問題都是由于模板。

Views that are restricted to logged in users

在某些情況下,您需要測試僅限登錄用戶的視圖。 例如,我們的 LoanedBooksByUserListView 與我們以前的視圖非常相似,但只適用于已登錄的用戶,并且只顯示當前用戶借用的 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')

將以下測試代碼添加到 /catalog/tests/test_views.py 。 這里我們首先使用 SetUp()創(chuàng)建一些用戶登錄帳戶和 BookInstance 對象(以及它們相關的書和其他記錄),我們稍后將在測試中使用它們。 一半的書籍是由每個測試用戶借用的,但我們最初將所有書籍的狀態(tài)設置為"維護"。 我們使用 SetUp()而不是 setUpTestData(),因為我們稍后將修改一些對象。

注意:以下 setUp()代碼會創(chuàng)建指定語言的圖書,但 Language 模型,因為它是作為一個挑戰(zhàn)創(chuàng)建的。 如果是這種情況,只需注釋掉創(chuà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')

要驗證如果用戶未登錄,視圖將重定向到登錄頁面,請使用 assertRedirects ,如 test_redirect_if_not_logged_in()中所示。 要驗證是否為登錄用戶顯示該頁面,我們首先登錄我們的測試用戶,然后再次訪問該頁面,并檢查我們是否獲得了一個 status_code 為200(成功)。

其余的測試驗證我們的觀點只返回貸款給我們當前的借款人的圖書。 復制上面測試類末尾的(自解釋)代碼。

? ? 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)

你也可以添加分頁測試,如果你愿意!

Testing views with forms

使用表單測試視圖比上面的情況更復雜一些,因為您需要測試更多的代碼路徑:初始顯示,數據驗證失敗后顯示,驗證成功后顯示。 好消息是,我們使用客戶端進行測試,幾乎與我們對僅顯示視圖的方式一樣。

為了演示,讓我們?yōu)橛糜诟聢D書的視圖編寫一些測試(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})

我們需要測試該視圖是否只對具有 can_mark_returned 權限的用戶可用,并且如果用戶嘗試續(xù)訂 BookInstance ,則會將用戶重定向到HTTP 404錯誤頁面, 代碼>不存在。 我們應該檢查表單的初始值是否為未來3周的日期播種,如果驗證成功,我們將重定向到"全借書"視圖。 作為檢查驗證失敗測試的一部分,我們還將檢查我們的表單是否發(fā)送相應的錯誤消息。

將測試類的第一部分(如下所示)添加到 /catalog/tests/test_views.py 的底部。 這將創(chuàng)建兩個用戶和兩個圖書實例,但只向一個用戶授予訪問視圖所需的權限。 在測試期間授予權限的代碼以粗體顯示:

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')

將以下測試添加到測試類的底部。 這些檢查只有具有正確權限的用戶( testuser2 )才能訪問視圖。 我們檢查所有情況:當用戶沒有登錄時,當用戶登錄但沒有正確的權限,當用戶有權限但不是借款人(應該成功),以及當他們嘗試 訪問不存在的 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')

添加下一個測試方法,如下所示。 這將檢查表單的初始日期是三個星期。 注意我們如何能夠訪問表單字段的初始值的值(以粗體顯示)。

? ? 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 )

下一個測試(將它添加到類中)會檢查如果更新成功,則視圖重定向到所有借用圖書的列表。 這里不同的是,我們第一次展示如何使用客戶端 POST 數據。 后數據是post函數的第二個參數,并被指定為鍵/值的字典。

? ? 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),您的代碼可能會重定向到主頁"/"。 如果是這樣,修改測試代碼的最后兩行就像下面的代碼。 請求中的 follow = True 可確保請求返回最終目標網址(因此檢查 / 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/')

將最后兩個函數復制到類中,如下所示。 這些會再次測試 POST 請求,但在這種情況下,請求的續(xù)訂日期無效。 我們使用 assertFormError()來驗證錯誤消息是否符合預期。

? ? 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')

相同類型的技術可以用于測試另一個視圖。

模板

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

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

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

  • 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)自己

有更多的模型和意見,我們可以測試。 作為一個簡單的任務,嘗試為 AuthorCreate 視圖創(chuàng)建一個測試用例。

class AuthorCreate(PermissionRequiredMixin, CreateView):
    model = Author
    fields = '__all__'
    initial={'date_of_death':'12/10/2016',}
    permission_required = 'catalog.can_mark_returned'

請記住,您需要檢查您指定的或設計的一部分。 這將包括誰有訪問權限,初始日期,使用的模板以及視圖在成功時重定向的位置。

概要

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

在本教程中,我們向您展示了如何為模型,表單和視圖編寫和運行測試。 最重要的是,我們簡要總結了你應該測試什么,這在開始使用時通常是最難的。 還有很多要知道的,但即使你已經學到了,你應該能夠為您的網站創(chuàng)建有效的單元測試。

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

也可以看看

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號