当Pytest遇上MVC分层设计自动化用例就该这么写

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 引子 数据写在代码里,追求快速编写用例,是我设计tep的一个特点,这在个人编写时是一种非常良好的体验。但相比于HttpRunner、JMeter等来说,总觉得还差点意思。思考良久,总结为三个字:工程化。工程化是我近一年在学习Java并参与了2个测试平台模块开发,和写了几个小工具后,感受到的一种编程思想。而其中最明显的就是Spring的MVC分层设计。为了让tep更工程化,后续版本将以MVC模块编写用例为准,同时会兼容之前的脚本式快速编写。 示例 目录结构 测试用例都放在一个文件夹下: test_case:用例主程序; steps:测试步骤; data:纯粹的json; 测试用例 test_case.py是测试用例,包含的只有测试步骤: import allure from examples.tests.LoginToPayFlow.steps.step_add_cart import step_add_cart from examples.tests.LoginToPayFlow.steps.step_order import step_order from examples.tests.LoginToPayFlow.steps.step_pay import step_pay from examples.tests.LoginToPayFlow.steps.step_search_sku import step_search_sku from utils.cache import TepCache from utils.step import Step """ 测试登录到下单流程,需要先运行utils/fastapi_mock.py """ @allure.title("从登录到下单支付") def test(login, env_vars, case_vars): case_vars.put("token", login["token"]) cache = TepCache(env_vars=env_vars, case_vars=case_vars) Step("搜索商品", step_search_sku, cache) Step("添加购物车", step_add_cart, cache) Step("下单", step_order, cache) Step("支付", step_pay, cache) TepCache是“缓存”,包括global_vars、env_vars、case_vars三个级别的变量池。 Step是一个泛化调用类,作用是打日志,调用第二个参数对应的步骤函数。 测试步骤 from utils.cache import TepCache from utils.func import data from utils.http_client import request def step_search_sku(cache: TepCache): url = cache.env_vars["domain"] + "/searchSku" headers = {"token": cache.case_vars.get("token")} body = data("查询SKU.json") response = request("get", url=url, headers=headers, params=body) assert response.status_code < 400 cache.case_vars.put("skuId", response.jsonpath("$.skuId")) cache.case_vars.put("skuPrice", response.jsonpath("$.price")) from utils.cache import TepCache from utils.func import data from utils.http_client import request def step_add_cart(cache: TepCache): url = cache.env_vars["domain"] + "/addCart" headers = {"token": cache.case_vars.get("token")} body = data("添加购物车.json") body["skuId"] = cache.case_vars.get("skuId") response = request("post", url=url, headers=headers, json=body) assert response.status_code < 400 cache.case_vars.put("skuNum", response.jsonpath("$.skuNum")) cache.case_vars.put("totalPrice", response.jsonpath("$.totalPrice")) 步骤函数以step开头,尤其注意的是cache借助Python Typing提示,可以在编写下面代码时,获得PyCharm语法提示,所以一定不要忘了加上: TepCache: ...

2026-01-16 · 3 分钟 · 492 字 · 测试开发刚哥 · 原创项目 · #Tep

关于版本质量的思考V2.0

本文来聊一下缺陷逃逸率,上线后发现缺陷数 / 有效缺陷总数,也就是漏测分析。

原来Python函数只是个对象

写Python越多,写函数越多,于是乎有人觉得Python是函数式语言,其实不然,Python只是从函数式语言中借鉴了一些好的想法而已。

Java官方笔记12异常

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 Exception Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program’s instructions. the checked exception 比如,java.io.FileNotFoundException the error 比如,java.io.IOError the runtime exception 比如,NullPointerException error和runtime exception又叫做unchecked exception Catching and Handling Exceptions try { code } catch and finally blocks . . . try { } catch (ExceptionType name) { } catch (ExceptionType name) { } 使用|catch多个: ...

