? “Python猫” ,一个值得加星标的公众号
花下猫语:本文是《提升你的 Python 项目代码健壮性和性能》系列的第二篇。该系列主要讲解一些提升代码健壮性的姿势和小技巧。参见 第一篇
作者:无与童比(Python/Golang/JS, 全干(栈)工程师)
原文:https://zhuanlan.zhihu.com/p/56863684
图 | 《天空之城》剧照
本文的更多的是写给 Python 后端的程序员。
来简单分享一下我对写测试的理解。
在上一篇文章中,我提到了代码覆盖率,即测试的一种指标。
本期就聊聊测试这件小事情。
本文目录如下:
▼ 如何通过测试提升 Python 代码的健壮性 : section
0x00 前言 : section
▼ 0x01 测试的分类 : section
后端主要关注哪些测试 : section
▼ 0x02 为什么要写测试 : section
让新手更快的了解代码 : section
让发布代码的时候更加有底气 : section
让程序更容易重构 : section
加快团队的开发速度 : section
▼ 0x03 为什么不要写测试 : section
测试不能解决的问题 : section
不适当的测试为什么是负担 : section
并不是所有地方都容易测试的 : section
▼ 0x04 写 Python 测试的一些注意事项 : section
项目的环境隔离 : section
测试的基本环境 : section
单测 / 功测 / 端对端 : section
如何处理外部服务 : section
其他 Pytest 小技巧 : section
0xEE 参考 : section
测试有很多种,按照测试设计的方法可以分为:1. 黑盒 2. 白盒
按照测试目的:
1. 功能测试
单元测试
功能测试
集成测试
场景测试
A/B 测试
2. 非功能测试
压力测试
安全性测试
可访问性测试
其他
回归测试
易用性测试
还有不少,懒得去整理了.....
代码覆盖率顾名思义,就是测试用例覆盖运行代码的比重。
来讲讲测试的优点。
为什么要写测试来覆盖代码。
既不是不写,也不是狂写一气。看到这里你可能有些疑惑?写测试还加快速度?Are you kidding?
一个一个来解释吧。
举个简化版的例子,『用户下单』到『用户收货』。
测试用例里的数据,往往是能跑通某段代码的最佳测试数据集合。
假如,有个程序员写了 『下单-在线支付-确认收货』的集成测试。作为刚接手这段代码的人。可以在最短的时间内,通过阅读测试代码从而理解整个流程。
有 fixture, 新手可以在很短的时间内知道 setup 能让项目跑起来的基本数据
当然,如果过多的写了测试,也会导致阅读起来比较困难。
写测试,是为了验证代码运行正确。
一个流程,通常包含若干个子流程,子流程是对的,整个流程才是对的。
如果不写测试对一些关键的流程进行全面的覆盖,则会导致
当你知道写测试代码有这么多优点的之后,你的第一反应是,这我都知道,但是,写测试还能加快开发速度?
当然,你要知道,一个需要去维护的有价值的产品,往往需要不断的修改流程。
一开始,PM 告诉你只需要下单买个东西,后来,要加上满减券,再后来要加上各种类型的券,然后你要对接第三方服务,接下来你要对付各种不按照你设定的流程出牌的用户….
写测试,则是通过不断的补充一些测试,实现整个流程的测试自动化。形成一套测试该项目的测试代码。流程长的令人发指,你指望全靠人肉来测试?
当然,前提是
虽然说,我写的是加快团队的开发速度,但实际上,也适用于个人。
除非,你是写渲染页面的…. 所见即所得。无需任何测试
依照软件界著名的『没有银弹』理论,说完了测试的优越性,也要来说说测试的局限性,主要有三点:
测试能确保代码的运行质量,但无法确保代码编写质量,也无法保证产品设计逻辑上的问题。
也就是说
当你觉得测试代码写起来比较难受的时候,你应该考虑重构一下你的程序了。
人总要习惯的是:
放到测试上来说,测试,也是测不完的。
写了一个 IF ELSE , 你需要测两组,多写了一个 IF ELSE, 你就要测四组。如果是一个比较复杂的流程的话,基本上全面测试就很难写完了。
我的想法是:
并不是所有地方都容易测试的。
这类业务如果做的比较深入,需要 Mock 掉很多逻辑。
从整体项目角度,代码的运行环境应该区分 Local/Test/Stage/Prod 四种环境。
之所以要做这种区分,是因为不同的环境侧重点不同。
一般起一个 Docker-Compose 文件,来快速初始化测试环境。
比如 WebApp / Celery Worker / Celery Beats / Redis / RabbitMQ / MySQL 可以 make start 直接起这些服务。
之前说,后端需要注意下面的测试
性能测试一般可以通过监控来提前对系统在哪些地方有瓶颈。看场景,一般观察监控会更加容易预测系统的瓶颈,这个更多偏向于调优,放到后面来说吧。
框架假设我们使用 Flask , 再假设有这么一个 BBS(我知道你想吐槽为什么又拿博客 /BBS 举例子,懒得交代过多的业务场景背景知识了,逃…)
tests # 测试文件目录
├── __init__.py
├── conftest.py # 这里存放可能被子目录引用到的集合
├── e2e # 『端对端测试』
│ ├── __init__.py
│ ├── test_viewer.py
│ ├── test_user.py
│ ├── test_admin.py
│ └── test_organization.py
├── functional # 『功能测试』
│ ├── __init__.py
│ ├── test_do_simple_reply.py
│ ├── test_do_complex_reply.py
│ └── test_helper.py
├── unit # 『单元测试』
| ├── __init__.py
| ├── test_auth.py
| └── test_calc_some_thing.py
├── test_auth_helper.py # 存放基本的用于切换身份的代码
├── test_const.py
└── test_factory_helper.py # 可以用来批量初始化数据
这个流程并不算复杂,但足以写测试了。
前者比较简单,后者相对而言更加靠近集成测试。各有利弊。我一般在关键流程上多做几个拉起来测试的代码。
但拉起来测试要解决的问题就多了一个,即,用户登陆认证。你调用某个 Service 的时候,是以匿名用户 / 用户身份 / Admin / Org 调用的。
即在调用不同的 Service 解决问题的时候,你可能需要快速的切换身份。切换完身份再速度切换回来。于是,test auth helper 出来了。helper 里面有个 switch as 函数,每次需要切换身份的时候,把 g 变量里面的登陆快照 g.user g.admin http://g.org push 到 LocalStack 栈里 (from werkzeug.local import LocalStack), 调用完 Service 再 Pop 出来。
拉起来测试的效果是这样子的。
def test_complex_process(org, user, admin):
with switch_as_org(org) as org: # 1. 组织 Organization 发布了一个 Thread
thread = publish_thread_by_org()
with switch_as_user(user) as user: # 2. 用户 User 在这个 Thread 进行了 Reply
reply = reply_thread(thread)
assert reply
with switch_as_anonymous() as anonymous_user:
_thread = see_thread(thread)
assert reply in _thread.replies # 『未注册的用户能看见』
with switch_as_admin() as admin: # 3. 管理员 Admin 发现了 User 似乎发布了不该发布的信息。删 Reply。
delete_reply(reply)
assert reply.deleled
with switch_as_anonymous() as anonymous_user:『未注册的用户看不见』
_thread = see_thread(thread)
assert reply not in _thread.replies
# 在这里,我的身份还是 user
_thread = see_thread(thread)
assert reply in _thread.replies # 『Ower 用户能看见』
# 4. 最后 User 进行申诉,Admin 发现其实发布的东西挺 OK 的,给予通过。『未注册的用户能看见』
作为开发者,你只需要让这个测试跑通就基本开发完毕了。在这个过程中,你也可以更好的梳理你的代码。
在拉起来做测试的时候,假如我们多了一个流程,用户可以通过微信支付赞赏 reply, 这就不得不依赖于外部的服务。
而拉起来做测试的时候,就会遇到一个非常尴尬的问题,因为我上面的接口都粒度都比较大,是赞赏这个流程里面的非常小的流程,必须要走微信的 http 请求。
解决方式也很简单。mock 掉请求微信的函数。手动调用一下支付回调函数,即可。
当然,对于 http 请求,也可以使用 responses 这个神器来快速 mock 神器 requests 的 response
大致的用法如下
def mock_success_pay():
def request_callback(request):
headers = {}
dispatch_callback(data=data)
return 200, headers, resp_body
responses.add_callback(
responses.POST,
PAY_URL,
callback=request_callback,
content_type="application/json",
)
@responses.activate
def test_pay(user):
mock_success_pay()
switch_as_user(user) as u:
order = pay_order(u)
assert order.status == "PAID"
有的时候 ipdb 比 pdb 用起来不止好了一点点。如何在 pytest 里用上呢?
pytest -v --pdb --pdbcls=IPython.terminal.debugger:Pdb
https://www.zhihu.com/question/21017354/answer/589574939
https://www.zhihu.com/question/312395573/answer/604772703