使用pytest进行项目的自动化流程测试
初探pytest
pytest
是比较流行的python
测试框架, 对于较高职业素养的测试人员来说, 也是一门必要掌握的语言. 因此在技术选型时首选python
, 那么pytest
也就顺理成章地使用了起来.
安装pytest
pip install pytest
使用pytest
通常你的.py
文件都是已test
开头的文件, 如test_something.py
. 如果你想单纯运行某个文件, 可以:
pytest -v test_something.py
如果你想运行某个目录下的所有文件, 则:
pytest -v path_of_folder/
另外, 有些自己实现的功能模块(如工具类模块), 想要借助pytest
来运行一些单元测试的流程, 比如有个路径为./utils/user.py
的文件:
class User:
def say_hello():
# do something..
def test_user_say_hello():
# test code...
直接运行pytest -v ./utils/user.py
是无法进行测试的, 因为pytest
只会寻找test
开头的.py
文件. 这时可以通过声明pythonpath
告诉pytest
这是个可测试的文件, 可以在项目根目录新建pytest.ini
文件:
[pytest]
pythonpath = utils
-v
参数是verbose
的意思, 日志会详细一点. 另外我常用的参数还有--lf
, 让pytest
只运行上次失败的case
, 有些不可抗力因素导致case
失败的场景, 用这个参数就可以再次确认该case是否真的异常.
基础模块的稳定性保证
在写真正的测试代码前, 肯定免不了先编写一些基础的模块. 比较通用的模块现在可以考虑用AI
帮你生成代码, 省时省力又好用. 但如何保证我们的基础模块代码都是稳定的呢? 这就是我上面提到的先测试pythonpath
声明的文件路径, 如:
pytest -v ./module_a/.py ./module_b/.py
善用fixture
fixture
是pytest
里一个重要的功能, 主要是作为装饰器, 把函数的运行结果应用到每个测试函数上面, 非常方便.
fixture
的结果缓存作用域有多个级别, 默认是每个测试函数都会运行一遍. 可以指定class
, module
, 甚至是session
. 作用域越大,
执行的次数就越少, 但是需要考虑sideEffect
的问题, 根据实际情况使用.
全局的fixture
如果有些fixture
需要在不同的测试文件中使用, 可以在根目录创建一个conftest.py
, 把你的fixture
函数写上, 就可以了:)
封装assert组合
在编写测试代码的过程中, 经常遇到不同的case
需要相同的校验逻辑, 比如文件权限的修改, 对应不同成员的权限控制校验. 这时候就可以通过合理的封装函数, 进行组合式的assert校验. 虽然可能看起来有点复杂, 堆栈比较长, 但是编写成本, 代码可维护性会大大增加.
测试报告的生成
一般我们运行测试代码, 最后的结果都是终端里显示. 如果没问题还好, 问题比较多就麻烦了. 看错误堆栈看得想死人. 有没有一个不错的测试报告生成器呢? 简单地探索了一下, 我决定使用了pytest-html
安装pytest-html
pip install pytest-html
生成测试报告
pytest -v --html=report.html your_case_folder/
指定报告结果保存在根目录的report.html
. 报告信息相当详细, 比如错误case
的堆栈, 每个case
的执行事件. 网页上还有一些交互, 总得来说相当不错. 美中不足的是运行--lf
的时候, 如果指定相同的文件, 那么则会把上一次的结果覆盖掉(可能有其他参数控制, 没仔细研究过).
测试报告可以作为一个发版评估文件使用, 也可以用于开发检查阶段, 妈妈再也不用担心我在命令行中滚几页才找到我想看的错误堆栈拉!
其他无关的经验
类型提示
python
在3.7
的版本里引入了类型系统. 作为工程来讲, 类型系统还是不可或缺的, 虽然写起来有点繁琐, 牺牲了脚本语言的轻便型. 但总体来说是利大于弊的(反正谁用谁知道).
fixture结果的类型提示
目前编辑器还做不到自动关联, 所以要我们自己做, 如:
from typing import TypedDict
class FixtureResult(TypedDict):
value: int
@pytest.fixture
def make_a() -> FixtureResult:
# blablabla
def test_a(make_a: FixtureResult):
# 编辑器能推断make_a.value是个int
api接口类型提示
因为我们的api接口文档是提供json
示例, 我目前使用的是dict_typer
, 可以把json
转成TypedDict
.
首先把每个接口的json
, 保存到一个目录, 并按一定的规则命名文件名, 如:
api_json/
- create_user.json
- create_team.json
- ...
新建使用一个gen.py
, 调用dict_typer
:
from os import listdir
from os.path import join
from json import loads
from dict_typer import get_type_definitions
json_dir = "api_json"
typing_dir = "typings/api_json"
json_files = listdir(json_dir)
for file in json_files:
source = None
with open(join(json_dir, file), "r", encoding="utf-8") as f:
source = loads(f.read())
with open(join(typing_dir, file.replace(".json", ".py")), "w") as f:
f.write(get_type_definitions(source, show_imports=True) + "\n")
运行gen.py
, 则会得到一个typings/api_json
的目录, 如下:
typings/
- api_json/
- create_user.py
- create_team.py
- ...
- __init__.py # 自己创建
这样就可以在别的文件里引用类型了:
from typings.api_json.create_user import Root as TypedUser