tep支持pytest-xdist分布式执行用例及合并Allure报告

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 tep近期更新频率较快,一方面是作者在积极投入到tep工具开发中;另一方面是我们聚集了20位小伙伴,一起合力打造EasyPytest测试平台,teprunner的FastAPI升级版本,依托于tep,帮你高效管理pytest测试用例。陆续也会有平台开发日志发布,欢迎持续关注。 tep0.11.1版本升级内容: 1、pytest-xdist分布式执行用例,合并Allure报告; 2、global_vars全局变量配置; 分布式执行用例 借助于pytest-xdist,在命令行执行时添加参数-n auto: pytest -n auto pytest-xdist会自动根据本地机器硬件配置,设置最优并发,并分发用例,分布式执行。 测试时先运行main.py,然后在case100目录下执行pytest -n auto --tep-reports: 第一次串行,第二次xdist并行: 执行时间从50s一下降到5s,性能提升还是非常的明显。 合并Allure报告 pytest-xdist分布式执行,只要把allure源文件,也就是那一堆json文件,存到同一个目录下,报告的数据就是一体的,不需要单独合并。但是有个问题,tep封装了--tep-reports 命令行参数一键生成Allure报告,背后的逻辑是在pytest_sessionfinish hook函数里面实现的,分布式执行时,每个xdist的node都是一个单独的session,多个node就会生成多份报告: c 10个node生成了11份报告,其中1份master节点生成的。pytest-xdist的原理如图所示: master节点不运行任何测试,只是通过一小部分消息与节点通信。子节点执行后会通过workeroutput把数据回传给master节点。所以只需要通过是否有workeroutput属性来判断master节点: def _is_master(config): """ pytest-xdist分布式执行时,判断是主节点master还是子节点 主节点没有workerinput属性 """ return not hasattr(config, 'workerinput') 然后只在主节点的pytest_sessionfinish,生成1次报告,就能避免生成多份报告。这样在xdist分布式执行模式下,--tep-reports也只会生成1份合并后的包含所有测试用例的Allure HTML报告。 完整实现代码: import time import shutil import allure_commons from allure_commons.logger import AllureFileLogger from allure_pytest.listener import AllureListener from allure_pytest.plugin import cleanup_factory reports_path = os.path.join(Project.root_dir, "reports") allure_source_dir_name = ".allure.source.temp" allure_source_path = os.path.join(reports_path, allure_source_dir_name) def _tep_reports(config): """ --tep-reports命令行参数不能和allure命令行参数同时使用,否则可能出错 """ if config.getoption("--tep-reports") and not config.getoption("allure_report_dir"): return True return False def _is_master(config): """ pytest-xdist分布式执行时,判断是主节点master还是子节点 主节点没有workerinput属性 """ return not hasattr(config, 'workerinput') def pytest_addoption(parser): """ allure测试报告 命令行参数 """ parser.addoption( "--tep-reports", action="store_const", const=True, help="Create tep allure HTML reports." ) def pytest_configure(config): """ 这段代码源自:https://github.com/allure-framework/allure-python/blob/master/allure-pytest/src/plugin.py 目的是生成allure源文件,用于生成HTML报告 """ if _tep_reports(config): if os.path.exists(allure_source_path): shutil.rmtree(allure_source_path) test_listener = AllureListener(config) config.pluginmanager.register(test_listener) allure_commons.plugin_manager.register(test_listener) config.add_cleanup(cleanup_factory(test_listener)) clean = config.option.clean_alluredir file_logger = AllureFileLogger(allure_source_path, clean) # allure_source allure_commons.plugin_manager.register(file_logger) config.add_cleanup(cleanup_factory(file_logger)) def pytest_sessionfinish(session): """ 测试运行结束后生成allure报告 """ if _tep_reports(session.config): if _is_master(session.config): # 只在master节点才生成报告 # 最近一份报告的历史数据,填充allure趋势图 if os.path.exists(reports_path): his_reports = os.listdir(reports_path) if allure_source_dir_name in his_reports: his_reports.remove(allure_source_dir_name) if his_reports: latest_report_history = os.path.join(reports_path, his_reports[-1], "history") shutil.copytree(latest_report_history, os.path.join(allure_source_path, "history")) current_time = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(time.time())) html_report_name = os.path.join(reports_path, "report-" + current_time) os.system(f"allure generate {allure_source_path} -o {html_report_name} --clean") shutil.rmtree(allure_source_path) global_vars全局变量 在使用环境变量模版时,有些变量需要在多个模版重复设置,tep新增了global_vars全局变量fixture,可以将重复设置的变量,定义在resources/global_vars.yaml: ...

