CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Django 实战入门/课时/测试与部署
RawBlame
课时目录12 节

Django 实战入门

  1. 01Django 概览
  2. 02项目结构
  3. 03URL 路由
  4. 04视图层
  5. 05模型与 ORM
  6. 06查询与优化
  7. 07模板 DTL
  8. 08Admin 与认证
  9. 09表单与安全
  10. 10DRF 与异步任务
  11. 11测试与部署
  12. 12综合练习

测试与部署

pytest、日志、WhiteNoise 与 checklist。

30 分钟

测试与部署

这一课你将能做到

  1. 用 Django TestCase + pytest 写单元测试、集成测试和 API 测试
  2. 配置日志系统、debug toolbar、性能分析,建立开发调试工作流
  3. 用 WhiteNoise 处理生产环境静态文件,并跑通完整的生产部署 checklist

为什么要学

写代码不测试,上线就哭。Django 自带测试框架,从模型测试到浏览器模拟全有覆盖。但「能跑」和「上线」之间隔着十万八千里:DEBUG 关了吗?密钥藏好了吗?HTTPS 强制的吗?WhiteNoise 配了吗?这些检查项少一个,生产环境就可能泄露敏感信息或被攻击。

和 Spring Boot 对比:Spring 有 @SpringBootTest + TestRestTemplate,Django 有 TestCase + Client。测试思想完全一致:模拟请求、断言响应、隔离数据库。但 Django 测试框架内置了数据库事务回滚,每个测试后自动清理,这点比 Spring 的 @Transactional 在测试层更顺手。

先建立图像

开发环境                          生产环境
─────────                        ─────────
DEBUG = True                     DEBUG = False
SQLite                           PostgreSQL
runserver                        Gunicorn + Nginx
STATICFILES_DIRS                 STATIC_ROOT + WhiteNoise
密钥写 settings.py               密钥读环境变量
邮件打印到控制台                  发真实 SMTP

跟着做

步骤 1:Django 测试基础

# blog/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Article, Category


class ArticleModelTests(TestCase):
    def setUp(self):
        """每个测试方法执行前运行"""
        self.user = User.objects.create_user(username="tester", password="123456")
        self.cat = Category.objects.create(name="Python", slug="python")
        self.article = Article.objects.create(
            title="测试文章",
            slug="test-article",
            content="正文内容",
            author=self.user,
            category=self.cat,
            status="published"
        )

    def test_article_creation(self):
        """测试文章创建"""
        self.assertEqual(self.article.title, "测试文章")
        self.assertEqual(str(self.article), "测试文章")
        self.assertTrue(self.article.is_hot())  # 假设 views 默认 0,is_hot 返回 False

    def test_article_slug_unique(self):
        """测试 slug 唯一性"""
        with self.assertRaises(Exception):
            Article.objects.create(
                title="重复",
                slug="test-article",  # 重复
                content="...",
                author=self.user,
            )


