進階測試主題¶
請求工廠¶
RequestFactory
與測試客戶端共享相同的 API。然而,與其像瀏覽器一樣運作,RequestFactory 提供了一種產生請求實例的方法,該實例可以用作任何視圖的第一個參數。這表示您可以像測試任何其他函式一樣測試視圖函式,作為黑盒子,具有明確已知的輸入,並測試特定的輸出。
RequestFactory
的 API 是測試客戶端 API 的略微受限子集。
它僅能存取 HTTP 方法
get()
、post()
、put()
、delete()
、head()
、options()
和trace()
。這些方法接受所有相同的參數,除了
follow
之外。由於這只是一個產生請求的工廠,因此由您來處理回應。它不支援中介軟體。如果視圖要正常運作,則必須由測試本身提供工作階段和驗證屬性。
新增了 query_params
參數。
範例¶
以下是使用請求工廠的單元測試
from django.contrib.auth.models import AnonymousUser, User
from django.test import RequestFactory, TestCase
from .views import MyView, my_view
class SimpleTest(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = User.objects.create_user(
username="jacob", email="jacob@…", password="top_secret"
)
def test_details(self):
# Create an instance of a GET request.
request = self.factory.get("/customer/details")
# Recall that middleware are not supported. You can simulate a
# logged-in user by setting request.user manually.
request.user = self.user
# Or you can simulate an anonymous user by setting request.user to
# an AnonymousUser instance.
request.user = AnonymousUser()
# Test my_view() as if it were deployed at /customer/details
response = my_view(request)
# Use this syntax for class-based views.
response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200)
AsyncRequestFactory¶
RequestFactory
建立類似 WSGI 的請求。如果您想建立類似 ASGI 的請求,包括具有正確的 ASGI scope
,則可以改用 django.test.AsyncRequestFactory
。
此類別與 RequestFactory
直接 API 相容,唯一的差別在於它會傳回 ASGIRequest
實例,而不是 WSGIRequest
實例。它的所有方法仍然是同步可呼叫的。
defaults
中的任意關鍵字引數會直接新增到 ASGI 範圍中。
新增了 query_params
參數。
測試基於類別的視圖¶
為了在請求/回應週期之外測試基於類別的視圖,您必須確保它們已正確配置,方法是在實例化之後呼叫 setup()
。
例如,假設有以下基於類別的視圖
views.py
¶from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = "myapp/home.html"
def get_context_data(self, **kwargs):
kwargs["environment"] = "Production"
return super().get_context_data(**kwargs)
您可以直接測試 get_context_data()
方法,方法是先實例化視圖,然後將 request
傳遞至 setup()
,然後繼續您的測試程式碼
tests.py
¶from django.test import RequestFactory, TestCase
from .views import HomeView
class HomePageTest(TestCase):
def test_environment_set_in_context(self):
request = RequestFactory().get("/")
view = HomeView()
view.setup(request)
context = view.get_context_data()
self.assertIn("environment", context)
測試和多個主機名稱¶
執行測試時,會驗證 ALLOWED_HOSTS
設定。這可讓測試客戶端區分內部和外部 URL。
支援多租戶或以其他方式根據請求的主機變更業務邏輯並在測試中使用自訂主機名稱的專案,必須在 ALLOWED_HOSTS
中包含這些主機。
這樣做的第一個選項是將主機新增到您的設定檔案。例如,docs.djangoproject.com 的測試套件包含以下內容
from django.test import TestCase
class SearchFormTestCase(TestCase):
def test_empty_get(self):
response = self.client.get(
"/en/dev/search/",
headers={"host": "docs.djangoproject.dev:8000"},
)
self.assertEqual(response.status_code, 200)
設定檔案包含專案支援的網域清單
ALLOWED_HOSTS = ["www.djangoproject.dev", "docs.djangoproject.dev", ...]
另一個選項是使用 override_settings()
或 modify_settings()
將所需的主機新增至 ALLOWED_HOSTS
。對於無法封裝自己的設定檔的獨立應用程式,或者對於網域清單不是靜態的專案 (例如,多租戶的子網域),此選項可能更佳。例如,您可以針對網域 http://otherserver/
撰寫測試,如下所示
from django.test import TestCase, override_settings
class MultiDomainTestCase(TestCase):
@override_settings(ALLOWED_HOSTS=["otherserver"])
def test_other_domain(self):
response = self.client.get("http://otherserver/foo/bar/")
執行測試時停用 ALLOWED_HOSTS
檢查 ( ALLOWED_HOSTS = ['*']
) 可防止測試客戶端在您遵循重新導向至外部 URL 時發出有用的錯誤訊息。
測試和多個資料庫¶
測試主要/複本配置¶
如果您正在測試具有主要/複本 (某些資料庫稱為主機/從屬) 複寫的多個資料庫配置,則建立測試資料庫的此策略會產生問題。建立測試資料庫時,不會有任何複寫,因此,在主要資料庫上建立的資料不會在複本上看到。
為了彌補這一點,Django 允許您定義資料庫為測試鏡像。請考慮以下 (簡化的) 範例資料庫配置
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "myproject",
"HOST": "dbprimary",
# ... plus some other settings
},
"replica": {
"ENGINE": "django.db.backends.mysql",
"NAME": "myproject",
"HOST": "dbreplica",
"TEST": {
"MIRROR": "default",
},
# ... plus some other settings
},
}
在此設定中,我們有兩個資料庫伺服器:dbprimary
,由資料庫別名 default
描述,以及 dbreplica
,由別名 replica
描述。如您所預期的,資料庫管理員已將 dbreplica
設定為 dbprimary
的讀取複本,因此在正常活動中,任何寫入 default
的動作都會出現在 replica
上。
如果 Django 建立兩個獨立的測試資料庫,這會中斷任何預期會發生複寫的測試。然而,replica
資料庫已設定為測試鏡像 (使用 MIRROR
測試設定),表示在測試期間,replica
應被視為 default
的鏡像。
當設定測試環境時,將不會建立 replica
的測試版本。而是會將連線到 replica
的連線重新導向至 default
。因此,寫入 default
的操作會出現在 replica
上,但這並非因為兩個資料庫之間有資料複寫,而是因為它們實際上是同一個資料庫。由於這取決於交易,測試必須使用 TransactionTestCase
而不是 TestCase
。
控制測試資料庫的建立順序¶
預設情況下,Django 會假設所有資料庫都依賴於 default
資料庫,因此總是會先建立 default
資料庫。然而,對於測試設定中其他資料庫的建立順序,則不作任何保證。
如果您的資料庫設定需要特定的建立順序,您可以使用 DEPENDENCIES
測試設定來指定現有的依賴關係。請考慮以下(簡化的)資料庫設定範例:
DATABASES = {
"default": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds"],
},
},
"diamonds": {
# ... db settings
"TEST": {
"DEPENDENCIES": [],
},
},
"clubs": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds"],
},
},
"spades": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds", "hearts"],
},
},
"hearts": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds", "clubs"],
},
},
}
在這個設定下,diamonds
資料庫會先建立,因為它是唯一沒有依賴關係的資料庫別名。接著會建立 default
和 clubs
別名(儘管這對別名的建立順序不保證),然後是 hearts
,最後是 spades
。
如果在 DEPENDENCIES
定義中有任何循環依賴關係,將會引發 ImproperlyConfigured
例外。
TransactionTestCase
的進階功能¶
- TransactionTestCase.available_apps¶
警告
此屬性為私有 API。未來可能會變更或移除,而不會有棄用期,例如為了適應應用程式載入的變更。
它用於最佳化 Django 自己的測試套件,該套件包含數百個模型,但不同應用程式中的模型之間沒有關聯。
預設情況下,
available_apps
設定為None
。在每個測試之後,Django 會呼叫flush
來重設資料庫狀態。這會清空所有資料表,並發出post_migrate
訊號,該訊號會為每個模型重新建立一個內容類型和四個權限。此操作會隨著模型數量的增加而變得越來越耗費資源。將
available_apps
設定為應用程式清單會指示 Django 的行為如同只有這些應用程式中的模型是可用的。TransactionTestCase
的行為會發生以下變更:在每個測試之前,會觸發
post_migrate
,為可用應用程式中的每個模型建立內容類型和權限,以防它們遺失。在每個測試之後,Django 只會清空可用應用程式中模型對應的資料表。然而,在資料庫層級,截斷可能會串聯到不可用應用程式中的相關模型。此外,不會觸發
post_migrate
;它將會在下一個TransactionTestCase
中觸發,在選取正確的應用程式集之後。
由於資料庫並未完全清空,如果測試建立未包含在
available_apps
中的模型實例,它們將會洩漏,並且可能會導致不相關的測試失敗。請小心使用會話的測試;預設的會話引擎會將它們儲存在資料庫中。由於在清空資料庫之後不會發出
post_migrate
,因此TransactionTestCase
之後的資料庫狀態與TestCase
之後的狀態不同:它缺少了post_migrate
的監聽器所建立的資料列。考慮到 測試執行的順序,這不是問題,前提是給定測試套件中的所有TransactionTestCase
都宣告available_apps
,或它們都不宣告。available_apps
在 Django 自己的測試套件中是強制性的。
- TransactionTestCase.reset_sequences¶
在
TransactionTestCase
上設定reset_sequences = True
將確保序列在測試執行之前總是會重設。class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase): reset_sequences = True def test_animal_pk(self): lion = Animal.objects.create(name="lion", sound="roar") # lion.pk is guaranteed to always be 1 self.assertEqual(lion.pk, 1)
除非您明確測試主鍵序列號,否則建議您不要在測試中硬式編碼主鍵值。
使用
reset_sequences = True
將會減慢測試速度,因為重設主鍵是相對昂貴的資料庫操作。
強制循序執行測試類別¶
如果您有無法並行執行的測試類別(例如,因為它們共享一個共同的資源),您可以使用 django.test.testcases.SerializeMixin
來循序執行它們。這個 mixin 使用檔案系統 lockfile
。
例如,您可以使用 __file__
來確定在繼承自 SerializeMixin
的同一個檔案中的所有測試類別都會循序執行。
import os
from django.test import TestCase
from django.test.testcases import SerializeMixin
class ImageTestCaseMixin(SerializeMixin):
lockfile = __file__
def setUp(self):
self.filename = os.path.join(temp_storage_dir, "my_file.png")
self.file = create_file(self.filename)
class RemoveImageTests(ImageTestCaseMixin, TestCase):
def test_remove_image(self):
os.remove(self.filename)
self.assertFalse(os.path.exists(self.filename))
class ResizeImageTests(ImageTestCaseMixin, TestCase):
def test_resize_image(self):
resize_image(self.file, (48, 48))
self.assertEqual(get_image_size(self.file), (48, 48))
使用 Django 測試執行器測試可重複使用的應用程式¶
如果您正在編寫一個可重複使用的應用程式,您可能想要使用 Django 測試執行器來執行您自己的測試套件,從而受益於 Django 測試基礎架構。
常見的做法是在應用程式程式碼旁邊建立一個 tests 目錄,其結構如下:
runtests.py
polls/
__init__.py
models.py
...
tests/
__init__.py
models.py
test_settings.py
tests.py
讓我們看看其中幾個檔案的內容:
runtests.py
¶#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings"
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
這是您調用以執行測試套件的腳本。它會設定 Django 環境、建立測試資料庫並執行測試。
為了清楚起見,此範例僅包含使用 Django 測試執行器所需的最低限度。您可能想要新增命令列選項,以控制詳細程度、傳遞要執行的特定測試標籤等等。
tests/test_settings.py
¶SECRET_KEY = "fake-key"
INSTALLED_APPS = [
"tests",
]
此檔案包含執行應用程式測試所需的 Django 設定。
同樣地,這是一個最小的範例;您的測試可能需要額外的設定才能執行。
由於在執行測試時,tests 套件已包含在 INSTALLED_APPS
中,您可以在其 models.py
檔案中定義僅限測試的模型。
使用不同的測試框架¶
顯然,unittest
並非唯一的 Python 測試框架。雖然 Django 沒有為替代框架提供明確的支援,但它確實提供了一種方法,可以調用為替代框架建構的測試,如同它們是正常的 Django 測試一樣。
當您執行 ./manage.py test
時,Django 會查看 TEST_RUNNER
設定來決定如何執行。預設情況下,TEST_RUNNER
指向 'django.test.runner.DiscoverRunner'
。這個類別定義了 Django 預設的測試行為。此行為包含:
執行全域的測試前設定。
在目前目錄下的任何檔案中,尋找名稱符合
test*.py
模式的測試。建立測試資料庫。
執行
migrate
將模型和初始資料安裝到測試資料庫中。執行系統檢查。
執行找到的測試。
摧毀測試資料庫。
執行全域的測試後清理。
如果您定義了自己的測試執行器類別,並將 TEST_RUNNER
指向該類別,Django 將在您執行 ./manage.py test
時執行您的測試執行器。這樣一來,就可以使用任何可以從 Python 程式碼執行的測試框架,或者修改 Django 測試執行流程以滿足您可能有的任何測試需求。
定義測試執行器¶
測試執行器是一個定義了 run_tests()
方法的類別。Django 內建了一個 DiscoverRunner
類別,定義了 Django 預設的測試行為。這個類別定義了 run_tests()
的進入點,以及一組由 run_tests()
用來設定、執行和清理測試套件的其他方法。
- class DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, shuffle=False, logger=None, durations=None, **kwargs)[原始碼]¶
DiscoverRunner
將在任何符合pattern
的檔案中搜尋測試。top_level
可用來指定包含您的頂層 Python 模組的目錄。通常 Django 可以自動判斷出來,所以沒有必要指定這個選項。如果指定了,它通常應該是包含您的manage.py
檔案的目錄。verbosity
決定了將列印到控制台的通知和除錯資訊量;0
表示不輸出,1
表示正常輸出,而2
表示詳細輸出。如果
interactive
為True
,則測試套件在執行時有權要求使用者給予指示。此行為的一個範例是要求是否允許刪除現有的測試資料庫。如果interactive
為False
,則測試套件必須能夠在沒有任何人工干預的情況下執行。如果
failfast
為True
,則在偵測到第一個測試失敗後,測試套件將停止執行。如果
keepdb
為True
,則測試套件將使用現有的資料庫,或在必要時建立一個。如果為False
,則會建立一個新的資料庫,並提示使用者刪除現有的資料庫(如果存在)。如果
reverse
為True
,則測試案例將以相反的順序執行。這對於除錯沒有正確隔離且具有副作用的測試可能很有用。當使用此選項時,會保留依測試類別分組。此選項可以與--shuffle
一起使用,以反轉特定隨機種子的順序。debug_mode
指定在執行測試之前,DEBUG
設定應設定為的值。parallel
指定程序數量。如果parallel
大於1
,則測試套件將在parallel
個程序中執行。如果測試案例類別的數量少於設定的程序,Django 將相應地減少程序數量。每個程序都有自己的資料庫。此選項需要第三方tblib
套件才能正確顯示回溯。tags
可用來指定一組標記以篩選測試。可以與exclude_tags
結合使用。exclude_tags
可用來指定一組標記以排除測試。可以與tags
結合使用。如果
debug_sql
為True
,則失敗的測試案例將輸出記錄到 django.db.backends logger 的 SQL 查詢,以及回溯。如果verbosity
為2
,則會輸出所有測試中的查詢。test_name_patterns
可用來指定一組模式,以依名稱篩選測試方法和類別。如果
pdb
為True
,則會在每個測試錯誤或失敗時產生偵錯工具(pdb
或ipdb
)。如果
buffer
為True
,則會捨棄通過測試的輸出。如果
enable_faulthandler
為True
,則會啟用faulthandler
。如果
timing
為True
,則會顯示測試時間,包括資料庫設定和總執行時間。如果
shuffle
是一個整數,則會在執行之前以隨機順序對測試案例進行洗牌,並使用該整數作為隨機種子。如果shuffle
為None
,則會隨機產生種子。在這兩種情況下,種子都會被記錄下來,並在執行測試之前設定為self.shuffle_seed
。此選項可用於協助偵測未正確隔離的測試。依測試類別分組在使用此選項時會被保留。logger
可用來傳遞 Python Logger 物件。如果提供,記錄器將會用來記錄訊息,而不是列印到控制台。記錄器物件將會遵循其記錄層級,而不是verbosity
。durations
將顯示 N 個最慢的測試案例列表。將此選項設定為0
將會顯示所有測試的持續時間。需要 Python 3.12+。Django 可能會不時透過新增參數來擴展測試執行器的功能。
**kwargs
宣告允許這種擴展。如果您繼承DiscoverRunner
或撰寫自己的測試執行器,請確保它接受**kwargs
。您的測試執行器也可以定義額外的命令列選項。建立或覆寫
add_arguments(cls, parser)
類別方法,並在方法內呼叫parser.add_argument()
來新增自訂參數,以便test
命令可以使用這些參數。Django 5.0 新增。新增了
durations
參數。
屬性¶
- DiscoverRunner.test_suite¶
用於建構測試套件的類別。預設情況下,它設定為
unittest.TestSuite
。如果您希望實作不同的邏輯來收集測試,可以覆寫此設定。
- DiscoverRunner.test_runner¶
這是用於執行個別測試和格式化結果的底層測試執行器的類別。預設情況下,它設定為
unittest.TextTestRunner
。儘管命名慣例非常相似,但這與DiscoverRunner
的類別類型不同,後者涵蓋了更廣泛的職責。您可以覆寫此屬性以修改測試的執行和報告方式。
- DiscoverRunner.test_loader¶
這是載入測試的類別,無論是從 TestCase、模組或其他方式,並將它們捆綁到測試套件中以供執行器執行。預設情況下,它設定為
unittest.defaultTestLoader
。如果您的測試將以不尋常的方式載入,您可以覆寫此屬性。
方法¶
- DiscoverRunner.run_tests(test_labels, **kwargs)[來源]¶
執行測試套件。
test_labels
允許您指定要執行的測試,並支援多種格式(請參閱DiscoverRunner.build_suite()
以取得支援的格式清單)。此方法應傳回失敗的測試數量。
- classmethod DiscoverRunner.add_arguments(parser)[來源]¶
覆寫此類別方法,以新增
test
管理命令接受的自訂參數。有關將參數新增到剖析器的詳細資訊,請參閱argparse.ArgumentParser.add_argument()
。
- DiscoverRunner.setup_test_environment(**kwargs)[來源]¶
透過呼叫
setup_test_environment()
並將DEBUG
設定為self.debug_mode
(預設為False
)來設定測試環境。
- DiscoverRunner.build_suite(test_labels=None, **kwargs)[來源]¶
建構與提供的測試標籤相符的測試套件。
test_labels
是描述要執行之測試的字串清單。測試標籤可以採用四種形式之一path.to.test_module.TestCase.test_method
– 執行測試案例類別中的單一測試方法。path.to.test_module.TestCase
– 執行測試案例中的所有測試方法。path.to.module
– 在指定的 Python 套件或模組中搜尋並執行所有測試。path/to/directory
– 在指定的目錄下搜尋並執行所有測試。
如果
test_labels
的值為None
,則測試執行器將在目前目錄下搜尋名稱與其pattern
相符的所有檔案中的測試(請參閱上方)。傳回準備好執行的
TestSuite
實例。
- DiscoverRunner.setup_databases(**kwargs)[來源]¶
透過呼叫
setup_databases()
來建立測試資料庫。
- DiscoverRunner.teardown_databases(old_config, **kwargs)[來源]¶
透過呼叫
teardown_databases()
來銷毀測試資料庫,並還原測試前條件。
測試工具¶
django.test.utils
¶
為了協助建立您自己的測試執行器,Django 在 django.test.utils
模組中提供了許多實用方法。
- setup_test_environment(debug=None)[原始碼]¶
執行全域測試前設定,例如安裝樣板渲染系統的儀器並設定虛擬電子郵件收件匣。
如果
debug
不是None
,則DEBUG
設定會更新為其值。
- setup_databases(verbosity, interactive, *, time_keeper=None, keepdb=False, debug_sql=False, parallel=0, aliases=None, serialized_aliases=None, **kwargs)[原始碼]¶
建立測試資料庫。
傳回一個資料結構,提供足夠的詳細資訊來還原已做的變更。此資料將在測試結束時提供給
teardown_databases()
函式。aliases
引數決定應該為哪些DATABASES
別名設定測試資料庫。如果未提供,則預設為所有DATABASES
別名。serialized_aliases
引數決定應該將哪些aliases
測試資料庫的狀態序列化,以允許使用 serialized_rollback 功能。如果未提供,則預設為aliases
。
- teardown_databases(old_config, parallel=0, keepdb=False)[原始碼]¶
摧毀測試資料庫,恢復測試前的狀態。
old_config
是一個資料結構,定義需要反轉的資料庫設定中的變更。它是setup_databases()
方法的傳回值。
django.db.connection.creation
¶
資料庫後端的建立模組也提供了一些在測試期間可能有用的實用工具。
- create_test_db(verbosity=1, autoclobber=False, serialize=True, keepdb=False)¶
建立新的測試資料庫並對其執行
migrate
。verbosity
的行為與run_tests()
中的行為相同。autoclobber
描述如果發現與測試資料庫同名的資料庫時將發生的行為如果
autoclobber
為False
,則會要求使用者批准摧毀現有的資料庫。如果使用者不同意,則會呼叫sys.exit
。如果
autoclobber
為True
,則會摧毀資料庫而不諮詢使用者。
serialize
決定 Django 是否在執行測試之前將資料庫序列化為記憶體中的 JSON 字串 (如果您沒有交易,則用於還原測試之間的資料庫狀態)。如果您的測試類別沒有任何 serialized_rollback=True,則可以將此設定為False
以加快建立時間。keepdb
決定測試執行是否應使用現有的資料庫,或建立新的資料庫。如果True
,將會使用現有的資料庫,如果不存在則會建立。如果False
,則會建立新的資料庫,並提示使用者移除現有的資料庫 (如果存在)。傳回它建立的測試資料庫名稱。
- destroy_test_db(old_database_name, verbosity=1, keepdb=False)¶
摧毀名稱為
DATABASES
中NAME
值的資料庫,並將NAME
設定為old_database_name
的值。verbosity
引數的行為與DiscoverRunner
相同。如果
keepdb
引數為True
,則會關閉與資料庫的連線,但不會摧毀資料庫。
與 coverage.py
的整合¶
程式碼涵蓋率描述了有多少原始碼經過測試。它會顯示您的程式碼的哪些部分正在透過測試執行,哪些部分沒有。它是測試應用程式的重要部分,因此強烈建議檢查您測試的涵蓋率。
Django 可以輕鬆地與 coverage.py 整合,coverage.py 是一個用於測量 Python 程式碼涵蓋率的工具。首先,安裝 coverage。接下來,從包含 manage.py
的專案資料夾中執行下列程式碼
coverage run --source='.' manage.py test myapp
這會執行您的測試並收集專案中已執行檔案的涵蓋率資料。您可以透過輸入以下指令來查看此資料的報告
coverage report
請注意,在執行測試時執行了一些 Django 程式碼,但由於傳遞給上一個指令的 source
旗標,因此未在此處列出。
如需更多選項,例如詳細說明遺漏程式碼行的註釋 HTML 清單,請參閱 coverage.py 文件。