gunhawk

gunhawk

Frontend Developer

Coding is part of my life, 加藤恵は大好き=。=

使用pytest进行项目的自动化流程测试

作者: gunhawk时间: 2023-10-27python

初探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

fixturepytest里一个重要的功能, 主要是作为装饰器, 把函数的运行结果应用到每个测试函数上面, 非常方便.

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的时候, 如果指定相同的文件, 那么则会把上一次的结果覆盖掉(可能有其他参数控制, 没仔细研究过).

测试报告可以作为一个发版评估文件使用, 也可以用于开发检查阶段, 妈妈再也不用担心我在命令行中滚几页才找到我想看的错误堆栈拉!

其他无关的经验

类型提示

python3.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