2026-01-15 · 2 分钟 · 220 字 · 测试开发刚哥 · 原创项目 · #Tep

深度剖析Python字典和集合(下)

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 集合 集合的字面量是{1}、{1, 2},和字典有点像,不同的是集合只有值没有键。 set构造方法 set构造方法如下: my_set = {1, 2, 3} my_set = set([1, 2, 3]) 前者比后者更快更易读,因为对于前者,Python会利用一个专门的叫做BUILD_SET的字节码来创建集合。而对于后者,Python必须先从set这个名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。 {}是空字典,空集合必须用set()。 集合也有集合推导: >>> my_set = {x for x in range(1, 4)} >>> my_set {1, 2, 3} >>> type(my_set) <class 'set'> 集合还有个不可变类型叫做frozenset,如同元组之于列表,它的构造函数如下: >>> frozenset(range(10)) frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 只有这一种方法,没有字面量。 集合的操作 集合的本质是许多唯一对象的聚集,它最常见的操作是用来去重: >>> my_list = [1, 2, 2, 3] >>> set(my_list) {1, 2, 3} >>> list(set(my_list)) [1, 2, 3] 除此之外,集合还能进行数学运算,比如求交集、并集、差集等。示例,求needles的元素在haystack里出现的次数,暴力解法: found = 0 for n in needles: if n in haystack: found += 1 集合解法: found = len(needles & haystack) 即使不是set也能先转换为set: found = len(set(needles) & set(haystack)) ## 另一种写法 found = len(set(needles).intersection(haystack)) 集合支持的操作如下: 散列表揭秘 这一部分的内容略微有点硬,请注意提前喝点水! 从上篇的简介可以知道,散列表就是一张表,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录。散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组),散列表里的单元叫作表元,在dict的散列表中,每个键值对占用一个表元,每个表元有两个部分,一个是对键的引用,另一个是对值的引用,因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。 为什么要用稀疏数组?举个例子,身份证号411697199702076425,如果把它作为键存储到数组中,虽然能用O(1)时间就找到,但是需要开辟一个999999999999999999大的空间。假如只有1的空间,就只能把最后一位作为键存储到数组中,多个身份证号的键就容易冲突,得多看n位才能找到,要用O(n)时间。空间既不能太大,也不能太小,需要结合时间,在两者之间产生一个平衡,即空间和时间的平衡,所以要用稀疏数组! Python会设法保证大概还有三分之一的表元是空的,用空间换时间,提高散列表查询效率。如果剩余空间不足,原有的散列表会被复制到一个更大的空间里面。 散列表的键值,又称为散列值,Python中可以用hash()方法来计算所有内置类型对象的散列值。 自定义类型实际上调用的是自定义的__hash__。 如果两个对象在比较的时候是相等的,那么它们的散列值必须相等,否则散列表就不能正常运行了: >>> a = 1 >>> b = 1 >>> a == b True >>> hash(a) 1 >>> hash(b) 1 >>> a = "x" >>> b = "x" >>> a == b True >>> hash(a) 706802421 >>> hash(b) 706802421 越是相似但不相等的对象,它们的散列值的差别越大: ...

性能测试指标TP99,比平均响应时间更需要关注

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 什么是TP99? TP是Top Percentile的首字母缩写,TP99是指“前百分之九十九”,用来表示满足百分之九十九的网络请求所需的最低耗时。 该怎么理解?举个简单例子,下图是100次请求的响应时间走势图: 第23次请求的响应时间为600ms,其他请求的响应全部为1ms,这个走势图也就是常说的“尖刺”。我们来计算一下: ①平均响应时间 平均响应时间 = 所有请求响应时间之和 / 请求次数,等于6.99ms ②TP99 TP99 = 把响应时间从小到大排列,取第99%那个位置的值,等于1ms 为什么TP99更需要关注? 平均响应时间受极端值的影响较大,因为出现1个尖刺,平均响应时间几乎是TP99的7倍! TP99更能反映大多数请求的情况,通过TP99为1ms可以知道,应用系统对于大多数请求,都能在1ms内响应,整体性能达标。而对于尖刺,我们可以通过走势图的MAX来发现,单独进行分析。 在JMeter的Aggregate Report中,有3个字段:90% Line、95% Line、99%Line 其实就是指TP90、TP95、TP99。此外,在实际工作中,我们还会关注TP999,三个九,也就是99.9%。关注TP多少,取决于我们对性能的要求怎么样,我们希望应用系统满足多少请求。如果对性能要求不高,关注TP90就够了,允许10%请求出现波动。如果对性能要求很高,则需要关注TP999,99.9%请求都能够快速响应。

