ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pytest测试框架实战:从入门到接口自动化与CI集成

Pytest测试框架实战:从入门到接口自动化与CI集成 1. 为什么我最终把测试栈全押在了 Pytest 上刚入行那会儿我写测试用的是 Python 自带的unittest。倒不是说它不能用而是每次写一个用例都要继承TestCase、写setUp和tearDown断言还得记一堆assertEqual、assertTrue的方法名。一个简单的接口校验代码量能膨胀到让人不想维护。后来团队里有人扔了个pytest的脚本过来我第一反应是“这玩意儿连类都不用写”第二反应是“断言直接用assert”。跑了一遍之后我就知道回不去了。Pytest 本质上是一个 Python 的测试框架但它和unittest最大的区别在于它把“写测试”这件事的门槛降到了几乎为零。你不需要继承任何类不需要记住几十个断言方法一个函数、一个assert、一个文件名以test_开头就能跑起来。但它又不止于此——参数化、Fixture、插件生态、并发执行、报告生成这些在真实项目里迟早要面对的需求它都有对应的解法而且解法通常比unittest优雅得多。这篇文章适合谁看如果你刚开始接触 Python 自动化测试或者从unittest想迁到pytest又或者你已经在用pytest但总觉得 Fixture 的作用域没搞明白、参数化写得不够顺手、报告出来一堆乱码那这篇内容应该能帮你把大部分坑填上。我不会只讲语法而是按一个真实项目的推进节奏从环境搭建到框架分层把每个环节的“为什么这么选”和“踩过什么坑”都摊开说。2. 环境准备与项目骨架搭建2.1 Python 环境与 Pytest 安装的几种姿势装pytest之前先把 Python 环境理顺。我见过太多人因为系统里同时存在多个 Python 版本导致pip install pytest装到了 A 版本但运行时用的是 B 版本然后报ModuleNotFoundError。所以第一步不是急着装pytest而是确认你当前终端里的python和pip指向同一个解释器。在终端里执行python --version pip --version如果两个命令输出的路径前缀一致比如都是/usr/local/bin/python3.11和/usr/local/bin/pip3.11那就没问题。如果不一致建议用python -m pip install pytest的方式来安装这样能保证装到当前python对应的环境里。python -m pip install pytest装完之后验证一下pytest --version能输出版本号就说明安装成功。这里有个小细节如果你用的是虚拟环境强烈建议用记得先激活再装。虚拟环境的好处是项目之间的依赖不会互相污染尤其是当你同时维护多个测试项目时A 项目需要pytest 7.xB 项目需要pytest 8.x没有虚拟环境就会很痛苦。创建虚拟环境的命令python -m venv venv source venv/bin/activate # Linux/macOS venv\Scripts\activate # Windows激活之后终端提示符前面会出现(venv)字样这时候再装pytest就只影响这个项目。2.2 项目目录结构怎么设计才不乱很多人写测试一开始就是一堆test_xxx.py平铺在根目录跑起来没问题但用例一多就找不到北。我习惯按下面的结构来组织project/ ├── tests/ │ ├── conftest.py │ ├── test_user/ │ │ ├── test_login.py │ │ └── test_profile.py │ ├── test_order/ │ │ ├── test_create.py │ │ └── test_query.py │ └── utils/ │ ├── api_client.py │ └── data_loader.py ├── pytest.ini └── requirements.txtconftest.py是pytest的一个特殊文件放在tests/根目录下里面定义的 Fixture 可以被整个目录树下的用例共享。子目录里也可以放conftest.py作用范围只限当前目录及子目录。这个机制后面讲 Fixture 的时候会详细展开。pytest.ini是配置文件用来指定默认的运行参数、标记、日志格式等。我一般会写这几项[pytest] testpaths tests python_files test_*.py python_classes Test* python_functions test_* addopts -v --tbshort --strict-markers markers slow: marks tests as slow smoke: marks tests as smoke teststestpaths告诉pytest默认去tests目录找用例这样在根目录直接敲pytest就不会扫到其他无关文件。addopts里的--strict-markers很关键它强制你声明的标记必须注册否则拼错标记名的时候会直接报错而不是默默忽略。2.3 第一个能跑起来的用例环境搭好之后写个最简单的用例验证一下。新建tests/test_demo.pydef test_addition(): assert 1 1 2 def test_string_upper(): assert hello.upper() HELLO在项目根目录执行pytest你会看到类似这样的输出tests/test_demo.py::test_addition PASSED tests/test_demo.py::test_string_upper PASSED两个用例都通过。注意这里没有if __name__ __main__也没有任何unittest的导入pytest自动发现了这两个函数并执行。这就是它最直观的“入门友好”之处。注意pytest默认只识别以test_开头的文件和函数。如果你写了一个check_login()函数它是不会被执行的。这个命名约定看起来是限制实际上是保护——它让你在测试文件里可以自由定义辅助函数而不用担心被误当成用例。3. 断言、参数化与数据驱动实战3.1 为什么assert比assertEqual更值得用pytest直接使用 Python 原生的assert语句但它做了一件很聪明的事当断言失败时它会重写断言表达式把中间值打印出来。比如def test_dict_compare(): expected {name: Alice, age: 30} actual {name: Alice, age: 31} assert actual expected失败时的输出会精确告诉你哪个 key 的值不一致AssertionError: assert {name: Alice, age: 31} {name: Alice, age: 30} Omitting 1 identical items, use -vv to show Differing items: {age: 31} ! {age: 30}如果用unittest的assertEqual你只能得到一个“不相等”的结论还得自己加打印。pytest这种“断言重写”机制在调试复杂数据结构时特别省事尤其是对比嵌套字典或列表的时候。对于异常断言pytest提供了pytest.raises上下文管理器import pytest def test_division_by_zero(): with pytest.raises(ZeroDivisionError) as exc_info: _ 1 / 0 assert division by zero in str(exc_info.value)这里exc_info捕获了异常对象可以进一步校验异常信息。我经常用它来验证接口返回的错误码或自定义异常的消息内容。3.2 参数化一份代码跑十组数据参数化是pytest最实用的功能之一。假设你要测试一个登录接口需要覆盖正常登录、密码错误、账号不存在、账号被锁定等多种情况。如果每种情况写一个函数代码会非常冗余。用pytest.mark.parametrize可以一次性解决import pytest pytest.mark.parametrize(username, password, expected_code, [ (admin, correct_pwd, 200), (admin, wrong_pwd, 401), (nonexistent, any_pwd, 404), (locked_user, correct_pwd, 423), ]) def test_login(username, password, expected_code): result login_api(username, password) assert result.status_code expected_codeparametrize的第一个参数是字符串形式的参数名列表第二个参数是数据列表。pytest会自动为每一组数据生成一个独立的测试用例在报告里分别显示。这样做的好处是某一组数据失败不会影响其他组继续执行而且失败时能清楚知道是哪组参数出了问题。如果参数名比较多可以用逗号分隔的字符串也可以用列表pytest.mark.parametrize([a, b, expected], [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), ]) def test_add(a, b, expected): assert a b expected3.3 数据驱动的进阶玩法从文件读取用例当测试数据量大到不适合写在代码里时可以把数据抽到外部文件。常见的有 JSON、YAML、CSV 三种格式。我一般用 YAML因为可读性好支持注释写起来比 JSON 舒服。假设有一个data/login_cases.yaml- username: admin password: correct_pwd expected_code: 200 - username: admin password: wrong_pwd expected_code: 401 - username: nonexistent password: any_pwd expected_code: 404然后在测试文件里读取import yaml import pytest def load_cases(path): with open(path, encodingutf-8) as f: return yaml.safe_load(f) pytest.mark.parametrize(case, load_cases(data/login_cases.yaml)) def test_login_with_yaml(case): result login_api(case[username], case[password]) assert result.status_code case[expected_code]这里把整个case字典作为一个参数传入用例内部按 key 取值。这样做的好处是新增用例只需要改 YAML 文件不用动测试代码。对于接口自动化来说这种“数据与代码分离”的模式非常关键因为接口参数经常变但测试逻辑相对稳定。实操心得YAML 文件里的布尔值、数字、字符串要小心。password: 123456会被解析成整数而接口可能期望字符串。我踩过这个坑后来统一在 YAML 里给密码加引号或者用str()转换。另外YAML 对缩进极其敏感建议用编辑器的 YAML 插件做语法校验。3.4 参数化的 ID 定制与可读性优化默认情况下pytest会用参数的字符串表示作为用例 ID比如test_login[admin-correct_pwd-200]。如果参数值很长或者包含特殊字符ID 会变得很难看。可以用ids参数自定义pytest.mark.parametrize(username, password, expected_code, [ (admin, correct_pwd, 200), (admin, wrong_pwd, 401), ], ids[正常登录, 密码错误]) def test_login(username, password, expected_code): ...这样报告里显示的就是test_login[正常登录]和test_login[密码错误]一眼就能看出哪条失败了。对于中文项目来说这个技巧能大幅提升报告的可读性。4. Fixture 机制从共享数据到分层管理4.1 Fixture 到底是什么为什么不用 setUpunittest里的setUp和tearDown是每个用例执行前后各跑一次粒度是固定的。但真实项目里有些资源只需要初始化一次比如数据库连接池有些需要每个用例独立比如临时文件有些需要每个模块共享比如登录后的 token。unittest要处理这些不同粒度得用setUpClass、setUpModule等一堆方法而且继承关系一复杂就容易乱。pytest的 Fixture 用装饰器定义通过scope参数控制生命周期通过函数参数注入到用例中。粒度从细到粗有function、class、module、package、session五档。默认是function也就是每个用例执行前都会调用一次。import pytest pytest.fixture def sample_data(): return {name: test, value: 42} def test_with_fixture(sample_data): assert sample_data[value] 42用例函数只需要把 Fixture 名字作为参数写进去pytest就会自动调用它并把返回值注入。这种“按需注入”的方式比继承TestCase灵活得多因为一个用例可以同时使用多个 Fixture而且 Fixture 之间也可以互相依赖。4.2 不同 scope 的选择逻辑与实测差异scope的选择直接影响测试执行效率和隔离性。我按经验给一个选择参考scope生命周期适用场景注意事项function每个用例前后临时数据、独立状态默认值隔离性最好class每个测试类前后类内共享的初始化类内用例共享类间隔离module每个模块前后模块级资源如文件句柄模块内共享注意状态污染package每个包前后包级配置较少使用session整个测试会话数据库连接、登录 token全局共享需谨慎清理举个例子登录 token 通常用session级别因为整个测试会话只需要登录一次pytest.fixture(scopesession) def auth_token(): token login_api(admin, correct_pwd) yield token logout_api(token)这里用了yield而不是returnyield之前的代码是 setup之后的代码是 teardown。pytest会在所有使用该 Fixture 的用例执行完后自动执行yield后面的清理逻辑。这个设计比unittest的tearDown更直观因为 setup 和 teardown 写在一起不用来回翻代码。注意session级别的 Fixture 如果被多个模块使用要确保它不依赖任何模块级的状态。我曾经写过一个session级别的数据库 Fixture结果某个模块的用例修改了数据库连接的超时时间导致后续模块全部超时。后来改成module级别才解决。所以 scope 越大越要保证 Fixture 内部状态的纯净。4.3 conftest.py 的分层共享机制conftest.py是pytest实现 Fixture 跨文件共享的核心。它的规则很简单conftest.py里定义的 Fixture对同目录及子目录下的所有测试文件可见。子目录里的conftest.py可以覆盖父目录的同名 Fixture。我通常这样分层项目根conftest.py放session级别的全局 Fixture比如配置加载、日志初始化。tests/conftest.py放module或function级别的通用 Fixture比如 API 客户端、临时目录。tests/test_user/conftest.py放用户模块专用的 Fixture比如测试用户数据。这种分层的好处是每个模块只关心自己需要的 Fixture不会因为根目录的conftest.py太臃肿而影响加载速度。pytest在收集用例时会按目录层级依次加载conftest.py所以 Fixture 的查找是就近优先的。4.4 Fixture 的自动使用与参数化有些 Fixture 不需要在用例参数里显式声明比如日志记录、计时统计。可以用autouseTruepytest.fixture(autouseTrue) def log_test_name(request): print(f\n开始执行: {request.node.name}) yield print(f执行结束: {request.node.name})request是pytest内置的 Fixture提供了当前用例的上下文信息比如request.node.name就是用例名。autouse的 Fixture 会在每个用例前后自动执行不需要在用例函数里写参数。Fixture 本身也可以参数化pytest.fixture(params[mysql, postgresql, sqlite]) def db_driver(request): return request.param这样每个使用db_driver的用例都会跑三遍分别对应三种数据库驱动。这个特性在做兼容性测试时特别有用比如验证同一套接口在不同数据库下的行为是否一致。5. 标记、跳过与用例筛选策略5.1 内置标记与自定义标记的用法pytest内置了几个常用标记pytest.mark.skip无条件跳过。pytest.mark.skipif满足条件时跳过。pytest.mark.xfail预期失败失败不算错成功反而会提示XPASS。pytest.mark.parametrize参数化。自定义标记需要在pytest.ini里注册否则--strict-markers模式下会报错。注册之后可以这样用pytest.mark.smoke def test_login_smoke(): ... pytest.mark.slow def test_full_regression(): ...运行时通过-m筛选pytest -m smoke # 只跑 smoke 标记的用例 pytest -m not slow # 排除 slow 标记的用例 pytest -m smoke and not slow # 组合条件这个机制在 CI 流水线里非常实用。比如每次提交只跑smoke用例每天凌晨跑全量回归。标记让同一套用例可以在不同场景下按需执行不用维护多份测试代码。5.2 skipif 的条件表达式与动态跳过skipif支持 Python 表达式可以基于环境变量、Python 版本、依赖库是否存在等条件动态跳过import sys import pytest pytest.mark.skipif(sys.version_info (3, 10), reason需要 Python 3.10) def test_new_syntax(): ... pytest.mark.skipif( not pytest.importorskip(redis, reasonredis 未安装), reasonredis 不可用 ) def test_redis_cache(): ...pytest.importorskip会在模块导入失败时直接跳过整个模块比在用例级别判断更高效。我通常用它来处理可选依赖比如某些测试需要pymysql但本地开发环境没装就自动跳过而不是报错。5.3 xfail 的正确使用场景xfail用来标记“已知会失败”的用例。比如某个 bug 还没修但用例已经写好了可以先标记为xfail这样 CI 不会因为这个问题而红掉。等 bug 修复后pytest会报告XPASS提醒你去掉xfail标记。pytest.mark.xfail(reason已知问题 #1234等待修复) def test_known_bug(): assert buggy_function() expectedxfail还可以配合strictTrue使用表示“如果这个用例意外通过了就当成失败处理”。这在验证 bug 是否真的修复时很有用。6. 接口自动化实战从单接口到业务流6.1 接口测试的分层设计接口自动化最容易犯的错误是把所有逻辑写在一个用例里发请求、断言状态码、断言响应体、清理数据全堆在一起。用例一多维护成本急剧上升。我习惯按三层来组织接口层封装单个接口的请求方法只负责发请求和返回响应不做断言。业务层组合多个接口完成一个业务场景比如“创建订单”可能涉及登录、查库存、下单、支付四个接口。用例层调用业务层方法做断言和参数化。这样分层的好处是当接口路径或参数格式变化时只需要改接口层当业务逻辑调整时只需要改业务层用例层基本不动。接口层的封装示例import requests class UserApi: def __init__(self, base_url): self.base_url base_url self.session requests.Session() def login(self, username, password): return self.session.post( f{self.base_url}/api/login, json{username: username, password: password} ) def get_profile(self, user_id): return self.session.get(f{self.base_url}/api/users/{user_id})业务层class UserBusiness: def __init__(self, api): self.api api def login_and_get_profile(self, username, password): login_resp self.api.login(username, password) token login_resp.json()[token] self.api.session.headers[Authorization] fBearer {token} return self.api.get_profile(login_resp.json()[user_id])用例层def test_login_and_profile(user_api): business UserBusiness(user_api) resp business.login_and_get_profile(admin, correct_pwd) assert resp.status_code 200 assert resp.json()[username] admin这种分层在项目初期看起来有点“过度设计”但当接口数量超过二十个之后优势就非常明显了。尤其是当后端改了登录接口的返回结构时只需要改UserApi.login和UserBusiness里取 token 的那一行所有相关用例自动适配。6.2 用 Fixture 管理接口客户端与登录态接口测试的 Fixture 通常包括base_url、session、auth_token。我一般这样写pytest.fixture(scopesession) def base_url(): return https://api.example.com pytest.fixture(scopesession) def user_api(base_url): return UserApi(base_url) pytest.fixture(scopesession) def auth_token(user_api): resp user_api.login(admin, correct_pwd) return resp.json()[token] pytest.fixture def authorized_api(user_api, auth_token): user_api.session.headers[Authorization] fBearer {auth_token} yield user_api user_api.session.headers.pop(Authorization, None)authorized_api是function级别每个用例拿到的是同一个user_api实例但请求头会在用例结束后清理。这样做既复用了 session 的连接池又保证了用例之间的隔离性。6.3 响应断言与 JSON Schema 校验接口测试的断言不能只看状态码。我通常分三层校验状态码是否符合预期。关键字段是否存在且类型正确。业务字段的值是否符合预期。对于结构复杂的响应可以用jsonschema库做模式校验from jsonschema import validate user_schema { type: object, properties: { id: {type: integer}, username: {type: string}, email: {type: string, format: email}, }, required: [id, username, email] } def test_user_schema(authorized_api): resp authorized_api.get_profile(1) validate(instanceresp.json(), schemauser_schema)Schema 校验的好处是当后端偷偷改了字段类型比如id从整数变成字符串测试会立刻发现而不是等到前端报错才后知后觉。6.4 数据库断言与数据清理有些业务场景需要验证数据是否真正落库。这时候需要在用例里查数据库pytest.fixture def db_conn(): conn pymysql.connect(hostlocalhost, usertest, passwordtest, databasetest_db) yield conn conn.close() def test_create_order(authorized_api, db_conn): resp authorized_api.post(/api/orders, json{product_id: 1, quantity: 2}) order_id resp.json()[order_id] with db_conn.cursor() as cursor: cursor.execute(SELECT status FROM orders WHERE id %s, (order_id,)) row cursor.fetchone() assert row[0] created数据清理通常放在 Fixture 的 teardown 里或者用事务回滚的方式。我倾向于在conftest.py里定义一个db_transactionFixture每个用例开始前开启事务结束后回滚这样测试数据不会污染数据库。7. 报告、并发与 CI 集成7.1 生成可读性强的测试报告pytest默认的终端输出适合快速查看但给团队看或者存档就需要 HTML 报告。pytest-html插件可以生成pip install pytest-html pytest --htmlreport.html --self-contained-html--self-contained-html会把 CSS 和 JS 内联到 HTML 里方便直接发给别人。报告里会显示每个用例的执行时间、状态、失败信息还可以通过--capturetee-sys把 print 输出也带进去。如果需要更详细的失败截图或日志可以配合pytest-metadata和pytest-cov一起用。覆盖率报告pip install pytest-cov pytest --covsrc --cov-reporthtml这会在htmlcov/目录下生成覆盖率报告能看到哪些代码行被测试覆盖了哪些没有。7.2 并发执行与用例隔离用例多了之后串行执行会越来越慢。pytest-xdist支持多进程并发pip install pytest-xdist pytest -n 4 # 4 个进程并发但并发的前提是用例之间完全隔离。如果多个用例共享同一个数据库记录并发时就会互相干扰。我一般会确保每个用例使用独立的数据比如用 UUID 生成唯一的用户名或者用tmp_pathFixture 创建临时文件。tmp_path是pytest内置的 Fixture每个用例都会得到一个独立的临时目录def test_file_upload(tmp_path): file tmp_path / test.txt file.write_text(hello) assert file.read_text() hello这个目录在用例结束后会自动清理不会残留垃圾文件。7.3 在 CI 流水线里跑 PytestCI 里跑pytest和本地差不多但有几个细节要注意用--junitxmlreport.xml生成 JUnit 格式的报告方便 CI 平台解析。用-x在第一个失败时停止快速反馈。用--timeout60设置单个用例超时防止卡死。一个典型的 CI 命令pytest -n 4 --junitxmlreport.xml --timeout60 --covsrc --cov-reportxml如果 CI 环境没有数据库可以用pytest-docker插件在测试前启动容器或者用skipif跳过依赖数据库的用例。8. 常见问题与排查技巧实录8.1 用例收集失败与导入错误问题运行pytest时报ModuleNotFoundError但手动python -c import xxx没问题。原因pytest的导入路径和直接运行 Python 脚本不同。pytest会把测试文件的根目录加入sys.path但如果你的项目结构比较复杂可能需要手动配置。解决在pytest.ini里加pythonpath .或者在conftest.py里用sys.path.insert(0, ...)。更推荐的方式是用pip install -e .把项目以可编辑模式安装到虚拟环境里。8.2 Fixture 找不到或作用域冲突问题用例报fixture xxx not found。排查步骤确认 Fixture 定义在conftest.py或当前测试文件里。确认conftest.py的目录层级是否正确。确认 Fixture 名字拼写一致大小写敏感。如果 Fixture 定义在类里需要加pytest.fixture并且用self访问。作用域冲突session级别的 Fixture 不能依赖function级别的 Fixture否则会报ScopeMismatch。解决方法是把依赖的 Fixture 也提升到相同或更大的 scope。8.3 参数化数据中的中文乱码问题参数化 ID 里的中文在报告里显示为乱码。解决在pytest.ini里加disable_test_id_escaping_and_forfeit_all_rights_to_community_support True或者在conftest.py里配置pytest_collection_modifyitems钩子来修改 ID 编码。8.4 常见问题速查表问题现象可能原因解决方法用例未被收集文件名或函数名不以test_开头重命名或修改python_files配置断言失败信息不详细未启用断言重写确保pytest版本 5.0且未禁用重写Fixture 执行顺序不对依赖关系不明确用 Fixture 参数显式声明依赖并发时数据冲突用例共享数据用tmp_path或 UUID 隔离数据报告中文乱码编码未指定在pytest.ini里加encoding utf-88.5 几个我踩过的坑第一个坑是autouseTrue的 Fixture 在session级别下执行时机。我曾经写了一个session级别的autouseFixture 来初始化日志结果发现它在第一个用例开始前才执行而不是在收集阶段。如果日志配置需要在收集阶段就生效得用pytest_configure钩子。第二个坑是parametrize的堆叠。多个parametrize装饰器叠加时参数组合是笛卡尔积。比如两个parametrize各有 3 组数据最终会生成 9 个用例。如果不注意用例数量会爆炸。我一般会控制单个用例的参数组合不超过 20 个。第三个坑是yieldFixture 里的异常处理。如果yield之前的代码抛异常yield之后的清理代码不会执行。所以清理逻辑要放在try/finally里或者用request.addfinalizer注册清理函数。9. 从单测到全链路Pytest 的扩展边界pytest的插件生态是它区别于其他测试框架的核心优势。除了前面提到的pytest-html、pytest-xdist、pytest-cov还有几个值得关注的pytest-mock提供mockerFixture比unittest.mock更顺手。pytest-asyncio测试异步代码支持async def用例。pytest-bdd行为驱动开发用 Gherkin 语法写用例。pytest-randomly随机化用例执行顺序发现用例间的隐式依赖。对于 UI 自动化pytest可以和playwright、selenium结合。playwright官方提供了pytest-playwright插件用例里直接注入pageFixture 就能操作浏览器。对于移动端appium也可以封装成 Fixture在session级别启动和关闭驱动。嵌入式软件的单元测试稍微特殊一些因为代码通常跑在目标板上。常见的做法是在宿主机上编译一个测试版本用pytest调用交叉编译后的可执行文件通过串口或网络读取输出。这种场景下pytest更多是作为测试调度和报告生成的工具具体的断言逻辑可能写在 C 代码里。10. 我个人在实际项目中的几点体会第一不要一开始就追求“完美框架”。我见过很多团队花两周搭了一个分层清晰、插件齐全的测试框架结果用例写了不到五十个就没人维护了。先让用例跑起来再根据痛点逐步重构比一开始就设计过度要务实得多。第二Fixture 的 scope 宁小勿大。function级别的 Fixture 虽然慢一点但隔离性最好。等到确实遇到性能瓶颈时再考虑提升 scope并且一定要做好清理逻辑。第三参数化的数据尽量外置。写在代码里的数据改一次就要动代码容易引入错误。放到 YAML 或 CSV 里产品和测试都能维护效率高很多。第四报告是给团队看的不是给自己看的。用例名、失败信息、截图、日志这些都要考虑可读性。一个只有AssertionError的报告对排查问题帮助有限。最后分享一个小技巧在conftest.py里加一个pytest_terminal_summary钩子可以在测试结束后打印自定义的汇总信息比如“本次共执行 120 个用例通过 115 个失败 5 个耗时 3 分 20 秒”。这个汇总比默认输出更直观团队晨会时直接截图就能用。
返回列表