Django Testing 單元測試與整合測試入門 | Django 教學
Django Testing 是 Django 內建的完整測試基礎設施,基於 Python 標準函式庫的 unittest 模組建構,提供 TestCase、SimpleTestCase、TransactionTestCase 等測試類別,搭配內建的 Test Client(測試客戶端)可以模擬 HTTP 請求,無需啟動伺服器就能測試 View、Model、Form 等所有元件。本文將從 Django 測試框架的運作機制開始,帶你掌握 Model 測試、View 測試、Form 測試的基礎寫法,以及各種 assert 斷言方法與測試執行技巧。
為什麼要寫測試?
在專案規模逐漸增長的過程中,每次新增功能或修改程式碼都可能不小心影響到已有的功能。手動測試耗時且容易遺漏,自動化測試(Automated Testing)則能幫你:
- 防止回歸錯誤(Regression):確保修改程式碼後不會破壞現有功能
- 作為活文件(Living Documentation):測試案例清楚描述了每個功能的預期行為
- 支撐重構:有完善的測試覆蓋,你才能放心地重構程式碼
- 加速開發:看似多花時間寫測試,實際上大幅減少了除錯的時間
Django 的測試框架讓你能夠以最小的設定成本開始撰寫測試,而不需要額外安裝任何套件。
Django 測試框架概覽
Django 的測試框架建構於 Python 標準函式庫的 unittest 模組之上。當你執行 python manage.py test 時,Django 會:
- 建立一個獨立的測試資料庫(在原資料庫名稱前加上
test_前綴) - 執行所有的 Migration(遷移)來建立資料表結構
- 依序執行所有找到的測試方法
- 測試完成後銷毀測試資料庫
這表示你的測試永遠不會影響到正式的資料庫,完全隔離且安全。
測試金字塔
軟體測試一般遵循「測試金字塔(Testing Pyramid)」的原則,從底層到頂層分為三類:
/\
/ \
/ E2E \ 最少(Selenium、Playwright)
/ Tests \
/----------\
/ Integration \ 中量(View + Model 互動)
/ Tests \
/------------------\
/ Unit Tests \ 最多(Model 方法、工具函式)
/______________________\
- Unit Tests(單元測試):測試最小單元,如 Model 的方法、工具函式、Serializer。執行速度最快,數量最多
- Integration Tests(整合測試):測試元件間的互動,如 View 與 Model 的配合、表單驗證流程。允許資料庫操作
- E2E Tests(端到端測試):模擬真實使用者操作(需要瀏覽器驅動),速度最慢,數量最少
TestCase 類別選擇
Django 提供了多種測試基礎類別,根據你的測試需求選擇最適合的:
| 類別 | 資料庫操作 | Transaction 行為 | 適用場景 |
|---|---|---|---|
SimpleTestCase | 不允許 | 無 | 純函式、URL 配置、Template 渲染 |
TestCase | 允許 | 每個 test 在 Transaction 中,結束後 rollback | 最常用:Model CRUD、View 測試 |
TransactionTestCase | 允許 | 真實 Transaction(不自動 rollback) | on_commit() 鉤子、Transaction 相關邏輯 |
LiveServerTestCase | 允許 | 同 TransactionTestCase | 啟動真實 HTTP 伺服器,配合 Selenium |
最常用的是 TestCase,它用資料庫的 SAVEPOINT 包裹每個測試方法,測試結束後自動 rollback,既能存取資料庫又不會殘留測試資料,速度也最快。
TransactionTestCase 使用真實的 Transaction commit/rollback,速度較慢,只在你需要測試 transaction.on_commit() 或手動管理 Transaction 時才使用。
Model 測試
Model 測試是最基礎也最重要的測試類型,確保你的資料模型(Model)行為正確。
# articles/tests/test_models.py
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from articles.models import Article
class ArticleModelTest(TestCase):
"""測試 Article Model 的欄位驗證與方法。"""
@classmethod
def setUpTestData(cls):
"""在整個 TestCase 中只執行一次,建立共用的唯讀測試資料。
比 setUp() 更快,因為不需要每個測試方法都重新建立。
"""
cls.user = User.objects.create_user(
username='testauthor',
email='author@example.com',
password='testpass123',
)
cls.article = Article.objects.create(
title='測試文章',
content='這是測試文章的內容',
author=cls.user,
)
def test_article_str_method(self):
"""測試 __str__ 方法回傳文章標題。"""
self.assertEqual(str(self.article), '測試文章')
def test_article_default_ordering(self):
"""測試 Model 的預設排序。"""
Article.objects.create(
title='第二篇文章',
content='內容',
author=self.user,
)
articles = Article.objects.all()
# 假設 Meta.ordering = ['-created_at']
self.assertEqual(articles[0].title, '第二篇文章')
def test_title_max_length(self):
"""測試欄位的最大長度限制。"""
self.article.title = 'x' * 201 # 假設 max_length=200
with self.assertRaises(ValidationError):
self.article.full_clean() # 觸發 Model 層級的驗證
def test_article_creation_sets_timestamps(self):
"""測試建立文章時自動設定時間戳記。"""
self.assertIsNotNone(self.article.created_at)
self.assertIsNotNone(self.article.updated_at)
def test_article_absolute_url(self):
"""測試 get_absolute_url 方法。"""
expected_url = f'/articles/{self.article.pk}/'
self.assertEqual(self.article.get_absolute_url(), expected_url)
setUp vs setUpTestData
setUp(self):實例方法,在 每個 測試方法前都會執行一次。適合建立每次測試都需要獨立修改的資料setUpTestData(cls):類別方法(@classmethod),在整個 TestCase 類別中 只執行一次。建立的資料在所有測試方法間共享,但不應該在測試中修改它。執行效率更高
class ExampleTest(TestCase):
@classmethod
def setUpTestData(cls):
"""只執行一次,建立唯讀的共用資料。"""
cls.user = User.objects.create_user('shared_user', password='pass')
def setUp(self):
"""每個測試方法前都執行,建立需要獨立修改的資料。"""
self.article = Article.objects.create(
title='每次測試的獨立文章',
content='...',
author=self.user,
)
def tearDown(self):
"""每個測試方法後都執行(通常不需要,TestCase 會自動 rollback)。"""
pass
View 測試
Django 內建的 Test Client(django.test.Client)可以模擬 HTTP 請求,不需要啟動真實的伺服器。它直接呼叫 Django 的 URL resolver 和 View,回傳 Response 物件供你檢驗。
# articles/tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from articles.models import Article
class ArticleListViewTest(TestCase):
"""測試文章列表 View。"""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('author', password='pass')
self.article = Article.objects.create(
title='測試文章',
content='內容',
author=self.user,
)
def test_list_view_returns_200(self):
"""測試列表頁回傳 HTTP 200。"""
url = reverse('articles:list') # 使用 URL name 避免硬編碼
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_list_view_uses_correct_template(self):
"""測試使用正確的 Template。"""
url = reverse('articles:list')
response = self.client.get(url)
self.assertTemplateUsed(response, 'articles/list.html')
def test_list_view_contains_article_title(self):
"""測試列表頁包含文章標題。"""
url = reverse('articles:list')
response = self.client.get(url)
self.assertContains(response, '測試文章')
def test_list_view_context_has_articles(self):
"""測試 Context 中包含文章資料。"""
url = reverse('articles:list')
response = self.client.get(url)
self.assertIn('articles', response.context)
self.assertEqual(len(response.context['articles']), 1)
class ArticleCreateViewTest(TestCase):
"""測試文章建立 View。"""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('author', password='pass')
self.create_url = reverse('articles:create')
def test_create_requires_login(self):
"""測試未登入時被重新導向到登入頁面。"""
response = self.client.post(
self.create_url,
{'title': '新文章', 'content': '內容'},
)
self.assertRedirects(
response,
f'/accounts/login/?next={self.create_url}',
)
def test_create_article_success(self):
"""測試登入後可成功建立文章。"""
self.client.login(username='author', password='pass')
response = self.client.post(self.create_url, {
'title': '新文章',
'content': '這是透過表單建立的文章',
})
self.assertEqual(Article.objects.count(), 1)
self.assertRedirects(response, reverse('articles:list'))
def test_create_article_invalid_data(self):
"""測試提交無效資料時顯示錯誤。"""
self.client.login(username='author', password='pass')
response = self.client.post(self.create_url, {
'title': '', # 標題為空,應該驗證失敗
'content': '內容',
})
self.assertEqual(response.status_code, 200) # 表單重新顯示
self.assertEqual(Article.objects.count(), 0) # 文章未建立
Test Client 常用方法
# GET 請求
response = self.client.get('/articles/')
response = self.client.get('/articles/', {'page': 2}) # 帶查詢參數
# POST 請求
response = self.client.post('/articles/create/', {'title': 'Test'})
# 帶 JSON 資料的 POST
response = self.client.post(
'/api/articles/',
data={'title': 'Test'},
content_type='application/json',
)
# 登入與登出
self.client.login(username='user', password='pass')
self.client.logout()
# 強制登入(跳過密碼驗證,更簡潔)
self.client.force_login(self.user)
Form 測試
表單(Form)是 Django 中使用者輸入的第一道關卡,測試 Form 可以確保驗證邏輯正確運作。
# articles/tests/test_forms.py
from django.test import TestCase
from articles.forms import ArticleForm
class ArticleFormTest(TestCase):
"""測試文章表單的驗證邏輯。"""
def test_valid_form(self):
"""測試有效的表單資料。"""
form = ArticleForm(data={
'title': '有效的文章標題',
'content': '這是文章的內容',
})
self.assertTrue(form.is_valid())
def test_empty_title_is_invalid(self):
"""測試空白標題應驗證失敗。"""
form = ArticleForm(data={
'title': '',
'content': '內容',
})
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_title_too_long_is_invalid(self):
"""測試超過最大長度的標題。"""
form = ArticleForm(data={
'title': 'x' * 201,
'content': '內容',
})
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_form_error_messages(self):
"""測試錯誤訊息的內容。"""
form = ArticleForm(data={'title': '', 'content': ''})
self.assertFalse(form.is_valid())
# 檢查錯誤訊息是否包含預期的文字
self.assertIn('這個欄位是必須的', str(form.errors['title']))
def test_form_save(self):
"""測試表單儲存功能。"""
user = User.objects.create_user('author', password='pass')
form = ArticleForm(data={
'title': '表單建立的文章',
'content': '內容',
})
self.assertTrue(form.is_valid())
article = form.save(commit=False)
article.author = user
article.save()
self.assertEqual(Article.objects.count(), 1)
你也可以在 View 測試中使用 assertFormError 來驗證 View 回傳的表單錯誤:
def test_create_view_shows_form_error(self):
"""測試 View 中的表單錯誤訊息。"""
self.client.force_login(self.user)
response = self.client.post(reverse('articles:create'), {
'title': '',
'content': '',
})
self.assertFormError(response.context['form'], 'title', '這個欄位是必須的。')
測試資料準備:Fixtures
除了在 setUp 中手動建立測試資料外,Django 還支援使用 Fixtures(固定資料檔)來載入預先準備好的測試資料。Fixtures 是 JSON 或 YAML 格式的檔案,包含要載入資料庫的資料:
# 匯出目前資料庫的資料為 Fixture
# python manage.py dumpdata articles --indent 2 > articles/fixtures/test_articles.json
[
{
"model": "articles.article",
"pk": 1,
"fields": {
"title": "Fixture 文章",
"content": "透過 Fixture 載入的測試資料",
"author": 1,
"created_at": "2026-06-25T10:00:00Z"
}
}
]
在測試中使用 Fixtures:
class ArticleWithFixtureTest(TestCase):
fixtures = ['test_users.json', 'test_articles.json']
def test_fixture_data_loaded(self):
"""測試 Fixture 資料已成功載入。"""
self.assertEqual(Article.objects.count(), 1)
self.assertEqual(Article.objects.first().title, 'Fixture 文章')
Fixtures 適合靜態的參考資料(如分類表、城市清單),但對於動態的測試資料,手動在 setUp 中建立或使用 Factory Boy(進階篇會介紹)是更好的選擇。
常用的 assert 斷言方法
Django 的 TestCase 繼承了 unittest.TestCase 的所有斷言方法,並額外提供了許多 Web 開發相關的斷言:
基本斷言(繼承自 unittest)
# 相等性檢查
self.assertEqual(a, b) # a == b
self.assertNotEqual(a, b) # a != b
# 真值檢查
self.assertTrue(expr) # expr is True
self.assertFalse(expr) # expr is False
# None 檢查
self.assertIsNone(obj) # obj is None
self.assertIsNotNone(obj) # obj is not None
# 型別檢查
self.assertIsInstance(obj, cls) # isinstance(obj, cls)
# 包含關係
self.assertIn(item, container) # item in container
self.assertNotIn(item, container) # item not in container
# 例外檢查
with self.assertRaises(ValidationError):
some_function()
# 數值比較
self.assertGreater(a, b) # a > b
self.assertLessEqual(a, b) # a <= b
Django 專屬斷言
# 回應狀態碼與內容
self.assertContains(response, '預期文字') # 回應包含指定文字,且狀態碼為 200
self.assertContains(response, '文字', count=2) # 指定出現次數
self.assertNotContains(response, '不該出現的文字')
# 重新導向
self.assertRedirects(response, '/expected/url/')
self.assertRedirects(
response, '/expected/url/',
status_code=302, # 重新導向的狀態碼
target_status_code=200, # 目標頁面的狀態碼
)
# Template 相關
self.assertTemplateUsed(response, 'articles/list.html')
self.assertTemplateNotUsed(response, 'articles/other.html')
# Form 錯誤
self.assertFormError(response.context['form'], 'title', '錯誤訊息')
# 資料庫查詢數量(效能測試)
with self.assertNumQueries(3):
Article.objects.select_related('author').all()
執行測試
Django 使用 manage.py test 指令來發現並執行測試:
# 執行專案中所有測試
python manage.py test
# 執行特定 App 的測試
python manage.py test articles
# 執行特定測試模組
python manage.py test articles.tests.test_models
# 執行特定 TestCase 類別
python manage.py test articles.tests.test_models.ArticleModelTest
# 執行特定測試方法
python manage.py test articles.tests.test_models.ArticleModelTest.test_article_str_method
常用的命令列選項
# 詳細輸出模式(顯示每個測試方法的名稱與結果)
python manage.py test -v 2
# 平行執行測試(利用多核心加速)
python manage.py test --parallel
# 保留測試資料庫(跳過建立/銷毀步驟,加速重複執行)
python manage.py test --keepdb
# 遇到第一個失敗就停止
python manage.py test --failfast
# 顯示 SQL 查詢(除錯用)
python manage.py test --debug-sql
# 反轉測試執行順序(偵測測試之間的依賴問題)
python manage.py test --reverse
測試檔案的組織方式
Django 預設在每個 App 下尋找 tests.py 或 tests/ 目錄中的測試。隨著測試數量增加,建議將 tests.py 拆分為 tests/ 目錄結構:
articles/
tests/
__init__.py
test_models.py
test_views.py
test_forms.py
test_urls.py
確保 tests/ 目錄下有 __init__.py,Django 才能正確發現測試模組。
測試 URL 配置
除了 Model、View、Form 之外,URL 配置也值得測試,確保 URL 解析正確:
# articles/tests/test_urls.py
from django.test import SimpleTestCase
from django.urls import reverse, resolve
from articles.views import article_list, article_detail
class ArticleURLTest(SimpleTestCase):
"""測試 URL 配置是否正確對應到 View。"""
def test_list_url_resolves(self):
"""測試列表 URL 對應正確的 View。"""
url = reverse('articles:list')
self.assertEqual(resolve(url).func, article_list)
def test_detail_url_resolves(self):
"""測試詳情 URL 對應正確的 View。"""
url = reverse('articles:detail', kwargs={'pk': 1})
self.assertEqual(resolve(url).func, article_detail)
def test_list_url_path(self):
"""測試 URL name 對應正確的路徑。"""
url = reverse('articles:list')
self.assertEqual(url, '/articles/')
由於 URL 測試不需要存取資料庫,使用 SimpleTestCase 即可,執行速度更快。
加速測試的實用技巧
當專案的測試數量增加後,測試執行時間可能會變得很長。以下是幾個常見的加速技巧:
使用較快的密碼雜湊演算法
Django 預設使用 PBKDF2 密碼雜湊,安全但速度慢。測試環境中可以切換為較快的演算法:
# settings/test.py
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
使用 setUpTestData 取代 setUp
如前所述,setUpTestData 只在整個 TestCase 類別中執行一次,而 setUp 每個測試方法都會執行。對於唯讀的共用資料,優先使用 setUpTestData。
善用 –keepdb 與 –parallel
# 保留測試資料庫,下次執行時跳過建立/銷毀
python manage.py test --keepdb
# 平行執行,利用多核心 CPU
python manage.py test --parallel
# 兩者結合
python manage.py test --keepdb --parallel
建立專用的測試設定檔
# settings/test.py — 繼承基礎設定並覆寫測試相關選項
from .base import *
# 使用較快的密碼雜湊
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
# 關閉除錯模式
DEBUG = False
# 停用 Celery 非同步(同步執行 Task)
CELERY_TASK_ALWAYS_EAGER = True
# 使用 in-memory Channel Layer
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
執行測試時指定設定檔:
python manage.py test --settings=myproject.settings.test
總結
本文介紹了 Django 測試框架的基礎知識與實作方式:
- 測試框架概覽:基於 Python
unittest建構,manage.py test會自動建立與銷毀測試資料庫 - TestCase 類別選擇:
SimpleTestCase(無資料庫)、TestCase(最常用,自動 rollback)、TransactionTestCase(真實 Transaction) - Model 測試:驗證欄位規則、
__str__方法、預設排序等 Model 行為 - View 測試:使用 Test Client 模擬 GET/POST 請求,檢查狀態碼、Template、Context 與重新導向
- Form 測試:驗證
is_valid()、錯誤訊息、表單儲存邏輯 - 測試資料準備:
setUp/setUpTestData/ Fixtures 三種方式各有適用場景 - assert 方法:基本的
assertEqual、assertTrue加上 Django 專屬的assertContains、assertRedirects、assertFormError等 - 執行技巧:
--parallel、--keepdb、--failfast等選項提升開發效率
良好的測試習慣是專業 Django 開發的基石。在下一篇中,我們將進入進階測試領域,學習 Mock、Factory Boy、Coverage 覆蓋率報告,以及如何將測試整合到 CI/CD 流程中。