ARTICLE DETAIL

资讯详情

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

Pytest自动化测试框架实战:从用例到Allure报告

Pytest自动化测试框架实战:从用例到Allure报告 1. 为什么要聊Pytest它到底解决了什么问题先说一个扎心的事实很多人学测试框架一开始就被UnitTest的样板代码劝退了。每个测试类都要继承TestCase每个用例都要写setUp和tearDown跑完还得自己拼报告。写测试比写业务代码还累最后干脆放弃测试回归“点点点”模式。Pytest的出现基本上把这个问题从根上解决了。它是一个Python第三方的测试框架核心卖点就一句话**写测试可以像写普通函数一样简单不需要继承任何基类不需要复杂的样板代码。**你只要写一个普通的函数函数名以test_开头里面用assert做断言Pytest就能自动发现、执行、统计结果。我第一次用的时候最大的感受是这玩意儿才是测试该有的样子。Pytest不仅适合Python单元测试现在主流的接口自动化测试、Web UI自动化测试底层跑用例的引擎也大量使用Pytest。你去看招聘网站上测试开发岗位的要求Pytest几乎是必写项。如果你打算往自动化测试方向走Pytest是绕不开的第一课。这篇文章不打算按官方文档的顺序罗列API那样太枯燥。我按自己实际做项目的路径来梳理从最基础的用例写法到fixture机制、参数化、断言、命令行用法、Allure报告集成再到项目实战中的目录结构和踩坑记录。看完你可以直接照着搭建一套属于自己的Pytest自动化测试项目。2. Pytest快速入门从安装到跑通第一个用例2.1 环境准备与安装Pytest要求Python 3.7及以上版本安装非常简单直接pip搞定pip install pytest装完验证一下版本pytest --version能看到版本号就说明环境就绪了。如果电脑里同时装了Python 2和Python 3注意用pip3或者python3 -m pip install pytest别装错环境。2.2 第一个测试用例函数式写法新建一个文件比如test_demo.py内容如下def test_add(): assert 1 1 2 def test_sub(): assert 2 - 1 1然后在终端执行pytest -v输出结果里会显示两个用例都通过。注意观察几个关键细节第一Pytest会自动识别文件名以test_开头或_test结尾的py文件在文件里自动收集函数名以test_开头的函数以及类名以Test开头的类中的test_开头的方法。这个“自动发现”机制是Pytest高效的基础。第二断言直接用Python原生的assert不需要像UnitTest那样调用assertEqual、assertTrue。Pytest会在断言失败时自动捕获表达式详细信息告诉你左边是多少、右边是多少。把test_add改成assert 1 1 3跑一下你会看到失败信息里明确显示assert (1 1) 3以及具体值。这种报告体验比UnitTest那种干巴巴的AssertionError友好得多。2.3 类式写法组织相关用例测试函数写多了以后建议用类做分组。把同一个模块、同一个接口或同一个功能的用例放在一个类里class TestOrder: def test_create_order_success(self): assert True def test_create_order_missing_param(self): assert True注意类名必须以Test开头且类里不要写__init__方法否则Pytest无法正常收集用例。这是新手最容易踩的坑之一。2.4 命令行核心参数速查日常工作高频使用的几个命令我整理成一个速查表命令作用pytest -v显示详细执行信息每个用例一行pytest -s显示print输出默认Pytest会捕获printpytest -k 关键字按名称模糊匹配用例比如-k order只跑名称含order的pytest -m 标记名按标记执行比如-m slow跑慢速用例pytest --lf只重跑上一次失败的用例pytest --co只收集用例不执行常用于排查收集问题pytest -q静默模式输出精简提示-s这个参数非常实用。调试的时候想在测试里打印一些中间变量不加-s是看不到的加了才能看到。很多人以为print没生效其实是被Pytest吞掉了。3. 断言体系Pytest的断言为什么更好用3.1 assert的语法与失败信息Pytest断言基于Python原生assert可以断言任何表达式。重点是def test_str(): name pytest assert py in name assert name.startswith(p) assert len(name) 6 def test_list(): data [1, 2, 3] assert 2 in data assert data[0] 1如果断言失败Pytest会做断言重写把失败的表达式拆解成你能看懂的信息。这一点在调试接口测试时特别重要。比如断言接口返回的JSON里某个字段时直接assert resp.json()[code] 200失败了Pytest会告诉你实际的code是多少而不是只抛一个异常。3.2 异常断言pytest.raises测试中经常需要验证“某个操作应该抛出指定的异常”。写法如下import pytest def test_zero_division(): with pytest.raises(ZeroDivisionError): result 1 / 0这种写法的好处是如果代码没有抛异常用例失败如果抛的异常类型不对用例也失败。还能提取异常信息做进一步断言def test_custom_exception(): with pytest.raises(ValueError) as exc_info: raise ValueError(param is invalid) assert invalid in str(exc_info.value)3.3 近似断言pytest.approx浮点数比较是测试里的大坑1.1 2.2 3.3在计算机里结果是False。用pytest.approx解决def test_float(): assert (0.1 0.2) pytest.approx(0.3, abs1e-6)以后做金额计算、价格校验的接口测试这个函数是保命的。4. Fixture机制Pytest最核心的武器4.1 什么是Fixture它解决什么问题先看一个没有fixture的痛点场景你有10个测试用例都要用到数据库连接或登录token如果每个用例里都写一遍初始化代码代码冗余到爆炸如果用模块级的公共变量又容易造成用例之间的数据污染。fixture就是Pytest给出的解法。fixture本质上是一个可复用的测试前置和后置逻辑。用装饰器定义函数名就是fixture名称其他用例函数把它作为参数传入即可自动调用。看例子import pytest pytest.fixture def login_token(): print(--- 获取登录token ---) token test_token_123 yield token print(--- 清理登录状态 ---)用yield分割前置和后置逻辑yield之前是setupyield之后是teardown。测试函数这样使用def test_get_user_info(login_token): assert login_token test_token_1234.2 Fixture的作用域与自动使用fixture的可选作用域有四个function默认每个用例执行前后各跑一次、class每个测试类执行一次、module每个模块执行一次、session整个测试会话执行一次。设置作用域的方式pytest.fixture(scopesession) def global_token(): token session_token yield tokenscopesession的fixture在整个测试过程中只跑一次适合那种初始化成本很高的资源比如创建数据库连接池、启动浏览器、生成全局唯一的登录状态。但要注意session级别的fixture尽量别放在测试文件里建议放在conftest.py里否则其他模块无法共享。如果你希望某个fixture对某个目录下的所有用例无条件生效不需要每个用例都写形参可以把autouseTrue加上pytest.fixture(autouseTrue) def setup_environment(): print(--- 每个用例都自动执行 ---) yield4.3 Conftest.py的用法全局共享的秘诀conftest.py是Pytest里一个特殊文件它所在的目录及子目录下的所有测试用例都可以导入里面的fixture不需要任何import语句。这正是Pytest能做到“隐式共享”的原因。一个典型接口自动化项目的conftest.py长这样import pytest pytest.fixture(scopesession) def base_url(): return http://api.example.com pytest.fixture(scopesession) def admin_token(base_url): # 实际项目里这里会发起登录请求然后返回token token get_token(base_url, admin, 123456) return token pytest.fixture(scopesession) def user_token(base_url): token get_token(base_url, user, 123456) return token以后在任意测试文件里直接写def test_order(base_url, admin_token):就能拿到值。这种隐式导入能力是UnitTest完全没有的设计。注意conftest.py的命名不能改必须是这个名字Pytest才会识别。不要在里面写测试用例它只放fixture和钩子函数。4.4 Fragment依赖与分层设计fixture是可以依赖其他fixture的。这给测试设计带来一个很大的便利你可以在不同层级组装测试前置条件。比如pytest.fixture(scopesession) def db_connection(base_url): conn create_db_conn(base_url) yield conn conn.close() pytest.fixture def created_order(db_connection, admin_token): order_id create_order_api(admin_token) yield order_id delete_order_api(admin_token, order_id)测试用例只需要def test_pay_order(created_order):不需要关心创建订单、清理订单这些细节。这些细节都被fixture链条封装掉了。这就是fixture设计的核心思想把测试前置条件的组装逻辑和测试本身的业务断言分离开。5. 参数化一条用例跑多组数据5.1 基本用法parametrize接口测试里最典型的场景一个登录接口要测合法的用户名密码、非法的用户名、非法的密码、空参数、被锁定的账号等一大堆组合。如果每个组合写一个用例测试文件会膨胀到没法看。用pytest.mark.parametrize一行代码搞定import pytest pytest.mark.parametrize(username,password,expected_code, [ (admin, 123456, 200), (admin, wrong, 400), (, 123456, 400), (admin, , 400), ]) def test_login(username, password, expected_code): result login_api(username, password) assert result[code] expected_code执行时Pytest会把参数组合展开成多条独立的用例哪一条失败哪些成功一目了然。上面这段会生成4条用例如果第二条挂了失败信息里会显示[usernameadmin, passwordwrong, expected_code400]。5.2 参数化与fixture结合造数据的进阶玩法参数化配合fixture可以做出更灵活的测试数据工厂。比如我要测试创建订单接口不同订单类型的初始化数据不同pytest.fixture def order_data(request): order_type request.param if order_type normal: return {product_id: 1001, qty: 1} elif order_type gift: return {product_id: 1002, qty: 1, is_gift: True} elif order_type pre_order: return {product_id: 1003, qty: 1, pre_order: True} pytest.mark.parametrize(order_data, [normal, gift, pre_order], indirectTrue) def test_create_order(order_data): resp create_order_api(order_data) assert resp[code] 200关键点是indirectTrue它告诉Pytest这个参数不是直接传给用例的而是先传给名为order_data的fixture由fixture加工后再传给用例。这种用法在需要根据不同数据做不同初始化逻辑的场景下非常实用。6. 标记机制给用例打标签灵活安排执行策略6.1 内置标记的用法Pytest自带一些标记最常用的有skip、skipif、xfail。skip用于无条件跳过某个用例pytest.mark.skip(reason接口暂未实现) def test_pay(): passskipif用于条件跳过import sys pytest.mark.skipif(sys.version_info (3, 10), reason需要Python3.10) def test_new_feature(): passxfail用于标记预期失败的用例适合那些已知bug但暂时没时间修的测试pytest.mark.xfail(reason已知bugBUG编号#123) def test_bug(): assert 1 26.2 自定义标记与运行控制你可以在pytest.ini里注册自定义标记[pytest] markers slow: 慢速用例 smoke: 冒烟测试然后在用例上标注pytest.mark.smoke def test_login(): pass pytest.mark.slow def test_big_data(): pass执行时用-m指定pytest -m smoke pytest -m not slow pytest -m smoke or regression不注册自定义标记直接使用Pytest会输出warning。虽然不是错误但正式项目里最好都注册一下保持规范。7. Allure报告集成让测试结果真正看得懂7.1 为什么要用AllurePytest自带的结果输出只是终端文本放在项目汇报或者持续集成展示的时候信息密度太低。Allure是一个测试报告框架能生成带分类、步骤、截图、失败详情、执行历史趋势的HTML报告。集成方式也很简单。先安装pip install allure-pytest执行测试时指定--alluredir目录pytest --alluredir./allure-results然后生成HTML报告allure generate ./allure-results -o ./allure-report --clean allure open ./allure-report7.2 Allure的关键注解想生成高质量报告光会跑命令不够还得在用例里加几个注解import allure allure.feature(订单模块) class TestOrder: allure.story(创建订单) allure.title(创建普通订单-成功场景) allure.severity(allure.severity_level.CRITICAL) def test_create_order_success(self): with allure.step(准备订单数据): data prepare_order_data() with allure.step(调用创建订单接口): resp create_order_api(data) with allure.step(校验结果): assert resp[code] 200allure.feature大模块名对应报告里的Feature分组。allure.story功能点对应Story分组。allure.title用例显示标题默认会用函数名但中文标题在报告里更直观。allure.severity严重级别有BLOCKER、CRITICAL、NORMAL、MINOR、TRIVIAL。allure.step(描述)在报告里展示每个步骤的详情失败时可以定位到具体哪一步出问题。7.3 失败截图与附件补充做UI自动化时失败截图是一个刚需。可以用以下方式把截图附加到Allure报告import allure def test_login_with_screenshot(): try: login(admin, 123456) assert True except Exception: allure.attach(driver.get_screenshot_as_png(), 登录失败截图, allure.attachment_type.PNG) raise接口自动化中也可以把请求参数、响应报文都附加到报告失败排查时就不用重新跑一遍了with allure.step(接口返回报文): allure.attach(json.dumps(resp.json(), ensure_asciiFalse, indent2), 响应, allure.attachment_type.TEXT)8. 项目实战从头搭建一个接口自动化测试项目8.1 目录结构设计我个人常用的目录结构如下参考了很多开源项目并做了简化project_root/ ├── conftest.py # 全局fixture ├── pytest.ini # Pytest配置 ├── requirements.txt ├── testcases/ │ ├── conftest.py # 测试用例级fixture │ ├── test_order.py │ ├── test_user.py │ └── test_payment.py ├── common/ │ ├── __init__.py │ ├── requests_client.py # 封装requests请求 │ ├── excel_reader.py # 读取测试数据 │ └── log.py ├── data/ │ ├── order_data.xlsx │ └── user_data.json └── reports/ ├── allure-results/ └── allure-report/关键设计要点testcases目录下按业务模块拆分测试文件一个文件对应一个模块。common目录存放所有公共封装测试用例文件里不直接写requests.get这种底层调用统一走封装的RequestsClient。data目录放测试数据文件测试用例里不硬编码数据。8.2 pytest.ini配置规范一份基础可用的pytest.ini[pytest] minversion 7.0 testpaths testcases python_files test_*.py *_test.py python_classes Test* python_functions test_* markers smoke: 冒烟测试 regression: 回归测试 slow: 慢速测试 addopts -v -s重点解释几个配置项testpaths告诉Pytest去哪找测试文件不写的话会遍历当前目录下所有文件浪费时间。addopts默认附加命令行参数-v -s能让每次执行默认就是详细模式并显示输出。python_classes、python_functions默认值就是上面的写法一般不用改。8.3 请求封装示例接口自动化测试的核心是让用例尽量简洁把HTTP细节封装到客户端里。比如import requests class RequestsClient: def __init__(self, base_url, tokenNone): self.base_url base_url self.session requests.Session() if token: self.session.headers.update({Authorization: fBearer {token}}) def get(self, path, **kwargs): return self.session.get(f{self.base_url}{path}, **kwargs) def post(self, path, **kwargs): return self.session.post(f{self.base_url}{path}, **kwargs) def close(self): self.session.close()测试用例里这样用def test_get_order_detail(client, created_order): resp client.get(f/api/order/{created_order}) assert resp.status_code 200 assert resp.json()[code] 08.4 数据驱动从Excel或JSON读取参数化数据把测试数据从代码里挪到数据文件是自动化测试走向工程化的必经一步。用pytest.mark.parametrize配合数据文件读取import json import pytest def load_user_data(): with open(./data/user_data.json, r, encodingutf-8) as f: return json.load(f) pytest.mark.parametrize(case, load_user_data()) def test_user_login(case): resp login_api(case[username], case[password]) assert resp[code] case[expected_code]data/user_data.json长这样[ {username: admin, password: 123456, expected_code: 200}, {username: admin, password: wrong, expected_code: 400} ]以后测试人员要加测试数据直接改JSON文件不需要改代码。这对团队协作非常友好。8.5 日志记录问题排查的关键测试失败后第一步永远是看日志。在common/log.py里配置一个简单的日志器import logging import datetime def setup_logger(): log_format %(asctime)s - %(levelname)s - %(filename)s - %(message)s logging.basicConfig( levellogging.INFO, formatlog_format, filenamef./reports/log_{datetime.date.today()}.log, filemodea ) return logging.getLogger(test)在用例里添加关键日志logger setup_logger() def test_order_flow(): logger.info( 开始测试订单流程 ) resp create_order_api(...) logger.info(f创建订单响应: {resp}) logger.info( 订单流程测试结束 )9. 常用插件与扩展让Pytest如虎添翼9.1 pytest-ordering控制用例执行顺序默认情况下Pytest按文件内定义的顺序执行但很多人希望跨文件或按更细粒度指定顺序。可以用pytest-orderingpip install pytest-ordering使用方式pytest.mark.run(order1) def test_a(): pass pytest.mark.run(order2) def test_b(): pass9.2 pytest-xdist并行执行提高效率用例多了以后单线程跑完可能要半小时用pytest-xdist可以开多进程并行pip install pytest-xdist pytest -n 4-n 4表示用4个进程并行执行。但注意并行执行时如果用例之间有共享资源或依赖关系很容易出问题。比如往同一个数据库里写数据或者共用同一个登录token要评估清楚再开启。9.3 pytest-rerunfailures失败用例自动重试网络不稳定导致的偶发失败用pytest-rerunfailures在指定次数内重试pip install pytest-rerunfailures pytest --reruns 3 --reruns-delay 2这个插件在接口测试里非常实用。但重试会让整体执行时间变长建议只在明确的偶发性失败场景使用测试环境本身的真实bug不要用重试掩盖。9.4 pytest-html轻量级HTML报告不想装Allure太重的话可以用pytest-html快速生成报告pip install pytest-html pytest --htmlreport.htmlAllure的功能更强大但pytest-html胜在轻量、无java依赖小项目够用了。10. 常见问题与排查经验10.1 用例收集不到提示“no tests ran”排查步骤检查文件名是否以test_开头或_test结尾。检查函数名是否以test_开头。检查类名是否以Test开头且__init__里没有写构造函数。检查pytest.ini里的testpaths是否正确指向测试目录。用pytest --co -v收集一遍看Pytest实际能发现哪些文件。10.2 Fixture参数写错导致的错误如果测试函数里写了一个fixture名称但Pytest找不到对应定义会在执行时直接报fixture xxxx not found。检查fixture是否定义在当前文件或conftest.py中conftest.py所在的目录层级是否覆盖了当前测试文件fixture名是否有拼写错误10.3 print不显示记得加-s。Pytest默认捕获print的输出只有用例失败时才显示。调试阶段建议在addopts里加-s或者直接在命令行加。10.4 断言字符串过长的显示问题接口返回JSON很长时Pytest默认会截断显示。可以在运行参数里加--tblong或者在pytest.ini中设置[pytest] addopts -v -s --tblong10.5 测试数据污染问题这是做自动化测试最容易忽视的坑。比如两个用例共用同一个数据库记录第一个用例改了状态第二个用例就挂了。解决方案每个用例的数据尽量独立用完即清理。使用fixture的yield机制做teardown。测试数据生成时带上时间戳或uuid避免硬编码同一份数据。10.6 怎么调试单个用例跑单个用例用这个命令pytest test_order.py::TestOrder::test_create_order -v格式是文件名::类名::方法名对于独立函数则是文件名::函数名。加-s就能看到函数里的print输出这是调试单个用例最顺手的组合。11. 结尾一点个人的经验总结最后分享几条我在实际项目中沉淀下来的经验。第一Pytest的学习曲线其实很短但真正用得好需要对fixture有深刻理解。刚开始不要着急扣语法细节先动手把几个简单用例跑起来然后尝试把重复的前置条件提炼成fixture慢慢就会理解这套设计的精妙之处。第二自动化测试框架选型Pytest目前确实是Python生态里最合适的选择。UnitTest作为标准库虽然稳定但开发效率和报告展示都弱于Pytest开源社区里大量的第三方插件也都是围绕Pytest生态来做的插件生态本身就是一种护城河。第三测试用例设计比框架本身更重要。Pytest只是工具用例冗余、断言不明确、数据互相污染再好的框架也救不了。建议重度使用fixture的依赖注入思想和参数化机制从结构上保证用例的独立性。第四Allure报告值得尽早接入。一份带步骤、带截图、带历史趋势的报告在项目汇报时远比一条命令行输出有说服力。实战中我发现报告是否好看直接影响团队对自动化测试的接受程度这虽然不是技术问题但也是工程推进中很实际的一个问题。希望这篇文章能帮你把Pytest真正用起来。接下来可以自己动手搭一套最小项目把fixture、参数化、Allure这些核心能力依次加进去跑通之后再逐步扩展会比自己死磕文档高效得多。
返回列表