Django Testing 單元測試與整合測試入門 | Django 教學

2026/06/25 2026/05/27
Django Testing 單元測試與整合測試入門 | Django 教學

Django Testing 是 Django 內建的完整測試基礎設施,基於 Python 標準函式庫的 unittest 模組建構,提供 TestCaseSimpleTestCaseTransactionTestCase 等測試類別,搭配內建的 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 會:

  1. 建立一個獨立的測試資料庫(在原資料庫名稱前加上 test_ 前綴)
  2. 執行所有的 Migration(遷移)來建立資料表結構
  3. 依序執行所有找到的測試方法
  4. 測試完成後銷毀測試資料庫

這表示你的測試永遠不會影響到正式的資料庫,完全隔離且安全。

測試金字塔

軟體測試一般遵循「測試金字塔(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 Clientdjango.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.pytests/ 目錄中的測試。隨著測試數量增加,建議將 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 測試框架的基礎知識與實作方式:

  1. 測試框架概覽:基於 Python unittest 建構,manage.py test 會自動建立與銷毀測試資料庫
  2. TestCase 類別選擇SimpleTestCase(無資料庫)、TestCase(最常用,自動 rollback)、TransactionTestCase(真實 Transaction)
  3. Model 測試:驗證欄位規則、__str__ 方法、預設排序等 Model 行為
  4. View 測試:使用 Test Client 模擬 GET/POST 請求,檢查狀態碼、Template、Context 與重新導向
  5. Form 測試:驗證 is_valid()、錯誤訊息、表單儲存邏輯
  6. 測試資料準備setUp / setUpTestData / Fixtures 三種方式各有適用場景
  7. assert 方法:基本的 assertEqualassertTrue 加上 Django 專屬的 assertContainsassertRedirectsassertFormError
  8. 執行技巧--parallel--keepdb--failfast 等選項提升開發效率

良好的測試習慣是專業 Django 開發的基石。在下一篇中,我們將進入進階測試領域,學習 Mock、Factory Boy、Coverage 覆蓋率報告,以及如何將測試整合到 CI/CD 流程中。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。