class ArticleViewTests(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(username="tester", password="123456")
        self.cat = Category.objects.create(name="Python", slug="python")
        for i in range(15):
            Article.objects.create(
                title=f"文章{i}",
                slug=f"article-{i}",
                content="...",
                author=self.user,
                category=self.cat,
                status="published"
            )

    def test_article_list(self):
        """测试文章列表页"""
        response = self.client.get(reverse("blog:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "文章列表")
        self.assertTemplateUsed(response, "blog/article_list.html")
        self.assertEqual(len(response.context["articles"]), 10)  # paginate_by=10

    def test_article_detail(self):
        """测试文章详情页"""
        response = self.client.get(reverse("blog:detail", args=["article-0"]))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "文章0")

    def test_login_required_redirect(self):
        """测试未登录重定向"""
        response = self.client.get(reverse("blog:dashboard"))
        self.assertEqual(response.status_code, 302)

步骤 2:用 pytest + factory-boy

pip install pytest pytest-django factory-boy
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = mysite.settings
python_files = tests.py test_*.py *_tests.py
# blog/factories.py
import factory
from django.contrib.auth.models import User
from .models import Article, Category


class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    username = factory.Sequence(lambda n: f"user{n}")
    email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")


class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category

    name = factory.Sequence(lambda n: f"分类{n}")
    slug = factory.Sequence(lambda n: f"category-{n}")


class ArticleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Article

    title = factory.Sequence(lambda n: f"文章{n}")
    slug = factory.Sequence(lambda n: f"article-{n}")
    content = "正文内容..."
    author = factory.SubFactory(UserFactory)
    category = factory.SubFactory(CategoryFactory)
    status = "published"
# blog/test_pytest.py
import pytest
from django.urls import reverse
from .factories import ArticleFactory, UserFactory


@pytest.fixture
def user():
    return UserFactory()


@pytest.fixture
def articles(user):
    return ArticleFactory.create_batch(15, author=user)


@pytest.mark.django_db
def test_article_list(client, articles):
    response = client.get(reverse("blog:index"))
    assert response.status_code == 200
    assert b"文章列表" in response.content


@pytest.mark.django_db
def test_article_create_api(api_client, user):
    api_client.force_authenticate(user=user)
    response = api_client.post("/api/articles/", {
        "title": "新文章",
        "slug": "new-article",
        "content": "...",
        "category_id": articles[0].category.id,
    })
    assert response.status_code == 201

步骤 3:配置日志

# settings.py
import os

LOG_DIR = BASE_DIR / "logs"
LOG_DIR.mkdir(exist_ok=True)

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {
            "format": "{asctime} [{levelname}] {name} {message}",
            "style": "{",
        },
        "simple": {
            "format": "[{levelname}] {message}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "level": "INFO",
            "class": "logging.StreamHandler",
            "formatter": "simple",
        },
        "file": {
            "level": "INFO",
            "class": "logging.handlers.RotatingFileHandler",
            "filename": LOG_DIR / "django.log",
            "maxBytes": 1024 * 1024 * 10,  # 10MB
            "backupCount": 5,
            "formatter": "verbose",
        },
    },
    "root": {
        "handlers": ["console", "file"],
        "level": "INFO",
    },
    "loggers": {
        "django": {
            "handlers": ["console", "file"],
            "level": "INFO",
            "propagate": False,
        },
        "blog": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

使用:

import logging

logger = logging.getLogger(__name__)

def some_view(request):
    logger.info("用户 %s 访问了文章列表", request.user)
    logger.debug("查询参数:%s", request.GET)

步骤 4:Debug Toolbar

pip install django-debug-toolbar
# settings.py(开发环境)
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]

INTERNAL_IPS = ["127.0.0.1"]

DEBUG_TOOLBAR_PANELS = [
    "debug_toolbar.panels.versions.VersionsPanel",
    "debug_toolbar.panels.timer.TimerPanel",
    "debug_toolbar.panels.settings.SettingsPanel",
    "debug_toolbar.panels.headers.HeadersPanel",
    "debug_toolbar.panels.request.RequestPanel",
    "debug_toolbar.panels.sql.SQLPanel",
    "debug_toolbar.panels.staticfiles.StaticFilesPanel",
    "debug_toolbar.panels.templates.TemplatesPanel",
    "debug_toolbar.panels.cache.CachePanel",
    "debug_toolbar.panels.signals.SignalsPanel",
    "debug_toolbar.panels.logging.LoggingPanel",
    "debug_toolbar.panels.redirects.RedirectsPanel",
    "debug_toolbar.panels.profiling.ProfilingPanel",
]

# urls.py(开发环境)
from django.urls import path, include

if settings.DEBUG:
    urlpatterns += [
        path("__debug__/", include("debug_toolbar.urls")),
    ]

步骤 5:生产环境部署

settings.py 生产配置:

import os

DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
assert SECRET_KEY, "SECRET_KEY 必须设置"

# 数据库换成 PostgreSQL
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("DB_NAME", "mydb"),
        "USER": os.getenv("DB_USER", "myuser"),
        "PASSWORD": os.getenv("DB_PASSWORD", ""),
        "HOST": os.getenv("DB_HOST", "localhost"),
        "PORT": os.getenv("DB_PORT", "5432"),
    }
}

# 静态文件
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

# 媒体文件
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# 安全
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

WhiteNoise 配置(用 WhiteNoise 替代 Nginx 直接服务静态文件):

pip install whitenoise
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",  # 加在 security 后面
    ...
]

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# 部署时收集静态文件
python manage.py collectstatic --noinput

Gunicorn 启动:

pip install gunicorn

# 启动(4 个 worker,每个 2 个线程)
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 4 --threads 2 --access-logfile -

Dockerfile 示例:

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "mysite.wsgi:application", "--bind", "0.0.0.0:8000"]

生产部署 Checklist:

检查项要求
DEBUGFalse
SECRET_KEY环境变量注入,不硬编码
ALLOWED_HOSTS配置域名
数据库PostgreSQL / MySQL(不用 SQLite)
静态文件collectstatic + WhiteNoise 或 Nginx
HTTPSSSL 证书 + SECURE_SSL_REDIRECT
HSTS开启
CSRF/Session CookieSecure + SameSite
日志配置 RotatingFileHandler
错误监控Sentry 等
定时任务Celery beat 或系统 cron
备份数据库定时备份

