第 11 章 · 测试、安全与生产配置
本章目标:使用 pytest-django 与 Django TestCase 编写模型、API 与视图测试;理解 DEBUG=False 下的行为差异;通过环境变量管理 SECRET_KEY 与数据库凭据;掌握 Gunicorn + Nginx 部署流程与 HTTPS 相关头;为 shop-demo 建立可上线的最小安全配置清单。
学时建议:5~6 小时(含 2 小时部署演练)
前置:本模块 ch01~ch10;python-dev ch10 虚拟环境与依赖。运维概念可对照 ops-deploy 模块。
11.1 测试金字塔(Django 视角)
┌─────────┐
│ E2E 少量 │ Selenium / Playwright(选修)
├─────────┤
│ API 集成 │ DRF APIClient、pytest-django
├─────────┤
│ 单元较多 │ Model、Serializer、utils
└─────────┘
| 层级 | 工具 | shop-demo 示例 |
|---|---|---|
| 单元 | pytest | validate_price、Serializer 校验 |
| 集成 | TestCase / APIClient | 商品 CRUD API |
| E2E | 浏览器自动化 | 登录后台(毕业项目加分) |
本章测试均针对虚构 shop-demo,不依赖任何企业内部 CI 或私有镜像仓库。
11.2 pytest-django 环境搭建
pip install pytest pytest-django pytest-cov
pytest.ini(项目根):
[pytest]
DJANGO_SETTINGS_MODULE = shopdemo.settings
python_files = tests.py test_*.py *_tests.py
addopts = -ra --strict-markers
conftest.py:
import pytest
@pytest.fixture
def api_client():
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def admin_user(db):
from django.contrib.auth import get_user_model
User = get_user_model()
return User.objects.create_superuser(
username="admin-demo",
email="admin@user-demo.example.com",
password="TestPass123!",
)
运行:
pytest
pytest catalog/tests/test_api.py -v
pytest --cov=catalog --cov-report=term-missing
11.3 模型与 Serializer 测试
catalog/tests/test_models.py:
import pytest
from decimal import Decimal
from catalog.models import Product, Category
@pytest.mark.django_db
def test_product_str():
cat = Category.objects.create(name="图书", slug="books")
p = Product.objects.create(
name="Django 实战",
slug="django-book",
price=Decimal("88.00"),
stock=10,
category=cat,
)
assert str(p) == "Django 实战"
@pytest.mark.django_db
def test_product_price_positive():
from catalog.serializers import ProductWriteSerializer
data = {"name": "x", "slug": "x", "price": "-1", "stock": 1, "category": 1}
ser = ProductWriteSerializer(data=data)
assert not ser.is_valid()
assert "price" in ser.errors
11.4 API 测试(DRF APIClient)
catalog/tests/test_api.py:
import pytest
from decimal import Decimal
from django.urls import reverse
from rest_framework import status
from catalog.models import Product, Category
@pytest.mark.django_db
def test_product_list_anonymous(api_client):
cat = Category.objects.create(name="图书", slug="books")
Product.objects.create(
name="测试书", slug="test-book", price=Decimal("29.9"),
stock=5, category=cat, is_published=True,
)
url = reverse("product-list")
res = api_client.get(url)
assert res.status_code == status.HTTP_200_OK
assert res.data["count"] == 1
assert res.data["results"][0]["name"] == "测试书"
@pytest.mark.django_db
def test_product_create_requires_auth(api_client):
url = reverse("product-list")
res = api_client.post(url, {"name": "x", "slug": "x", "price": "10"}, format="json")
assert res.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
@pytest.mark.django_db
def test_product_create_authenticated(api_client, admin_user):
cat = Category.objects.create(name="图书", slug="books")
api_client.force_authenticate(user=admin_user)
url = reverse("product-list")
payload = {
"name": "新品",
"slug": "new-sku",
"price": "99.00",
"stock": 100,
"category": cat.id,
}
res = api_client.post(url, payload, format="json")
assert res.status_code == status.HTTP_201_CREATED
assert Product.objects.filter(slug="new-sku").exists()
11.5 Django TestCase 与客户端
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.urls import reverse
class ProductViewTests(TestCase):
def setUp(self):
self.client = Client()
User = get_user_model()
self.user = User.objects.create_user(username="op", password="pass12345")
def test_login_and_create_product_page(self):
self.client.login(username="op", password="pass12345")
response = self.client.get(reverse("product_create"))
self.assertEqual(response.status_code, 200)
| pytest-django | TestCase |
|---|---|
| 函数式、fixture 灵活 | 类式、Django 传统 |
| 与 pytest 生态统一 | django.test 内置 |
| 推荐新项目 API 测试 | 模板/表单测试均可 |