大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
测试与部署
这一课你将能做到
- 用 Django TestCase + pytest 写单元测试、集成测试和 API 测试
- 配置日志系统、debug toolbar、性能分析,建立开发调试工作流
- 用 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:
| 检查项 | 要求 |
|---|---|
| DEBUG | False |
| SECRET_KEY | 环境变量注入,不硬编码 |
| ALLOWED_HOSTS | 配置域名 |
| 数据库 | PostgreSQL / MySQL(不用 SQLite) |
| 静态文件 | collectstatic + WhiteNoise 或 Nginx |
| HTTPS | SSL 证书 + SECURE_SSL_REDIRECT |
| HSTS | 开启 |
| CSRF/Session Cookie | Secure + SameSite |
| 日志 | 配置 RotatingFileHandler |
| 错误监控 | Sentry 等 |
| 定时任务 | Celery beat 或系统 cron |
| 备份 | 数据库定时备份 |
讲清楚要点
- Django 测试自动用事务隔离:每个
TestCase的子测试在一个事务里跑,测试完自动回滚。不要用pytest.mark.transactional以外的形式直接操作数据库。 pytest-django让测试更 Pythonic:fixture 机制比setUp/tearDown灵活,适合参数化测试和依赖注入。- 日志不是
print:生产环境print会被 WSGI 服务器吞掉。必须用logging,配置 handler 写到文件、邮件或日志服务。 - WhiteNoise 是小型站点的静态文件最优解:把静态文件服务集成到 Django 进程里,省掉一层 Nginx。高并发大流量场景还是 Nginx/CDN。
- 环境变量是生产配置的唯一正确方式:
python-dotenv或 Docker/K8s 的 env 注入。django-environ库可以简化解析。 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 |
卡点急救
collectstatic报错ValueError: Missing staticfiles:检查STATIC_ROOT是否配置,WhiteNoise 是否安装,app 的 static 目录是否存在。- Gunicorn 启动后请求超时:worker 数太少或连接池打满。增加 worker,或检查数据库连接数限制。
- 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文件读。
自测三问
TestCase的数据库隔离机制是什么?为什么测试完数据不会残留?- WhiteNoise 和 Nginx 服务静态文件各有什么优劣?
- 生产环境配置
SECRET_KEY的正确方式是什么?
小结一句话
Django 测试框架覆盖模型、视图、API 的全链路测试,生产部署从 DEBUG=False、环境变量、HTTPS、WhiteNoise 到 Gunicorn 每一步都要到位,一张 checklist 保平安。
延伸
- Django 官方文档 - 测试
- WhiteNoise 文档
- 下一课:综合练习——用 Django 完成一个完整的 Blog + API 项目。