五、匿名和登录测试
🌟 第一部分:自动登录测试(user_client
fixture + 登录态测试)
这部分是测试 **"登录后才能访问的页面"**,用 force_login
模拟用户登录,验证 "未登录 vs 已登录" 的不同响应~
1. user_client
fixture ------ 自动登录的 "魔法工具"
python
@pytest.fixture()
def user_client(client: Client, user):
client.force_login(user) # 模拟用户登录
return client
这个**user
** 参数会自动 "召唤" 咱们之前定义的 user
fixture------ 也就是那个提前在数据库里创建好的 test_user
测试用户~
它的作用就像 "递道具":
user_client
需要一个 "真实存在的用户" 来模拟登录,而 user
fixture 正好提前准备好了这个用户,直接拿来就能用~ 不用再手动创建啦!
@pytest.fixture()
: pytest 的 "魔法标记"🧙,把user_client
变成可复用的工具。def user_client(client: Client, user):
:client
是 Django 测试客户端(虚拟小浏览器),user
是之前user
fixture 创建的测试用户。- 函数名
user_client
是这个工具的名字,其他测试用例可以直接用它!
client.force_login(user)
:- 核心逻辑:模拟用户登录,相当于 "用测试用户自动登录,不用输密码"~
- 效果:之后用
user_client
发请求时,会带着 "已登录" 的身份!
return client
:把 "已登录的虚拟浏览器" 返回,其他测试用例可以直接用~
2. test_submit_anonymous
------ 测试 "未登录用户访问"
python
def test_submit_anonymous(client): # 未登录的客户端
resp: HttpResponse = client.get("/beifan/submit")
assert resp.status_code == 302
def test_submit_anonymous(client):
:- 参数
client
是 "未登录的虚拟浏览器"(因为没用user_client
)。 - 测试目标:未登录用户访问
/beifan/submit
会怎样?
- 参数
resp: HttpResponse = client.get("/beifan/submit")
:- 用 "未登录浏览器" 发 GET 请求,访问提交反馈的页面。
assert resp.status_code == 302
:- 302 是 "重定向" 状态码~ 说明未登录用户访问需要登录的页面时,会被重定向到登录页(符合预期!)
3. test_submit_user
------ 测试 "已登录用户访问"
python
def test_submit_user(user_client): # 已登录的客户端
resp: HttpResponse = user_client.get("/beifan/submit")
assert resp.status_code == 200
html = resp.content.decode()
assert "速度" in html
def test_submit_user(user_client):
:- 参数
user_client
是 "已登录的虚拟浏览器"(因为用了user_client
fixture)。 - 测试目标:已登录用户访问
/beifan/submit
会怎样?
- 参数
resp: HttpResponse = user_client.get("/beifan/submit")
:- 用 "已登录浏览器" 发 GET 请求,访问提交反馈的页面。
assert resp.status_code == 200
:- 200 是 "成功" 状态码~ 说明已登录用户可以正常访问!
html = resp.content.decode()
:- 把响应内容(二进制)转成字符串,方便检查页面内容。
assert "速度" in html
:- 检查页面里是否有 "速度" 这个词,确保页面内容符合预期~
🌟 第二部分:参数化测试(test_result_all
)------ 批量测试 "不同请求方法"
这部分用 @pytest.mark.parametrize
批量测试不同 HTTP 方法(GET/POST/DELETE 等)访问页面的情况~
python
@pytest.mark.parametrize(
"method",
[
"get",
"post",
"delete",
"put",
"beifan" # 注意:不是合法 HTTP 方法 但就是能成功请求
]
)
def test_result_all(client, method):
resp: HttpResponse = client.generic(method.upper(), "/beifan/result")
assert resp.status_code == 200
@pytest.mark.parametrize("method", [...])
:- pytest 的 "参数化魔法"🪄!批量生成测试用例,
method
会依次取列表里的值(get、post、delete、put、beifan)。 - 作用:用不同的 HTTP 方法访问
/beifan/result
,看是否都返回 200。
- pytest 的 "参数化魔法"🪄!批量生成测试用例,
def test_result_all(client, method):
:client
是虚拟浏览器,method
是当前要测试的 HTTP 方法。
resp: HttpResponse = client.generic(method.upper(), "/beifan/result")
:client.generic(...)
是 Django 测试客户端的 "万能方法",可以发任意 HTTP 方法的请求。method.upper()
:把方法名转成大写(因为 HTTP 方法规范是大写,比如GET
、POST
)。- 效果:依次用 GET、POST、DELETE、PUT、BEIFAN(注意:
beifan
不是合法 HTTP 方法但能行)访问/beifan/result
。
assert resp.status_code == 200
:- 断言不管用哪种方法访问,返回状态码都是 200(假设页面允许这些方法)。
🌈 整体流程总结
- 自动登录工具 :
user_client
fixture 帮你模拟用户登录,返回 "已登录的虚拟浏览器"。 - 测试登录态 :
- 未登录用户访问需要登录的页面 → 重定向(302)。
- 已登录用户访问 → 成功(200),还能检查页面内容。
- 批量测试请求方法 :用
parametrize
批量测试不同 HTTP 方法访问页面,确保都能正常响应~
为什么要用client.generic(...)而不能用反射直接写method占位
为啥用 client.generic(...)
而不是 "反射占位"~ 就像在问:"为啥点外卖要用平台,不能自己挨个打电话呀?"
🍔 核心原因 1:client.generic
是 "官方外卖平台",稳!
Django 测试客户端的 client.generic(method, url)
是官方专门设计的 "万能请求方法",它帮你做好了所有 "请求规范" 的事儿:
- 自动处理 HTTP 方法的大小写(比如你传
'get'
,它会转成'GET'
) - 自动构造符合 HTTP 协议的请求(请求头、请求体格式等)
- 兼容所有合法 HTTP 方法(GET/POST/PUT/DELETE 等)
而 "反射 + 手动拼接" 就像 "自己挨个给餐厅打电话",容易出错(比如方法名没大写、请求头忘加、协议格式不对)~
🛠️ 对比:用 client.generic
vs 手动写 "反射"
假设你想动态发请求,不用 client.generic
,尝试 "反射" 写:
❌ 手动写法(容易翻车):
python
def test_result_all(client, method):
# 假设用 getattr 反射
method_func = getattr(client, method.lower()) # 比如 client.get 或 client.post
resp = method_func("/beifan/result") # 调用方法
assert resp.status_code == 200
问题 1:不是所有方法都能直接调用
Django 测试客户端的方法(client.get
/client.post
等)有严格的参数要求:
- 比如
client.get
主要传url
、params
(查询参数) - 但
client.post
还要传data
(请求体)、content_type
等
如果用反射调用,遇到 PUT/DELETE
这类方法,参数传不对就会报错~
问题 2:不支持自定义 / 非标准方法
比如你参数里写了 'beifan'
(虽然它不是合法 HTTP 方法),client.beifan
根本不存在,反射会直接报错:
AttributeError: 'Client' object has no attribute 'beifan'
但 client.generic
不管方法是否合法,都会尝试发请求(虽然服务器可能返回 405 方法不允许,但测试能覆盖这种情况)~
✅ 用 client.generic
(稳如外卖平台):
python
resp = client.generic(method.upper(), "/beifan/result")
- 不管
method
是'get'
/'post'
/'delete'
还是乱写的'beifan'
,它都会:- 转成大写(
method.upper()
) - 构造一个 HTTP 请求,带着这个方法名发出去
- 服务器收到后,按实际支持的方法处理(支持就返回 200,不支持返回 405 等)
- 转成大写(
这就像外卖平台帮你处理 "商家是否接单""配送是否合规",你只需要填方法和地址就行~
🌟 总结:client.generic
是 "万能请求通道"
它的存在就是为了简化 "任意 HTTP 方法请求" 的测试,帮你避免手动调用方法时的参数坑、方法不存在的坑、协议规范的坑~
而 "反射 + 手动调用" 就像 "自己跑腿送外卖",容易漏步骤、出问题~
所以 Django 官方贴心地给了 client.generic
这个 "万能工具",咱当然优先用它啦! 🛠️
六、🎯 统计测试覆盖率:给代码做 "体检" 啦~
这行命令 --cov=beifan --cov-report html --cov-fail-under=95
就像给你的代码请了个 "体检医生" ,每个参数都是体检步骤:
1. --cov=beifan
👉 告诉 pytest:"我要体检的项目是 beifan
这个 app 里的代码!"
把 beifan
想象成 "体检科室" ,只检查这个科室里的代码哪些被测试覆盖了~
2. --cov-report html
👉 生成超可爱的 HTML 报告 !测试跑完后,会自动生成一个网页,点进去能看到:
- 哪些代码行被测试覆盖了(绿色✔️)
- 哪些代码行 "躺平" 没被测试(红色❌)
像玩 "找不同" 游戏,一眼发现代码里的 "漏网之鱼"~
3. --cov-fail-under=95
👉 给体检定个 "及格线":测试覆盖率必须≥95% !
如果覆盖率不够(比如只有 90%),测试直接 "不及格" ❌ ,逼着你补全测试用例~
🌟 总结:
这就像给代码开 "健康训练营" :
- 先用
--cov
做体检,生成报告看哪里 "不健康" - 补全测试用例,让代码覆盖率达标
- 最后所有测试通过,代码才能 "毕业" !
快动手补测试,让你的 Django 项目健健康康~ 🚀
七、🎮 Django 单元测试闯关思路
1. 🏗️ 搭建测试环境:准备 "测试小道具"
- 依赖安装 :用
pdm
装测试工具(pytest、pytest-django 等),像给游戏买 "辅助道具" - 配置文件 :写
.flake8
(代码风格检查)、pytest.ini
(pytest 配置),给测试定 "规则"
2. 📝 设计测试用例:规划 "闯关路线"
- 拆分测试点:把功能拆成小关卡(注册、登录、提交反馈等)
- 覆盖场景:每个关卡测全场景(成功、失败、边界情况),比如注册要测 "用户名空""密码不一致""注册成功"
3. 🛠️ 写测试代码:用工具 "闯关"
- fixture 复用 :用
@pytest.fixture
做 "通用道具"(比如user
造测试用户、user_client
模拟登录),避免重复劳动 - 发请求测试 :用 Django 测试客户端
client
发 HTTP 请求(get
/post
等),模拟用户操作 - 断言验证:检查响应状态码、内容、数据库变化,确保功能符合预期
4. 📊 统计覆盖率:验收 "闯关成果"
- 用
--cov
系列参数生成覆盖率报告,要求 ≥95% - 看 HTML 报告找 "漏测代码",补全测试用例,让代码 100% 被 "宠幸"
5. 🔄 迭代优化:反复 "刷关卡"
- 测试不通过 → 修 bug、补用例
- 覆盖率不够 → 补测试,直到每个文件达标
- 最终所有测试通过、覆盖率达标,代码 "毕业" !
🌈 核心思想:
把 Django 功能拆成 "小关卡",用测试用例逐个攻破,用覆盖率验收成果~
就像玩闯关游戏,每通过一关、补全一个漏洞,代码质量就升级一级! 🚀
(简单说:拆分功能→写用例→跑测试→补漏洞→达标 ,循环往复直到代码 "无懈可击"~)
八、🎭 Django 集成测试:组件联演大派对!
把 Django 应用想象成一场 "舞台剧",集成测试就是让"演员(视图 / 模型 / 数据库 / 模板)" 集体彩排 **,验证他们配合是否默契~
1. 🎪 测试范围:"多角色联演",不单打独斗!
- 单元测试 :像 "单人脱口秀",只测 1 个函数 / 模型(比如测
TodoItem.save()
方法)。 - 集成测试 :像 "群演大合唱",把视图、模型、数据库、模板全拉到舞台上,一起测流程!
👉 比如测 "待办事项提交":
从用户点 "提交按钮"(视图接收 POST 请求)→ 数据存数据库(模型 + 数据库)→ 页面跳转并显示新事项(模板渲染),全流程联测!
2. 🧑💻 测试方式:模拟真实 "用户操作"!
Django 测试客户端(Client
)是你的 **"虚拟观众"**,帮你模拟用户行为:
① 发请求:client.get()
/client.post()
👉 模拟用户 "访问页面""提交表单",像观众喊:"我要看待办列表!""我要提交新事项!"
② 数据库联动:自动读写测试库
👉 提交数据时,集成测试会真的往测试数据库里写数据(但会自动清理,不影响真实库),像舞台上的 "道具组" 配合换道具~
③ 模板渲染检查:看页面内容对不对
👉 用 response.content
检查模板渲染结果,像观众喊:"新事项有没有显示在页面上呀?"
3. 🛠️ 测试设置:搭好 "舞台环境"!
- 测试数据库 :Django 会自动建一个临时数据库(用完就删),结构和真实库一样,像给彩排单独搭舞台~
- 加载配置:中间件、URL 路由、设置全启用,像舞台灯光 / 音效全打开,模拟真实环境!
4. 📝 测试示例:待办事项 App 联演!
假设你有个待办 App,功能是 "提交事项 → 存数据库 → 页面显示",集成测试会这么写:
python
import pytest
from django.test import Client
from todo.models import TodoItem # 你的待办模型
# 虚拟观众(测试客户端)
@pytest.fixture
def client():
return Client()
# 测试:提交待办事项全流程
def test_todo_submit_flow(client):
# 1. 模拟用户提交表单(发 POST 请求)
data = {"title": "给代码写测试!"}
response = client.post("/todo/submit/", data)
# 2. 检查是否跳转(状态码 302 代表重定向)
assert response.status_code == 302
# 3. 检查数据库是否真的存了数据
todo = TodoItem.objects.first()
assert todo.title == "给代码写测试!"
# 4. 检查页面是否显示新事项(访问列表页)
list_response = client.get("/todo/list/")
assert "给代码写测试!" in list_response.content.decode()
👉 这个测试里,视图、模型、数据库、模板全联动了!像验证 "用户提交 → 数据存库 → 页面显示" 的完整流程~
5. 🎯 测试目的:确保 "演员配合默契"!
- 单元测试保证 "单个演员演技好",集成测试保证 "所有演员配合好"~
- 如果集成测试失败,说明组件间有 "配合漏洞"(比如视图存数据的逻辑和模型不一致,模板没正确渲染数据)。
🌈 总结:
Django 集成测试是 **"多组件联演测试"**,用虚拟客户端模拟用户操作,联动视图 / 模型 / 数据库 / 模板,验证全流程是否通顺~
就像舞台剧彩排:单个演员(单元测试)没问题 → 全体演员联演(集成测试)没问题 → 正式演出(上线)才稳! 🚀