知识卡片,在技术学习中的运用

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 一、引言 技术学习有个特点,看完就忘,再看再忘,还看还忘。艾宾浩斯遗忘曲线,解释了这一现象,学习后的20分钟内,记忆的遗忘速度最快。1个小时后,遗忘速度开始减慢。1天后,遗忘速度显著降低。一段时间后,记忆会变得相对稳定。 为了能够记忆技术知识点,我们可以尝试理解技术逻辑,定时复习,主动回忆。通常的做法是将原文摘抄下来,回顾时再读一遍。缺点在于,技术逻辑与文学不同,每次都要烧脑,无法唤醒记忆。 二、什么是知识卡片 知识卡片是我在《高效学习法》学到的笔记技巧。简单来说,知识卡片就是把知识“画”在一张卡片上,重点是“画”。 (图片来源于《高效学习法》) 三、为什么需要知识卡片 知识呈现形式,影响着阅读理解难度,图形>表格>文字。技术逻辑是一个思考过程,想通了以后,在我们脑海中会浮现出一副图像,可能是流程图、数据流、树状图等。如果每次复习都能看到这幅图像,那么复习效率将会显著提升。很有必要,将这幅图像刻画出来。 四、怎么制作知识卡片 我们可以找个本子,用笔进行绘画,但是相对于纸质笔记,我更推荐使用电子笔记,便于存储,多设备阅读和分享。 知识卡片,可以使用常见的办公软件来制作,PowerPoint,俗称PPT。 先做好知识卡片分类,将同一类知识卡片放在同一个文件里面,创建多张幻灯片。 画图时随意发挥,只要能把技术逻辑描绘出来,自己能看懂,方便理解即可。 五、我的知识卡片分享 ①流量录制回放 ②消息队列 ③ Java 六、总结 知识卡片,是一种视觉化的笔记方法,便于理解和记忆。学而不思则罔,思而不学则殆。知识卡片特别适合技术学习,温故而知新。感兴趣的小伙伴,动手画起来吧。

