CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
测试与部署
这一课你将能做到
用 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:生产环境部署 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 直接服务静态文件):
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # 加在 security 后面
...
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# 部署时收集静态文件
python manage.py collectstatic --noinput
pip install gunicorn
# 启动(4 个 worker,每个 2 个线程)
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 4 --threads 2 --access-logfile -
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"]
检查项 要求 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 保平安。
延伸