讲清楚要点

  1. Django 测试自动用事务隔离:每个 TestCase 的子测试在一个事务里跑,测试完自动回滚。不要用 pytest.mark.transactional 以外的形式直接操作数据库。
  2. pytest-django 让测试更 Pythonic:fixture 机制比 setUp/tearDown 灵活,适合参数化测试和依赖注入。
  3. 日志不是 print:生产环境 print 会被 WSGI 服务器吞掉。必须用 logging,配置 handler 写到文件、邮件或日志服务。
  4. WhiteNoise 是小型站点的静态文件最优解:把静态文件服务集成到 Django 进程里,省掉一层 Nginx。高并发大流量场景还是 Nginx/CDN。
  5. 环境变量是生产配置的唯一正确方式:python-dotenv 或 Docker/K8s 的 env 注入。django-environ 库可以简化解析。
  6. collectstatic 和 makemigrations 是部署必跑:漏了 collectstatic 静态文件 404;漏了 migrate 表结构对不上。

你可能会懵的地方

  • TestCase 和 TransactionTestCase 的区别? TestCase 用事务回滚,速度快但测不了事务内部行为(如信号)。TransactionTestCase 每次测试后 truncate 表,速度慢但更真实。

  • pytest 的 @pytest.mark.django_db 和 TestCase 冲突吗? 不冲突。pytest-django 会自动把 TestCase 子类转成 pytest 兼容。纯 pytest 风格用 @pytest.mark.django_db,Django 原生风格继承 TestCase。

  • 为什么生产不用 SQLite? SQLite 是文件锁,并发写入会锁整张表。PostgreSQL/MySQL 支持行锁、连接池、复制备份,是生产标准。

  • Gunicorn 的 worker 数怎么定? 经验公式:2 * CPU核心数 + 1。IO 密集型(等待数据库/网络)可以更多;CPU 密集型适当减少。

  • ASGI + Uvicorn 比 WSGI + Gunicorn 好吗? 如果用了 WebSocket、SSE、HTTP/2,必须用 ASGI + Uvicorn。纯 HTTP 的 API/模板站,Gunicorn + WSGI 更成熟稳定。

错法对照

错法问题正解
DEBUG = True 上生产泄露敏感信息、SQL/变量暴露DEBUG = False
密钥硬编码在代码里泄露后所有 session/cookie 失效环境变量
SQLite 上生产并发锁表PostgreSQL
忘记 collectstatic静态文件 404部署脚本里加上
ALLOWED_HOSTS = ["*"]Host 头攻击风险配具体域名
用 runserver 做生产服务器单线程、无安全加固Gunicorn/Uvicorn

卡点急救

  1. collectstatic 报错 ValueError: Missing staticfiles:检查 STATIC_ROOT 是否配置,WhiteNoise 是否安装,app 的 static 目录是否存在。
  2. Gunicorn 启动后请求超时:worker 数太少或连接池打满。增加 worker,或检查数据库连接数限制。
  3. Debug Toolbar 不显示:确认 INTERNAL_IPS 包含访问 IP,DEBUG=True,且 MIDDLEWARE 顺序正确(必须在 GZipMiddleware 之前)。

面试常问

  • Django 测试的 setUp 和 setUpClass 有什么区别? setUp 每个测试方法执行前运行,setUpClass 整个测试类只执行一次(用于创建共享的昂贵资源,如图片、大文件)。setUpTestData 是 Django 特有的优化:用类方法创建数据,测试方法间只读不改,省掉重复创建开销。

  • 生产环境为什么用 Gunicorn 不用 runserver? runserver 是单线程开发服务器,没有负载均衡、连接池、超时控制、日志格式化和 graceful reload。Gunicorn 是多进程预分叉模型,支持 worker 自动重启、信号处理、Unix socket 绑定,是 WSGI 标准生产服务器。

  • Django 配置分环境的最佳实践? 用环境变量做唯一配置源,settings 拆成 settings/base.py、settings/dev.py、settings/prod.py,DJANGO_SETTINGS_MODULE 指向对应文件。或用 django-environ 把所有配置统一从 .env 文件读。

自测三问

  1. TestCase 的数据库隔离机制是什么?为什么测试完数据不会残留?
  2. WhiteNoise 和 Nginx 服务静态文件各有什么优劣?
  3. 生产环境配置 SECRET_KEY 的正确方式是什么?

小结一句话

Django 测试框架覆盖模型、视图、API 的全链路测试,生产部署从 DEBUG=False、环境变量、HTTPS、WhiteNoise 到 Gunicorn 每一步都要到位,一张 checklist 保平安。

延伸

  • Django 官方文档 - 测试
  • WhiteNoise 文档
  • 下一课:综合练习——用 Django 完成一个完整的 Blog + API 项目。
加载本节练习…
← 上一课DRF 与异步任务下一课 →综合练习