AI安全

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 10.1 AI安全风险的来源和分类 系统风险:AI模型软件的供应链风险、暴露面风险以及算力劫持风险。 网络风险:面向公网的入侵攻击,以及内网的隔离风险。 身份风险:对非人类身份的管控,越权访问,身份冒充等。 数据风险:Agent模型训练时的数据投毒,以及输入/输出阶段的敏感信息泄露等。 模型风险:Agent模型输入输出内容的恶意诱导、提示词攻击等风险。 应用风险:当AI在线上提供服务时,会面临Web入侵、DDoS攻击导致服务不可用等风险。 10.2 保护应用安全 10.2.1 背景和挑战 攻击者不再需要直接寻找暴露在公网服务中的漏洞,他们只需操纵相关的AI Agent,就能让这些AI Agent代为攻击内外部的弱电服务。 服务器端请求伪造(SSRF)是引入AI Agent后最突出的威胁。攻击者仅需通过自然语言提示,即可诱导受信任的Agent向内部网络发起恶意请求,窃取云环境凭证或扫描内网资产。Agent也可能被攻击者诱导产生包含SQL注入或XSS等恶意载荷,攻击与之交互的后端服务。 10.3.2 新的攻击面 构造恶意的语言诱导,让AI Agent变为一个无节制的资源消耗工具,使其无法正常为其他用户提供服务。构造复杂度炸弹式的指令,诱使AI Agent陷入生成海量文本的陷阱,生成数万乃至数十万的Token,导致其自身的计算成本飙升。 攻击者也可以攻击AI Agent调用的外部API,高频或海量触发复杂或昂贵的API调用。攻击者还能将恶意指令隐藏在AI Agent将要处理的网页、表格、PDF等外部数据中,当AI Agent处理这些被污染的数据时,隐藏的指令就会被执行。 10.3.3 防护思路 沿用并强化传统的应用安全最佳实践,同时实施严格的资源治理策略,针对工作流劫持类攻击,则需要设计更加完善的上下文隔离机制。 10.3.4 解决建议 一方面,对在AI场景下输入输出做安全检测的AI安全护栏方案,以及相关AI应用开发平台和网关对AI安全护栏的集成。另一方面,由于AI Agent进一步打破了传统的攻击面和网络边界,要更加关注纵深防御、动态检测、运行时安全等多种安全措施的组合。 例如当AI Agent未经过WAF或防火墙直接在内网发起恶意请求时,可以通过RASP等运行时检测方案或VPC防火墙来做防护,当AI Agent在用各种方式泄露敏感数据时,可以通过API安全、NDR等流量安全方案,从敏感数据流转的角度去及时发现风险。 10.3 保护模型安全 10.3.1 背景与挑战 输入层威胁:包括对抗样本攻击(如通过微调图像/音频诱导多模态Agent误判)、提示词注入的变种攻击(如上下文分割攻击、语义混淆攻击),以及通过恶意文件(如PDF隐写术)触发供应链漏洞。 推理层威胁:涵盖模型越狱导致的伦理失控、RAG知识库的定向爬取引发的数据资产泄露,以及函数调用劫持(如篡改API参数执行未授权操作)。 输出层威胁:设计生成式钓鱼内容(如伪造银行通知)、模型幻觉在医疗/金融场景的致命误导,以及通过隐写术在AIGC内容中植入隐蔽指令。 10.3.2 防护机制 内容合规审核,提示词攻击防御,敏感信息防护,恶意文件检测,恶意URL拦截,提示词反爬机制,模型越狱检测,模型幻觉抑制,数字水印标识。 10.3.3 模型安全的未来 自定义检测Agent可满足不同行业客户的特定业务风险识别需求。它引入了专用算法模型,并支持多行业多场景灵活配置,从而实现从通用到专属的安全检测能力升级。 10.4 保护数据安全 10.4.1 背景和挑战 10.4.2 防护框架 打造“平台可靠、链路可信、数据可控、自主可选、操作可审、责任可追”的数据安全保障体系。 10.4.3 构建全数据全生命周期的安全保障 1、数据收集 (1)数据来源:用户上传类数据、模型文件和推理数据、运行日志类数据。 (2)数据分类:以阿里云数据安全中心为例,从数据价值、敏感性、数据合规和业务需求等多角度将数据分为4个安全级别:S1、S2、S3、S4。 (3)数据脱敏 (4)数据去毒:对自有的存储资源中多种违规内容(例如涉黄、涉政、暴力、违法等)进行实时监控、检测和拦截。 2、数据传输 (1)面向数据传输提供私有网络加密 ...

Java持久层框架Mybatis入门

本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。 MyBatis是什么 MyBatis是Java的持久层框架,GitHub的star数高达15.8k,是Java技术栈中最热门的ORM框架之一。它支持自定义SQL、存储过程以及高级映射,可以通过XML或注解来配置和映射原始类型、接口和Java POJOs为数据库中的记录。 POJOs,Plain Old Java Objects,纯的传统意义的Java对象,最基本的Java Bean只有属性加上属性的get和set方法。 安装 Maven pom.xml: <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>x.x.x</version> </dependency> 版本查询:https://mvnrepository.com/artifact/org.mybatis/mybatis SqlSessionFactory SqlSessionFactory的实例是每个MyBatis应用的核心,通过SqlSessionFactoryBuilder创建,能基于XML配置,也能使用Configuration类。 基于XML配置: String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 实际项目中不会手动设置XML文件路径,而是直接读取classpath去找XML。 使用Configuration类: DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); 由于Java注解的一些限制以及某些MyBatis映射的复杂性,MyBatis会优先选择XML配置。在上面的示例中,MyBatis会根据classpath和BlogMapper.class去找BlogMapper.xml。 ...