暂时未有相关云产品技术能力~
Linux 1.环境配置 1.1环境准备 Xshell(下载地址:https://xshell.en.softonic.com/) VMware Workstations(下载地址:https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html) Linux镜像(下载地址:https://developer.aliyun.com/mirror/) 选择centos 找到下载地址的链接,点进去 选择你想要下载的版本,我们这边选择7.0版本的 随后,我们选择isos/这个目录,再进入x86 64/ 在下载列表中,我们看见有很多种iso文件,这里不同名称代表不同类型的系统,例如DVD为标准版镜像,Everything为集成了各种插件的镜像文件,这里我们学习的话,下载DVD-2009.iso即可 1.2配置VMware 新建虚拟机 选择如下 点击稍后安装镜像文件 选择Linux,以及选取你在阿里云镜像官网下载的Linux版本 定义虚拟机名称、以及保存的位置 虚拟机处理器配置,默认即可 虚拟机内存配置,默认即可 网络使用NAT模式 后面的操作均为默认即可,都是一些默认的配置,不建议修改,这里不再赘述 最终出现如下即可,点击完成 最后点击生成好的虚拟机,右键--》设置,找到CD/DVD(IDE),点击使用ISO镜像文件,选择你刚刚下载好的Linux镜像文件即可 最后启动虚拟机即可、后续的虚拟机的安装与配置,可以参考以下优秀文章 ps:如果你不想安装Xshell,可以在安装时选择图形化界面,如果选择安装Xshell,就建议不安装图形化界面,可以降低资源占用,实际企业中一般不安装图形化界面 https://www.cnblogs.com/fuzongle/p/12769811.html 2.连接Linux 获得Linux的IP地址,进入Linux系统中,右键打开终端,输入命令ifconfig,即可拿到IP地址 新建Xshell会话,输入IP地址,端口号默认22 3.Linux学习 1.常见的Shell Bourne Shell Bourne Again Shell(“bash”,常用) C Shell K Shell Shell for Root 实际操作演示如何使用bash (1). vim hello.sh(新建一个sh文件) (2) #!bin/bash(定义使用何种shell) echo "hello linux!"(输出语句) (3):wq(保存退出) (4)./hello.sh (执行会报权限错误) (5)chmod +x hello.sh(添加执行权限) (6)./hello.sh(再次执行,成功) 2.常见文件命令 ls:列出当前目录所有文件 -a:显示隐藏文件 -l:显示文件详细信息 ps:蓝色表示为文件夹,白色表示为文件 cd:切换目录 pwd:显示当前的目录 mkdir:新建一个空目录 -p:递归创建目录 rmdir:删除一个空目录 -p 路径:递归删除目录 cp:复制文件或者目录 -r:若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。 rm:移除文件或目录 -rf:递归删除,忽略不存在的文件,不会出现警告信息(危险命令,三思) mv:移动文件或目录,修改文件、文件夹名称 实际操作如下: 3.文件属性,想要查看文件的属性,使用命令ls -l d表示此为目录文件,r代表可读,w表示可写,x表示可执行,分为三部分,第一部分表示创建该文件夹的用户,第二部分表示和创建此文件夹在同一个组的成员,第三部分表示其他的用户 4.chmod(修改文件、文件夹权限) r的权限表示为4,w的权限表示为2,x的权限表示为1 实例:修改一个文件权限为全部人可读可写可执行,则表示为4+2+1,4+2+1,4+2+1,即777 vim test.txt chmod 777 test.txt 5.netstat(网络监听命令) -a:显示所有选项. -t:仅显示tcp相关选项 -u:仅显示udp相关选项 -p:显示建立相关链接的程序名 -l:仅列出有在 Listen (监听) 的服务状态 -n:不显示别名,能显示数字的全部转化成数字。 netstat -lnpt 6.显示性能与进程命令 top ps -aux 显示所有用户进程 7.开关机操作 关机操作之前最好先使用sync将虚拟机的数据同步到磁盘中,控制台输入sync即可 shutdown:正常关机 shutdown -h 10:十分钟后关机 shutdown -h now:立刻关机 shutdown -h 23:00:今日23:00准时关机,相当于定时关机 shutdown -h +10:十分钟后关机 reboot:重启 8.Linux系统目录的学习 /bin:bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令。 /boot:这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。 /dev :dev 是 Device(设备) 的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的。 /etc:etc 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件和子目录。 /home:用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve。 /lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。 /lost+found:这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。 /media:linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。 /mnt:系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。 /opt:opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。 /proc:proc 是 Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。 /root:该目录为系统管理员,也称作超级权限者的用户主目录。 /sbin:s 就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序。 /tmp:tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。 /usr: usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 program files 目录。 /usr/bin:系统用户使用的应用程序。 9.chgrp(更改文件属组) -R:更改文件属于哪个组 10.chown(更改文件属主) -R:更改文件属于哪个用户 11.文件内容查看 cat:从第一行开始看 cat /etc/redhat-release:查看当前系统版本号 tac:从最后一行开始看 nl+文件名:显示行号 more+文件名:翻页的查看,不是一次性全部显示出来(空格翻页,enter进入下一行) less+文件名:与more相似,可以向前翻页(空格翻页。上下键翻页) head:只看头几行 -n + 数字:只看头XX行 ?+内容:寻找指定内容(n:查找下一个,N:查找上一个) tail:只看尾几行 :set nu:在文件中显示行号 12.touch(创建文件) touch:新建一个空白文件 13.ln(软、硬链接) ln(硬链接):以文件副本的形式存在,不占用实际空间(不允许给目录创建硬链接) ln-s(软链接):相当于快捷方式 区别:源文件删除后,硬链接依然还可以访问源文件,相当于备份了,而软连接则会由蓝色变成红色,无法访问源文件,使用ll命令,可以很好的看出硬链接和软链接的区别,软链接是蓝色的,且有->>标记 14.Vim(文本编辑) Vim的三种模式:命令模式、输入模式、底线命令模式 vim:新建一个文档文件,并进入 在文档中输入i:进入输入模式 ESC:退出输入模式,进入命令模式 :set nu:设置行号,输入:即可进入底线命令模式 :wq:保存并退出 vim命令大全:https://www.runoob.com/linux/linux-vim.html 15.useradd(添加用户) -m:自动创建用户主目录 -d:指定用户目录地址 -G:指定用户所在组 ps:以下操作均在管理员账号下操作 1.管理员给用户添加密码:passwd +用户名 2.普通用户自己设置密码:passwd passwd -l +用户名:锁定用户 passwd -d+用户名:删除用户的密码 16.userdel(删除用户) -r:删除用户并删除其目录 17.usermod(修改用户) -d:指定用户目录地址 18.su(切换用户) su + 用户名:切换到指定用户 19.logout(登出) logout:退出当前用户,回退到上一个用户 20.hostname(修改虚拟机主机名) hostname 用户名:修改当前虚拟机主机名,重启生效(主机reboot,ssh远程连接则重连) 21.groupadd(添加用户组)* -g:指定ID,不指定则为自增,详情查看/ete/group groupadd +用户名:新增一个组 22.groupdel(删除用户组) groupdel +用户名:删除一个指定的组 23.groupmod(修改用户组) -g:修改组ID -n:修改组名 24.df -h(显示磁盘使用量) 25.du(显示每个文件占用的空间) 26.mount(挂载) 类似于windows上的U盘的安装,把外部文件挂载到指定的目录下 mount [外部设备] [指定目录] 27.umoutn(卸载挂载) 类似于windows的U盘的退出 -f +位置:强制卸载 28.ps(显示当前进程的状态) -a:列出所有的进程 -aux :显示所有包含其他使用者的行程 -ef:查看父进程信息 常搭配管道符进行使用,例如查找一个mysql进程相关:ps -aux|grep mysql 进程树展示:pstree -pu 杀进程:kill -9 进程ID,相当于结束任务 29.Linux安装Java环境 rpm安装:http://www.oracle.com/technetwork/java/javase/downloads/index.html 卸载OpenJDK #检查是否存在jdk rpm -qa|grep jdk #如果存在则执行以下卸载命令 rpm -e --nodeps [jdk版本号] 将下载好的rpm文件,通过xftp上传到linux系统下 cd进入安装目录,执行命令 rpm -ivh jdk-8u221-linux-x64.rpm 配置java的环境变量 cd /etc/profile JAVA_HOME=/usr/java/jdk1.8.0_221-amd64 CLASSPATH=%JAVA_HOME%/lib:%JAVA_HOME%/jre/lib PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin export PATH CLASSPATH JAVA_HOME #保存后,刷新资源 source /etc/profile #测试是否安装成功 java -version 30.Linux安装Tomcat 官网下载Tomcat,切记是下载Linux版本,下载地址:https://tomcat.apache.org/download-90.cgi 解压Tomcat,通过xftp上传到Linux服务器,解压出来(tar -zxvf [tomcat压缩包名称]) 进入tomcat 的bin目录 防火墙操作 #查看防火墙的8080端口是否打开,因为tomcat默认端口是8080 # 查看全部信息 firewall-cmd --list-all # 只看端口信息 firewall-cmd --list-ports #开启80端口,如果是在云服务器,在安全组开启8080端口即可 firewall-cmd --zone=public --add-port=8080/tcp --permanent #重启防火墙,让他生效 systemctl restart firewalld.service #tomcat启动 ./startup.sh #tomcat停止 ./shutdown.sh 31.Linux安装Docker 1.查看是否已经安装了docker yum list installed | grep docker #如果已经安装了则卸载 sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine 2.yum安装gcc相关 yum -y install gcc yum -y install gcc-c++ 3.安装需要的软件包 yum install -y yum-utils device-mapper-persistent-data lvm2 4.设置stable镜像仓库 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 5.更新yum软件包索引 yum makecache fast 6.安装Docker CE yum -y install docker-ce docker-ce-cli containerd.io 7.启动docker systemctl start docker 8.测试docker是否安装成功 docker version docker run hello-world docker images 总结 Linux是测试必不可少的一门技术,以上是我学习Linux过程中记录的笔记,有想法的小伙伴可以在下面留言与我互动哦~:drooling_face:
(一) 性能测试需求分析 性能测试需求分析与传统的功能测试需求有所不同 功能测试需求分析:重点在于分析被测系统的功能是否满足产品功能需求规格(正向、逆向) 性能测试需求分析:重点在于分析被测系统是否能满足特定的业务需求场景(时间、资源) 需要从业务场景、程序代码、服务器、硬件配置等多个维度分析系统可能存在性能瓶颈1、获取有效的需求 1、客户方提出能够提出明确需求的一般是金融、银行、电信、医疗等企业,他们一般对系统的性能要求高,并且对性能也非常了解2、根据历史数据分析通过分析历史运营数据收集用户信息,如:#注册用户数、日活、月活,计算用户的增长速度#每月、每周、每天的峰值业务量是多少#用户频繁使用的功能模块是哪些2、 性能测试点的提取 用户频繁使用的业务功能 非常关键的业务功能 特殊交易日或峰值交易的业务功能 核心业务发生重大调整的业务功能 资源占用非常高的业务功能 商城性能测试点的提取 3、 确定性能测试目标 性能测试目标包括: 确定核心业务功能的TPS 对业务流程(多接口组合)进行压测 系统能在实际系统运行压力的情况下,稳定的运行24小时 (二)制定性能测试计划 1、测试背景 商城是公司新开发的一个电商项目,为了保证项目上线后能够稳定的运行,且在后期推广中能够承受用户的增长,需要对项目 进行性能测试 2、测试目的 对新电商项目进行性能测试的核心目的包括: 确定核心业务功能的TPS 对业务流程(多接口组合)进行压测 系统能在实际系统运行压力的情况下,稳定的运行24小时 3、测试范围 通过对性能测试需求的调研和分析,确定被测系统的测试范围如下 4、测试策略 4.1 基准测试 先做基准测试,确定估算的标准。 4.2 负载测试 通过逐步增加系统负载,测试系统性能的变化,并最终确定在满足系统的性能指标情况下,系统所能够承受的最大负载量的 测试。 分别模拟5、10、30、50、100个用户对系统进行负载测试,查看不同并发时系统软件各项指标是否符合需求。 4.3 稳定性测试 用200用户对系统进行7*24小时的不间断稳定性测试,查看服务器日志内有无异常和报错;系统软件各项指标中间有无异常波动;是否存在内存溢出之类的问题。 验证系统长期运行的稳定性以及是否存在内存溢出之类的问题5、风险控制 6、交付清单 性能测试计划、测试脚本、性能缺陷统计和性能测试报告等。7、进度与分工 (三)性能测试用例设计 (四)建立测试环境 在进行性能则试之前,需要先完成性能测试环境的搭建工作,测试环境一般包括硬件环境、软件环境及网络环境一般情况下可以要求运维和开发工程师协助完成 性能测试环境的特点 性能测试对测试环境的独立性要求更高,更为严格 如果某环境下运行多个系统,就很难判断其中的某个环境对资源的占用情况 尽量保持性能测试环境与真实生产环境的一致性 如何保证测试环境与生产环境的一致性 硬件环境:包括服务器环境、网络环境等软件环境:版本一致性:包括操作系统、数据库、被测应用程序、第三方软件等配置一致性:包括操作系统、数据库、被测应用程序、第三方软件等使用场景的一致性:基础业务数据的一致性业务操作模式的一致性:尽量模拟真实场景下用户的使用情况 构造测试数据 压测环境中的数据量尽量与生产环境中数据量一致,为了快速创建大量数据,可以直接操作数据库进行添加案例:通过编写Python脚本,构造10万条商品记录 import pymysql conn = pymysql.connect(host="127.0.0.1", user="litemall", password="litemall123456", database="litemall", port=3306) cursor = conn.cursor() goods_sql = """ INSERT INTO `litemall_goods` (`id`, `goods_sn`, `name`, `category_id`, `brand_id`, `gallery`, `keywords`, `brief`, `is_on_sale`, `sort_order`, `pic_url`, `share_url`, `is_new`, `is_hot`, `unit`, `counter_price`, `retail_price`, `detail`, `add_time`, `update_time`, `deleted`) VALUES ('{}', '{}', '小米手机-{}', '1008016', '1001000', '[\"http://182.92.81.159:8080/wx/storage/fetch/0b5lpf15ee8c6o10vkd8.jpg\",\ "http://182.92.81.159:8080/wx/storage/fetch/ykaptr4vntbofoi9l2f5.jpg\",\"http://182.92.81.159:8080/wx/storage/fetch/u5zc8sp2t4f9y9uw n179.jpg\"]', '手机,Android', '小米10 双模5G 骁龙865 1亿像素8K电影相机', '1', '100', 'http://182.92.81.159:8080/wx/storage/fetch/re5jul69plklzusso97 t.png', '', '1', '0', '个', '100.00', '99.00', '<p>小米10 双模5G 骁龙865 1亿像素8K电影相机 对称式立体声 8GB+256GB 冰海蓝 拍照智能游戏手机 !!!!!!!!!!</p>', '2020-03-11 14:19:50', '2020-03-26 14:55:58', '0'); """ goods_attr_sql = """ INSERT INTO `litemall_goods_attribute` (`goods_id`, `attribute`, `value`, `add_time`, `update_time`, `deleted`) VALUES ('{}', '产地', '中国山东', '2018-10-26 21:27:13', '2018-10-26 21:27:13', '0'), ('{}', '尺寸', '200*230cm/ 220*240cm', '2018-10-26 21:27:13', '2018-10-26 21:27:13', '0'), ('{}', '颜色', '条纹绿格', '2018-10-26 21:27:13', '2018-10-26 21:27:13', '0'), ('{}', '执行标准', 'GB/T 22844-2009', '2018-10-26 21:27:13', '2018-10-26 21:27:13', '0'); """ goods_product_sql = """ INSERT INTO `litemall_goods_product` (`goods_id`, `specifications`, `price`, `number`, `url`, `add_time`, `update_time`, `deleted`) VALUES ('{}', '[\"标准\"]', '999.00', '1000', 'http://182.92.81.159:8080/wx/storage/fetch/o4531ja3h9sgq5ib32f4.jpg', '2020-03-11 14: 19:50', '2020-03-11 14:19:50', '0'); """ goods_spec_sql = """ INSERT INTO `litemall_goods_specification` (`goods_id`, `specification`, `value`, `pic_url`, `add_time`, `update_time`, `deleted`) VALUES ('{}', '规格', '标准', '', '2020-03-11 14:19:50', '2020-03-11 14:19:50', '0'); """ goods_id_start = 200000 for i in range(100000): # 商品 goods_id = goods_id_start + i print("i={} goods_id={}".format(i, goods_id)) sql = goods_sql.format(goods_id, goods_id, goods_id) cursor.execute(sql) # 商品参数 sql = goods_attr_sql.format(goods_id, goods_id, goods_id, goods_id) cursor.execute(sql) # 商品货品 sql = goods_product_sql.format(goods_id) cursor.execute(sql) # 商品规格表 sql = goods_spec_sql.format(goods_id) cursor.execute(sql) conn.commit() cursor.close() conn.close() (五)执行测试脚本 先保证脚本调试通过之后,才能进入正式压测阶段。分布式执行如果单台压测机的并发量不能够满足要求,则可以通过分布式压测来提高并发量。 JMeter工具支持分布式压测,即多台机器同时执行同一个脚本,然后统计结果。分布式压测条件分布式压测,机器分为控制机和执行机,有几个条件必须满足:执行机和控制机必须在同一个网段之内压测机也必须安装相同版本的JMeter和JDK修改JMeter配置信息 修改控制机jmeter.properties配置文件中的 remote_hosts=xx ,xx表示压力机的IP和端口,多个用英文逗号分隔 修改执行机jmeter.properties配置文件中的 server_port=1099 ,端口号要和控制机配置文件中保持一致2.3 启动执行机打开jmeter目录\bin\jmeter-server.bat文件进行启动2.4 启动控制机打开jmeter目录\bin\jmeter.bat,添加执行脚本,通过运行-->远程启动或者远程全部启动,即可启动对应执行机 (六)性能测试监控 性能测试监控关键指标 系统指标:系统指标则与用户场景及需求直接相关并发用户数:某一物理时刻同时向系统提交请求的用户数平均响应时间:系统处理事务的响应时间的平均值。对于系统快速响应类页面,一般响应时间为3秒左右吞吐量 服务器资源指标:资源指标与硬件资源消耗直接相关CPU使用率:一般可接受上限为85%内存利用率:一般可接受上限为85%磁盘I/O网络带宽 Java应用:JVM监控:JVM内存、Full GC频率 数据库:慢查询缓存命中率数据池连接数mysql锁 压测机资源:CPU内存网络磁盘空间 性能监控工具要对性能测试指标进行监控,可以使用系统自带的监控工具,也可以使用第三方监控工具或者监控平台。 系统指标:通过性能测试工具(如LoadRunner、JMeter等)以图形化方式监控 服务器资源指标:使用Jmeter性能监控插件PerfMon 进行监控使用Linux命令监控:top、free、vmstat、sar、iostat等Nmon:全面监控linux系统资源使用情况,包括CPU、内存、I/O等,可独立于应用监控。 Java应用:jvisualvm 数据库 压测机资源:Windows自带“任务管理器” 1、MySQL监控 慢查询SQL 慢查询:指执行速度低于设置的阀值的SQL语句 作用:帮助定位查询速度较慢的SQL语句,方便更好的优化数据库系统的性能开启MySQL慢查询日志参数说明: slow_query_log: 慢查询日志开启状态[ON:开启,OFF:关闭] slow_query_log_file: 慢查询日志存放位置 long_query_time: 慢查询时长设置(超过该时长才会被记录,单位:秒) mysql> show variables like 'slow_query%'; +---------------------+---------------------------------------------------+ | Variable_name | Value | +---------------------+---------------------------------------------------+ | slow_query_log | OFF | | slow_query_log_file | /var/lib/mysql/iZ2ze6id2tww1o2zn0mznxZ-slow.log | +---------------------+---------------------------------------------------+ 2 rows in set mysql> show variables like 'long_query_time'; +-----------------+-----------+ | Variable_name | Value | +-----------------+-----------+ | long_query_time | 10.000000 | +-----------------+----- # 开启慢查询日志 mysql> set global slow_query_log='ON'; # 设置慢查询日志存放位置 mysql> set global slow_query_log_file='/data/slow_query.log'; # 设置慢查询时间标准,设置之后会在下次会话才生效 mysql> set global long_query_time=1; 2、JVM监控 使用本地jvisualvm远程监控服务器 添加应用程序启动参数,并启动服务 -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=182.92.81.159 -Dcom.sun.management.jmxremote.port=10086 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false 进入本地jdk安装目录bin目录,找到jvisualvm.exe并启动 右键“远程”选择“添加远程主机”,并输入主机IP 右键主机选择“添加JMX连接”,并输入JMX端口 连接成功后在主机下会有对应的连接显示,双击查看监控信息 如下图可见,堆中的内存使用长时间内呈现上升状态,说明在创建新的内存堆时,没有及时释放掉旧的堆,导致资源释放远远小于新建,最后内存泄漏,程序崩溃。 (七)性能测试报告 性能测试报告模板.doc
PV:(Page View):即页面访问量,每打开一次页面PV计数+1,刷新页面也是。PV只统计页面访问次数。 UV(Unique Visitor):唯一访问用户数,用来衡量真实访问网站的用户数量。 一般用UV统计用户活跃数,用PV统计用户访问页面的频率 (一)、 普通计算方法 计算公式:TPS= 总请求数 / 总时间 在2019年第32周,有4.13万的浏览量,那么总请求数,我们可以认为估算为4.13万(1次浏览都至少对应1个请求) 总请求数 = 4.13 万请求数 = 41300 请求数 总时间:由于不知道每个请求的具体时间,我们按照普通方法,我们可以按照一周的时间进行计算 总时间 = 1天 = 1 24 小时 = 24 3600 秒 套入公式可得: TPS = 41300请求数/24*3600秒 = 0.48请求数/秒 结论:按照普通计算方法,我们在测试环境对相同的系统进行性能测试时,每秒能够发送0.48请求就可以满足线上的需要。 (二)、 二八原则计算方法 二八原则就是指80%的请求在20%的时间内完成 计算公式 : TPS = 总请求数 80% / (总时间20%) 按照公式进行计算 TPS = 41300 0.8请求数 / 243600*0.2秒 = 1.91 请求数/秒 结论:按照二八原则计算,在测试环境我们的TPS只要能达到1.91请求数每秒就能满足线上需要。二八原则的估算结果会比平均值的计算方法更能满足用户需求。 (三)、 稳定性测试并发量 根据这些数据统计图,可以得出结论: 大部分订单在8点-24点之间,因此系统的有效工作时长为16个小时从订单数量统计,8-24点之间的订单占一天总订单的98%左右(40474个) 结合二八原则计算公式 : TPS = 总请求数 80% / (总时间20%) 需要在测试环境模拟用户正常业务操作(稳定性测试)的并发量为: TPS = 40474 0.8请求数 / 163600*0.2秒 = 2.81 请求数/秒 (四)、 压力测试并发量 根据这些数据统计图,可以得出结论: 订单最高峰在在21点-22点之间,一小时的订单总数大约为8853个 计算压力测试的并发数:TPS = 峰值请求数/峰值时间 系数 需要在测试环境模拟用户峰值业务操作(压力测试)的并发量为: TPS = 8853 请求数 / 3600秒 3(系数) = 7.38 请求数/秒 不同的系数由公司内部商议决定,一般增长可以设置为2,如果后期用户量会暴增的情况下,可以设置为3,或者更高。
在使用JMeter进行性能测试时,如果并发数比较大(比如项目需要支持10000并发),单台电脑的(CPU和内存)可能无法支持,这时 可以使用JMeter提供的分布式测试的功能。 JMeter分布式执行原理 JMeter分布式测试时,选择其中一台作为控制机(Controller),其它机器做为代理机(Agent)。 执行时,控制机会把脚本发送到每台代理机上,代理机拿到脚本后就开始执行,代理机执行时不需要启动JMeter界面,可以 理解它是通过命令行模式执行的。 执行完成后,代理机会把结果回传给控制机,控制机会收集所有代理机的信息并汇总。代理机(Agent)配置 Agent机上需要安装JMeter,可以从控制机上拷贝一份出来,因为JMeter是免安装的 修改服务端口 注意:非必须。如果是在同一台机器上演示需要使用不同的端口,多台机器可以不修改 打开bin/jmeter.properties文件,修改server_port,比如:server_port=2001 将RMI SSL设置为禁用 打开bin/jmeter.properties文件,修改为:server.rmi.ssl.disable=true 运行Agent上的jmeter-server.bat文件,启动JMeter,注意是jmeter-server.bat 案例: 控制机(Controller)配置 修改JMeter的bin目录下jmeter.properties配置文件,修改remote_hosts 示例:remote_hosts=192.168.182.100:1099,192.168.182.101:1099 IP和Port是Agent机的IP以及自定义的端口,多台Agent之间用","隔开 将RMI SSL设置为禁用 打开bin/jmeter.properties文件,修改为:server.rmi.ssl.disable=true 启动JMeter 选择菜单:运行-->远程启动/远程全部启动 注意事项 Changing to JMeter home directory Could not find ApacheJmeter_core.jar 启动jmeter-server.bat如出现上述报错,请在我的电脑——高级设置——环境变量——添加系统变量和path变量,系统变量新增一条JMETER_HOME,为jmeter的安装目录,path新增一条%JMETER_HOME%\bin如果使用csv进行参数化,那么需要把参数文件在每台代理机上拷一份且路径需要设置成一样的; 运行 分配每一位代理机的线程数,总线程数 = 控制机线程数代理机数,如图 5 X 2 = 10;结果
1、同步定时器 (Synchronizing Timer) loadrunner称为集合点,SyncTimer的目的是阻塞线程,直到阻塞了n个线程,然后立即释放它们。 同步定时器相当于一个储蓄池,累积一定的请求,当在规定的时间内达到一定的线程数量,这些线程会在同一个时间点一起并发,所以可以用来做大数据量的并发请求。 添加方式:测试计划 --> 线程组--> HTTP请求 --> (右键添加) 定时器 --> Synchronizing Timer 注意:定时器一定要放在HTTP请求下,不要和HTTP同级。有2个参数设置 模拟用户组的数量(Number of Simulated Users to Group by) 也就是并发数,集合多少请求后一起发出去 超时时间以毫秒为单位(Timeout in milliseconds) 指定人数多少秒没集合到算超时,默认为0,会一直等。设置500毫秒的话,如果500毫秒内凑齐并发数,就先发出去了。 案例:新增线程组,设置线程数(用户数)为200,启动时间为10秒,那么200/10 = 20,就是每1秒钟就会增长20个用户数,需要10秒才能达到200的用户数新增同步定时器,设置用户数为20,也就是当线程数达到20的时候,才会在同一时间并发发送请求。如果最终人数不够20人,那么可以设置超时,我设置的是20秒,比如线程组用户数为200,设置定时器模拟人数为30,那么达到180的时候,最后线程组还可以模拟20个人,但是定时器需要30人,就会死锁,无线等待,因此设置超时释放是有必要的。最后通过表格查看结果去查看是否在同一个时间内并发20个请求,如图所示,在一秒钟内,并发了20个请求,说明同步定时器设置成功。 2、吞吐量控制器(Constant Throughput Timer) 通过控制每分钟请求数(即控制吞吐的方式)来控制是否进行延时暂停。 例如,当我们需要使服务端长期处于一定的压力下时,可以通过该定时器来控制吞吐。案例:1、新建线程,用户数为1,启动时间为1,设置永久循环,不用户循环的话,无法持续进行稳定性测试2、测试没有吞吐量定时器时,QPS为多少,如图可以看见,极限吞吐量大概在30左右。3、设置吞吐量定时器,比如设置为每秒吞吐量为10,一个用户,吞吐量定时器是以每分钟来计算的,那么我们需要先计算一下。一分钟=60秒,每秒钟10qps /线程数1 X 60秒 = 600,那么需要设置目标吞吐量为600即可。为什么要除以线程数1呢,因为--This thread only :分别控制每个线程的吞吐量,选择这种模式时,总的吞吐量为设置的 target Throughput 乘以线程的数量。五个选择项代表的意思:this thread only:表示控制每个线程的吞吐量,选择这种模式 时,总的吞吐量为设置的目标吞吐量乘以线程的数量。 all active threads:表示设置的目标吞吐量将分配在每个活跃 线程上。每个活跃线程在上一次运行结束后等待合理的时间后 再次运行。活跃线程指某一时刻同时运行的线程。 all active threads in current thread group:表示设置的目标吞 吐量将分配在当前线程组的每一个活跃线程上,当测试计划中 只有一个线程组时,该选项和all active threads选项的效果完 全相同。 all active threads(shared):与all active threads选项基本相 同,区别是每个活跃线程都会在所有活跃线程上一次运行结束 后等待合理的时间后再次运行。 all active threads in current thread group (shared):与all active threads in current thread group选项基本相同,区别是 每个活跃线程都会在所有活跃线程的上一次运行结束后等待合 理的时间后再次运行。 4、查看结果,可以看到,获取用户信息接口QPS成功限制在了10/sec 3、固定定时器 就是一个强制等待,到了时间再发送请求,放在HTTP请求下
(一)、添加CSV数据文件配置 选择CSV Data Set Config (二)、创建csv文件并写入数据 如果编写了首行为列名,在csv data set config中需要选取忽略首行 (三)、配置CSV数据文件 变量名称对标我们csv文件中的每一列,以英文的逗号隔开 分隔符可以是很多,按照我们csv文件的分隔符进行设置 如果csv文件中编写了首行为列名,在csv data set config中需要选取忽略首行 当不允许带引号时,线程执行时,会还原引号,也就是数据中携带引号 文件结束符即为文件末尾,当csv文件行数小于线程数时,如十个用户,线程数为100,那么选择文件结束符再次循环时,会循环10次第一行到第十行的数据。如果文件结束符停止线程时,那么线程数到达10的时候,即刻停止。
协议:向目标服务器发送HTTP请求时的协议,可以是http或者是https ,默认值为http 。 服务器名称或IP :HTTP请求发送的目标服务器名称或IP地址。 端口号:目标服务器的端口号,默认值为80 。 方法:发送HTTP请求的方法,可用方法包括GET、POST、HEAD、PUT、OPTIONS、TRACE、DELETE等。 路径:目标URL路径(不包括服务器地址和端口) Content encoding :内容的编码方式,默认值为iso8859 同请求一起发送参数:GET请求时url中附带参数可以通过此方式添加 消息体数据:POST/PUT请求JSON数据存放地 自动重定向:表示JMeter会自动根据响应头等信息进行重定向操作,且在结果中不会记录重定向的中间过程,只会记录最后一次请求的结果。 跟随重定向:表示JMeter会自动根据响应头等信息进行重定向操作,且在结果中会会记录重定向的中间过程。 使用KeepAlive:表示支持长连接。 对 POST 使用 multipart/form-data:该选项用于需要做文件上传类的请求时。一旦勾选了该选项,则JMeter会自动将参数和文件页的内容组装为混合表单格式的数据,且会自动生成Content-Type请求头(在勾选该选项的时候,切记不要手动添加Content-Type请求头)。不勾选该选项,则参数部分将使用application/x-www-form-urlencoded格式组装并且发送。 与浏览器兼容的头:当使用multipart/form-data时,会隐藏Content-Type和 Content-Transfer-Encoding标头;仅发送Content-Disposition标头。 参数:一般分为两种情况。当使用get方法时,可以在此处实现URL地址参数的设置,然后JMeter会自动进行URLencode处理进行发送。当使用post方法时,仅当发送的请求为application/x-www-form-urlencoded或者 multipart/form-data时,才能使用参数进行请求主体的处理。消息体数据:这个选项和参数选项不能同时使用。该选项表示用户自己指定需要发送的请求主体,相当于是raw数据,仅适用于post方法。使用该选项来发送数据时,一定要手动指定请求头Content-Type。PS:一般来说,像json、xml这样格式的请求数据是必须使用消息体数据选项的。消息体数据也适用于所有类型的请求数据的发送,包括application/x-www-form-urlencoded和 multipart/form-data文件上传:该选项仅适用于勾选了对 POST 使用 multipart/form-data选项时。文件名称:文件路径参数名称:接口中用来传递文件的参数名称,一般就叫"file",具体以实际接口为准。MIME类型:即要上传的文件类型,比如image/jpeg之类,具体以实际情况为准。
元件与组件简介 jmeter元件: 代表jmeter工具菜单中的一个子菜单(功能),比如HTTP请求、事务控制器、响应断言等,就是一个元件。jmeter组件: 元件下的子组件,比如逻辑控制器中有事务控制器,仅一次控制器,循环控制器等,这些都是元件,但它们被归类到逻辑控制器中,逻辑控制器就是组件。 九大元件: (一)取样器 取样器用来模拟用户操作,向服务器(被测系统)发出http请求、webservice请求或者java请求等。可以把http请求元件看成是一个没有界面的浏览器,它可以发送http请求,接收服务器的相应数据。 (二)逻辑控制器 逻辑控制器(Logic Controller)可以自定义JMeter发送请求的行为逻辑,它与Sampler结合使用可以模拟复杂的请求序列。循环控制器可以设置请求的循环次数或永远循环(如果选中永远的话)、事务控制器可以将多个请求放在同一个事务中。如果选中Gegerate parent sample,则聚合报告中只显示事务控制器的数据,而不会显示其中的各个请求的数据,反之则全部显示。 (三)前置处理器 测试脚本开发过程中,在请求发送前可能会做一些环境或者参数的准备工作,那么可以在前置处理器中来完成这些工作。比如,在对数据库进行操作前需要先建立一个数据库连接,那么前置处理器就可以完成这个功能。前置处理器访问路径:【测试计划】-【前置处理器】或者 【测试计划】-【线程组】-【前置处理器】 (四)后置处理器 后置处理器一般放在取样器之后,用来处理服务器的返回结果。比如一个web应用程序,登录后会返回一个SessionID,这个SessionID在登录之后的业务操作过程中会作为验证条件,验证用户是否合法登录了。用取样器模拟这个请求时就需要带上这个属性,那么如何获取呢?首先要知道这个SessionID从哪里来?它是从服务器返回的。接着需要获取它,用什么工具获取呢?jmeter提供了元件,比如正则表达式提取器,它支持正则表达式的方式来获取数据。后置处理器就是专门用来响应数据做处理的元件,jmeter的关联就是通过后置处理器来完成的。后置处理器访问路径:【测试计划】-【后置处理器】或者 【测试计划】-【线程组】-【后置处理器】【前置处理器】、【配置元件】、【后置处理器】都是为取样器提供数据支持的,取样器关注的是业务逻辑。 (五)断言 断言用来验证结果是否正确,用过LoadRunner的应该会知道检查点这个功能,可以帮助判断请求是否成功返回且是否符合要求,在jmeter中不叫检查点,叫做断言。就是用一个预设的结果(值、表达式、时间长短等条件)与实际结果匹配,匹配到则成功,反之失败。jmeter断言元件有多个,比如响应断言、XML断言、BeanShell断言;较常用的是响应断言;对于复杂的断言还可以通过BeanShell脚本来完成(BeanShell是类java的脚本语言,实际上BeanShell是一个小巧的java源码解释器。可以直接调用java程序)。断言的访问路径:【测试计划】-【断言】或者 【测试计划】-【线程组】-【断言】 (六)定时器 为了足够真实地模拟用户负载,有时会需要模拟这些请求在同一时刻发送,就好像把大家集合在一条起跑线上,然后一声令响,同时起跑。那么此时就需要一个集合的功能,jmeter中可以通过定时器来完成这个需求。当然了,定时器可不只有这一个功能,定时器种类有很多,每一种代表了不同的功能。定时器访问路径:【测试计划】-【线程组】-【定时器】 (七)测试片段 控制器上一种特殊的线程组,它与线程组处于一个层级。与线程组不同的就是:测试片段不会执行。它是一个模块控制器或者被控制器应用时才会被执行。方便管理测试用例,按照不同的功能模块进行划分。因为版本不断的被迭代,接口越来越多,可以有效管理我们的测试用例。每个版本一个jmx脚本,里面包含多个测试片段,然后通过控制管理器进行调用。(可以打开几个jmeter进行来跑脚本)测试片段访问路径:【测试计划】-【线程组】-【测试片段】 然后在测试片段添加一些请求,相当于你的测试用例,然后保存。 (八)配置元件 性能测试中为了模拟大量用户操作往往需要做参数化,jmeter的参数化可以通过配置元件来完成,比如CSV Data Set Config,它可以从文件中读取测试数据。另外jmeter也提供了众多的函数(通过函数助手可以查看到)生成动态数据。当然配置元件的作用不仅于此,它还可以用来记录服务器的返回数据,比如http cache manager。自动记录服务器返回的cache信息。简而言之它为取样器提供预备数据,然后由取样器发出请求。配置元件访问路径:【测试计划】-【配置元件】或者 【测试计划】-【线程组】-【配置元件】 (九)监听器 jmeter的测试结果需要添加监听器来收集,jmeter结果收集程序的设计模式就是监听器模式。jmeter的监听器有两个任务: (1) 添加结果监听,并且可以保存测试结果到文件,这些结果数据可以供再次分析使用。(2) 展示结果,jmeter可以以表格及图形的形式展现结果,方便测试人员分析测试结果。在开发测试脚本时,不可避免需要调试,监听器也提供了辅助(比如察看结果树,可以在其中看到请求与响应的数据)。监听器访问路径:【测试计划】-【监听器】或者 【测试计划】-【线程组】-【监听器】【取样器】、【断言】、【监听器】组合在一起就可以完成发送请求、验证结果及记录结果三项。 元件作用域 取样器:元件不和其他元件相互作用,因此不存在作用域的问题;逻辑控制器:元件只对其子节点中的取样器和逻辑控制器作用;其他六大元件:除取样器和逻辑控制器元件外,如果是某个取样器的子节点,则该元件对其父子节点起作用;如果其父节点不是取样器,则其作用域是该元件父节点下的其他所有后代节点(包括子节点,子节点的子节点等); 元件执行顺序 同一作用域下不同元件: 配置元件前置处理器定时器取样器后置处理器断言监听器 不同作用域下相同元件:从上到下依次执行
requests详解与封装一、 基本概念requests 模块是 python 基于 urllib,采用 Apache2 Licensed 开源协议的 HTTP 库。它比 urllib 更加方便,可以节约我们大量的工作,完全满足 HTTP 测试需求。二、 安装通过 pip install requests 安装 requests 库三、快速入门(一)导包import requests(二)常用HTTP请求方法说明GET请求获取URL位置的资源HEAD请求获取URL位置资源的响应消息报告,即获得资源的头部信息POST请求向URL位置的资源后附加新的消息PUT请求向URL位置存储一个资源,覆盖原URL位置的资源PATCH请求局部更新URL位置的资源,即改变该处资源的部分内容DELETE请求删除URL位置存储的资源GET,HEAD是从服务器获取信息到本地,一般用于获取资源等场景PUT,POST,PATCH,DELETE是从本地向服务器提交信息。一般用于操作数据等场景其中GET,POST为常用方法,本期重点讲解代码演示:import requests requsts.requst() requsts.get() requsts.post() requsts.head() requsts.put() requsts.patch() requsts.delete() (三)Get详解常用参数参数类型作用params字典url为基准的url地址,不包含查询参数;该方法会自动对params字典编码,然后和url拼接url字符串requests 发起请求的地址headers字典请求头,发送请求的过程中请求的附加内容携带着一些必要的参数cookies字典携带登录状态proxies字典用来设置代理 ip 服务器timeout整型用于设定超时时间, 单位为秒代码演示:import requests resp = requests.get(url="https://www.baidu.com") print(resp) (四)Post详解常用参数参数类型作用data字典作为向服务器提供或提交资源时提交,主要用于 post 请求json字典json格式的数据, json合适在相关的htmlfiles文件向服务器接口提交文件数据files演示:import requests file = {'file': open('C://Users//hello.txt', 'rb')} res = requests.post(url="http://localhost:8081/uploadfile",files=file) print(r.json())data和json主要区别是请求数据的不同,data一般是表单数据,json是字典格式。(五)response详解属性说明resp.status_codehttp请求的返回状态,若为200则表示请求成功。resp.raise_for_status()该语句在方法内部判断resp.status_code是否等于200,如果不等于,则抛出异常resp.texthttp响应内容的字符串形式,即返回的页面内容resp.encoding从http header 中猜测的相应内容编码方式resp.apparent_encoding从内容中分析出的响应内容编码方式(备选编码方式)resp.contenthttp响应内容的二进制形式resp.json()得到对应的 json 格式的数据,类似于字典源码分析:requests请求调用的是session请求,session和requests请求的区别在于,Session可以自动管理cookie,而requests在需要cookie认证时,请求需要携带cookies参数。四、请求、基础路径封装SendUtil:class SendUtil: session = requests.Session() # 初始化时,就获得项目名称,和环境名称 def __init__(self, team, workspace): self.url = YamlUtil.read_yaml(team, workspace) # 根据数据文件的绝对路径,在调用时,拼接上具体的模块名,即可完成接口拼接 def send(self, method, url, **kwargs): url = self.url + url # 将请求大小写统一设置为小写 method = str(method).lower() # 多参数可以传入data,json,cookie等 res = self.session.request(method=method, url=url, **kwargs) print(f"当前环境是:{YamlUtil.now_workspace}:{url}") return res1、将请求方法method、请求url,以及**kwargs封装成形参,在外部调用请求时,必须传入参数,内部使用session管理请求,达到cookie管理效果2、session为类属性,方便不同的方法去调用该session,防止产生资源浪费3、在SendUtil类初始化时传入参数,传入的team为项目名称,workspace为环境名称,通过封装read_yaml去读取Yaml文件,随后将读取的数据传递给send请求方法的url,完成基础路径的拼接,这里这样做的意义是每个项目都有固定的基础路径,后面拼接的为具体的模块名,通过封装基础路径在调用时再拼接。YamlUtil:class YamlUtil: # 获得数据文件的绝对路径 now_workspace = None @classmethod def get_path(cls): return os.path.dirname(os.path.dirname(__file__)) # 根据数据文件的绝对路径获取数据,第一个参数表示是哪个项目,第二个表示是什么环境 @classmethod def read_yaml(cls, team, workspace): path = cls.get_path()+"/testcases/API/config.yaml" with open(path, mode="r", encoding="utf-8") as file: data = yaml.load(file, Loader=yaml.FullLoader) cls.now_workspace = workspace return data["BASE"][team][workspace]1、get_path为获取数据文件绝对路径方法,在实际项目中,尽量使用绝对路径去拼接,避免资源找不到错误(FileNotFoundError),read_yaml方法需要传入两个参数,第一个是项目名,第二个是环境,通过绝对路径去打开yaml文件,然后返回数据的时候根据我们的项目名和环境名去匹配对应的基础路径config.yaml:BASE: RuoYi: DEV: http://8.129.162.221:login TEST: http://8.129.162.221:getInfo Shop: DEV: http://8.129.162.220:upload TEST: http://8.129.162.220:download使用yaml格式去定义项目的基础路径,在read_yaml读取到项目路径后,返回项目和操作环境即可在跑自动化时一目了然。五、数据读取封装ReadYamlUtil:def read_yaml(path): with open(path, mode="r", encoding="utf-8") as file: data = yaml.load(file, Loader=yaml.FullLoader) return data通过传入path读取Yaml文件内容,随后返回内容,注意要使用encodingdata.yaml:- name: 登录接口 description: 验证登录模块 request: method: POST url: http://8.129.162.221:xxx/login data: code: 1234 password: **** username: cola uuid: 63b2234b8*******3cded6dbedd739 validate: None -test_login:@allure.epic("项目名称:若依管理系统") @allure.feature("模块名称:登录模块") class Test: @allure.story("登录模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=2) @pytest.mark.smoke @pytest.mark.parametrize("data", read_yaml("data.yaml")) def test_baidu01(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) # 未封装基础路径之前,使用我们yaml读取url # res = SendUtil.send(method=data["request"]["method"], # url=data["request"]["url"], json=data["request"]["data"]) # 封装了基础路径之后的用法,通过具体模块名拼接基础路径 res = SendUtil("RuoYi", "DEV").send(method=data["request"]["method"], url="/login", json=data["request"]["data"]) print(res.json()["msg"]) assert res.json()["msg"] == "操作成功"六、接口关联参数封装ReadYaml:接口关联的参数,如果每一个都放到全局变量中代码是非常冗余的,于是我们将其封装起来,常见办法有两种,一种是通过封装方法存入数据库,另一种方法是通过封装方法写入Yaml文件。 # 读取接口关联数据 @classmethod def read_extract(cls, param="Authorization"): path = cls.get_path()+"/testcases/API/extract.yaml" with open(path, encoding="utf-8") as file: data = yaml.load(stream=file, Loader=yaml.FullLoader) return data[param] # 写入接口关联数据 @classmethod def write_extract(cls, data): path = cls.get_path()+"/testcases/API/extract.yaml" with open(path, mode="a", encoding="utf-8") as file: yaml.dump(data=data, stream=file, allow_unicode=True) # 清空接口关联数据 @classmethod def clear_extract(cls): path = cls.get_path() + "/testcases/API/extract.yaml" with open(path, mode="w", encoding="utf-8") as file: file.truncate()test_login:在登录模块成功登录之后,通过write_extract方法以字典的格式写入Yaml文件保存,注意token格式,例如我的项目token需要在前置加上"Bearer "@allure.story("登录模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=1) @pytest.mark.smoke @pytest.mark.parametrize("data", YamlUtil.read_yaml("./testcases/API/data.yaml")) def test_login(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) # 封装了基础路径之后的用法,通过具体模块名拼接基础路径 res = SendUtil("RuoYi", "DEV").send(method=data["request"]["method"], url=data["request"]["url"], json=data["request"]["data"]) print(res.json()) # 登录成功后,写入关联数据token YamlUtil.write_extract({"Authorization": "Bearer "+res.json()["token"]}) assert res.json()["msg"] == "操作成功"在写入Yaml文件后,我们的token得到了保存,在其他接口调用时,即可通过读取yaml方法,获得token的值@allure.story("获取个人信息模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=2) @pytest.mark.smoke @pytest.mark.parametrize("data", YamlUtil.read_yaml("./testcases/API/getInfo.yaml")) def test_getinfo(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) # 封装了基础路径之后的用法,通过具体模块名拼接基础路径 res = SendUtil("RuoYi", "DEV").send(method=data["request"]["method"], url=data["request"]["url"], headers=data["request"]["headers"]) print(res.json()["msg"]) assert res.json()["msg"] == "操作成功"问题来了,当我们在进行参数化的时候,Yaml文件会产生多条Token决此办法需要通过conftest.py文件,每次调用前清空内容即可@pytest.fixture(scope="session", autouse=True, name="fixture") def execute_sql(): # 每次运行之前都清空接口关联的数据,否则每次执行都会产生相同的数据,久而久之数据会变得非常大 YamlUtil.clear_extract()由于我们项目中不止一个接口会需要用到接口参数关联,可能还有需要参数需要关联,因此我对read_yaml方法进行了优化,可以根据关联参数的键进行配对,找到需要关联的参数ReadYamlUtil:param参数默认是token登录鉴权,如果添加了其他关联参数,传递的参数会覆盖此参数,如果不传参数会报错,因此选择了默认参数。 # 读取接口关联数据 @classmethod def read_extract(cls, param="Authorization"): path = cls.get_path()+"/testcases/API/extract.yaml" with open(path, mode="r", encoding="utf-8") as file: data = yaml.load(file, Loader=yaml.FullLoader) data_format = {param: data[param]} return data_format为了实现方便管理,将来会把用例全部写入Yaml文件中,让不懂代码的人也可以跑接口自动化,因此需要将接口关联参数读取进行优化,通过{{params}} 对关联参数进行修饰,在读取时,通过正则表达式去除两边括弧,然后根据中间关键词去读取接口关联参数。getInfo.yaml- name: 获取个人信息接口 description: 个人信息模块 request: method: GET url: /getInfo headers: '{{Authorization}}' validate: NoneSendUtil.py(重点)class SendUtil: session = requests.Session() # 初始化时,就获得项目名称,和环境名称 def __init__(self, team, workspace): self.base_url = YamlUtil.read_configyaml(team, workspace) self.headers_dict = {} # 先生成字符串格式,替换URL中的{{}},然后返回原有格式的URL @classmethod def replace_value(cls, data): if data and isinstance(data, dict): value = json.dumps(data) else: value = data for item in range(0, value.count("{{")): if "{{" in value and "}}" in value: start_index = value.index("{{") end_index = value.index("}}") old_value = value[start_index:end_index+2] new_value = YamlUtil.read_extract(old_value[2:-2]) # replace方法只能传字符串,如果是数值,需要转换 if type(new_value) == int: new_value = str(new_value) value = value.replace(old_value, new_value) # 如果不先生成字符串格式,无法进行替换,替换了之后,需要把数据还原成字典格式 if value and isinstance(data, dict): new_data = json.loads(value) else: new_data = value return new_data # 根据数据文件的绝对路径,在调用时,拼接上具体的模块名,即可完成接口拼接 def send(self, method, url, headers=None, **kwargs): # 获取{{param}}参数,替换为关联参数 url = self.base_url + SendUtil.replace_value(url) # 替换请求头 if headers: # 取token的value headers_value = SendUtil.replace_value(headers) # 取key,拼接成字典格式 headers_key = headers[2: -2] self.headers_dict = {headers_key: headers_value} # 替换请求数据 for key, value in kwargs.items(): if key in ['params', 'data', 'json']: kwargs[key] = self.replace_value(value) method = str(method).lower() # 多参数可以传入data,json,cookie等 res = self.session.request(method=method, url=url, headers=self.headers_dict, **kwargs) print(f"当前环境是:{YamlUtil.now_workspace}:{url}") return res七、Yaml用例封装1.通过自定义Yaml必填规则,让业务人员填写时遵守Yaml编写规则2.判断用例是否需要请求头,有的情况下,需要将请求头读取后添加3.判断用例是否需要断言analysis_yaml(分析Yaml):# yaml测试用例规则约束 def analysis_yaml(self, case): # 获取yaml用例的所有键 resp = None case_key = dict(case).keys() # 判断必填的键是否存在 if 'name' in case_key and 'base_url' in case_key and 'request' in case_key and 'validate' in case_key: request_key = dict(case['request']).keys() # 判断request中的method和url是否存在 if 'method' in request_key and 'url' in request_key: # 获取method和url method = case['request']['method'] url = case['request']['url'] # 从列表中移除method和url和headers,因为要把可变长度的参数传给send方法的**kwargs del case['request']['method'] del case['request']['url'] headers = None # 通过jsonpath判断是否存在请求头 if jsonpath.jsonpath(case, '$.request.headers'): headers_value = case['request']['headers'] headers = self.replace_load(headers_value) # 从列表中移除method和url和headers,因为要把可变长度的参数传给send方法的**kwargs del case['request']['headers'] print("发送请求头为:", headers) resp = self.send(method=method, url=url, headers=headers, **case['request']) res_data = resp.json() res_text = resp.text if not case['validate'] is None: self.validate_result(res_data, case['validate']) elif case['validate'] is None: print("无需断言") if jsonpath.jsonpath(case, '$.extract'): for key, value in dict(case['extract']).items(): # 通过正则表达式提取token if '(.+?)' in value or '(*.?)' in value: re_value = re.search(value, res_text) if re_value: extract_data = {key: "Bearer "+re_value.group(1)} YamlUtil.write_extract(extract_data) # 通过json表达式提取token else: extract_data = {key: "Bearer "+res_data[value]} YamlUtil.write_extract(extract_data) else: print("request必填项不能为空") else: print("yaml用例必填项不能为空") return respYaml用例:- name: 岗位管理新增接口 description: 系统管理模块 base_url: http://8.129.162.225:8080 request: method: POST url: /system/post json: postCode: ${get_random_number(50,10000)} postName: ${get_random_name(1,1000)} postSort: 0 status: 0 headers: ${get_extract_data(Authorization)} validate: code: 200 equals: 操作成功测试用例(先进行yaml规则校验,然后再发送请求): @allure.story("岗位关联模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=3) @pytest.mark.smoke @pytest.mark.parametrize("data", YamlUtil.read_yaml("setPost.yaml")) def test_set_post(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) # 封装了基础路径之后的用法,通过具体模块名拼接基础路径 res = SendUtil("RuoYi", "DEV").analysis_yaml(data) print(res.json()) 八、Yaml优化—热加载通过反射方法,将yaml用例中的方法映射到Debug_talk.py文件的方法中,形成参数替换replace_load:# 热加载替换方式 @classmethod def replace_load(cls, data): if data and isinstance(data, dict): value = json.dumps(data, ensure_ascii=False) else: value = data for item in range(0, value.count("${")): if "${" in value and "}" in value: start_index = value.index("${") end_index = value.index("}") old_value = value[start_index:end_index + 1] # 获取yaml文件中的方法名 function_name = old_value[2: old_value.index('(')] # 获取yaml文件中的参数 args_value = old_value[old_value.index('(')+1: old_value.index(')')] # 将参数分割,变成单个个体 args_value_list = args_value.split(',') # 通过反射方法,去获取新的值 new_value = getattr(DebugTalk(), function_name)(*args_value_list) # 得到新的值,replace只能传字符串,需要强转 value = value.replace(old_value, str(new_value)) # 如果不先生成字符串格式,无法进行替换,替换了之后,需要把数据还原成字典格式 if data and isinstance(data, dict): data = json.loads(value) else: data = value return dataYaml用例:- name: 岗位管理新增接口 description: 系统管理模块 base_url: http://8.129.162.225:8080 request: method: POST url: /system/post json: postCode: ${get_random_number(50,10000)} postName: ${get_random_name(1,1000)} postSort: 0 status: 0 headers: ${get_extract_data(Authorization)} validate: code: 200 equals: 操作成功DebugTalk.py:import random from common.yaml_util import YamlUtil class DebugTalk: # 热加载获取随机数方法 @classmethod def get_random_number(cls, min_num, max_num): number = random.randint(int(min_num), int(max_num)) return number # 热加载获取随机名称 @classmethod def get_random_name(cls, min_num, max_num): num = random.randint(int(in_num), int(max_num)) name = str(num) + "测试岗位" return name # 热加载获取token或者其他关联参数 @classmethod def get_extract_data(cls, param): return YamlUtil.read_extract(param=param) 九、断言封装 # 断言封装 def validate_result(self, result, expect): if expect and isinstance(expect, dict): for key, value in dict(expect).items(): if key == "code" and type(value) == int: assert value == result[key] if type(key) == str and key == "equals": result_str = str(result) assert value in result_str十、其他的一些配置文件pytest.ini(添加运行参数):[pytest] addopts = -vs -m 'smoke or newModel' --alluredir=reports/temps --clean-alluredir testpaths = testcases/ python_files = "test_*.py" python_classes = "TestApi*" python_functions = "test_*" markers = smoke: All_test newModel: some_testconftest.py(全局夹具):import pytest from common.yaml_util import YamlUtil @pytest.fixture(scope="session", autouse=True, name="fixture") def execute_sql(): print("········夹具前置") # 每次运行之前都清空接口关联的数据,否则每次执行都会产生相同的数据,久而久之数据会变得非常大 YamlUtil.clear_extract() yield print("夹具后置········") run.py(用例执行):import os import pytest if __name__ == '__main__': # # 运行所有 # pytest.main() # # 指定模块 # pytest.main(['-vs', 'testcases/test_api.py']) # # 指定目录 # pytest.main(['-vs', 'testcases']) # # 通过node id指定用例运行 # pytest.main(['-vs', 'testcases/test_api.py::TestApi::test_baidu']) # # 失败重跑 # pytest.main(['-vs', '--reruns=2', 'testcases']) # # 发现失败即可停止运行 # pytest.main(['-vs', '-x', 'testcases/test_api.py']) # # 发现N个失败即可停止运行 # pytest.main(['-vs', '--maxfail=2', 'testcases/test_api.py']) # # 生成测试报告 # pytest.main(['-vs', '--maxfail=2', '--html=report/report.html', 'testcases/test_api.py']) # # 多线程运行 # pytest.main(['-vs', '-n 3', 'testcases/test_api.py']) # 根据测试用例的部分字符串指定测试用例 # pytest.main(['-vs', '-k', 'test_baidu03 or test_baidu04', 'testcases/test_api.py']) pytest.main() os.system("allure generate reports/temps -o reports/allures --clean")requirements.txt(项目依赖自动安装, pip install -r requirements):pytest~=7.1.2 pytest-html pytest-xdist pytest-ordering pytest-rerunfailures allure-pytest requests~=2.27.1 PyYAML~=6.0 jsonpath~=0.82总结:只是一个基础框架,归根结底很多代码需要根据公司实际业务去进行优化,代码只是编程的实现方式,更重要的是思维方式,ok,以下一张图结束本节。
Pytest+Allure实现用例管理与自定义测试报告(一)快捷安装所需依赖1、创建requirements.txt文件,里面填写我们项目所需的依赖2、在Pycharm下方的Terminal栏中,输入命令pip install -r <文件名>3、按回车键执行命令,即可自动下载我们项目所需要的依赖4、然后输入pip list,即可查看我们下载的依赖是否存在,存在则说明下载成功,如果失败的话,需要重新执行命令,有可能是网络波动导致下载依赖失败。(二)Pytest测试用例规则1、模块名必须要以test_开头或者_test结尾2、测试类必须以Test开头,并且不能有__init__方法3、测试用例必须要以test_开头(三)Pytest的运行方式-n <数值>: 支持多线程或者分布试运行测试用例,大大提升运行时速度(前提是安装了pytest -xdist依赖)--reruns <数值>:任何用例失败后会进行重跑2次(前提是安装了pytest -rerunfailures依赖)-x:表示只要一个用例报错,那么测试停止--maxfail=<数值> :表示出现两个用例失败就停止-k: 根据测试用例的部分字符串指定测试用例,多个名称使用关键字“or”,“and”分隔--html=<存放目录>: 运行完测试用例后生成测试报告(前提是安装了pytset -html)1、主函数方式一般会在目录的根目录下创建一个run.py文件,用来运行所有的测试用例,这个时候符合pytest测试用例规则的就会被发现并且运行,控制台输出一个.表示运行通过。2、命令行方式在pycharm下方的terminal中,输入pytest即可运行所有的符合测试用例规则的测试用例方法[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8RRZXlYn-1661627743675)(http://colablog.top/imagepytest-9.png)]。(1)输入pytest,运行所有测试用例(2)输入pytest -vs <文件名>,运行指定模块的测试用例(3)输入pytest -vs <目录名>,运行指定目录下的测试用例(4)输入pytest -vs <具体用例方法>,根据node id运行指定位置的测试用例[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHduJ6ts-1661627743676)(http://colablog.top/imagepytest-9.png)]3、Pytest.ini配置文件运行(1)在项目根目录下创建pytest.ini文件(2)[pytest]用于标记该文件为pytest配置文件(3)addopts表示命令行参数,多个参数使用空格分隔(4)testpaths表示测试用例搜索范围(5)python_files表示修改测试用例文件命名规则(6)python_classes表示修改测试用例类名命名规则(7)python_functions表示修改测试用例方法名命名规则(8)markers表示对用例进行分组,可按照不同的组来执行,此操作需要给测试用例方法添加装饰器,如模块名为“smoke”,测试用例需要添加@pytest.mark.smoke来表示此用例属于该分组,addopts需要添加-m <分组名>,多个分组使用 '<分组名1> or <分组名2>',类似于-k参数。ps:该文本不能存在中文字符!!!如果需要中文注释的话,需要使用Nodepad++打开,将编码格式修改为“GB2312”,在工具栏中的“编码-编码字符集-中文-GB2312”。示例图:(四)常用装饰器@pytest.mark.run(order=1)改变测试用例执行顺序,需要安装pytest-ordering插件。pytest默认执行顺序是从上到下,该装饰器可以修改测试用例的执行顺序,数值越小,优先级越高,有装饰器的比没有装饰器的优先级高。@pytest.mark.skip(reason="")@pytest.mark.skipif(<判断式>, reason="")跳过不需要执行的测试用例,skip和skipif,skipif需要添加判断条件,如果成立则跳过该条测试用例,不成立则执行用例,skif只需要添加原因,直接不执行该条用例。@pytest.fixture(scope="作用域",params="数据驱动",autouse="自动执行",ids="自定义参数名",name="别名")scope参数作用域function每条测试用例前后执行一次class每个测试类前后执行一次module在每个测试.py文件前后执行一次package/session所有包前后执行一次1、scopefunction级别:@pytest.fixture(scope="function") def execute_sql(): print("········夹具前置") yield print("夹具后置········") def test_baidu03(self, execute_sql): print("这是一条测试用例") assert 1 == 1yield可以返回数值,并且可以在返回后继续执行后面的代码,而使用return则不可以class级别:@pytest.fixture(scope="class", autouse=True) def execute_sql(): print("········夹具前置") yield print("夹具后置········") pytest.mark.usefixtures("execute_sql") class TestApi: def test_baidu01(self): print("这是一条测试用例") assert 1 == 1在测试类用需要添加夹具方法module级别:@pytest.fixture(scope="module", autouse=True) def execute_sql(): print("········夹具前置") yield print("夹具后置········")每个模块(.py文件)执行前后运行一次,效果类似于setup_module,teardown_module方法2、autouser代码示例:@pytest.fixture(scope="function", autouse=True)使用autouse,会自动给所有测试用例添加夹具3、paramsdef read_data(): return ['1', '2', '3', '4'] @pytest.fixture(scope="function", params=read_data(), autouse=True) def execute_sql(request): print("········夹具前置") yield request.param print("夹具后置········") @pytest.mark.newModel def test_baidu04(self, execute_sql): print(execute_sql) print("这是一条测试用例04") assert 1 == 1定义一个返回值方法,然后传入params参数中,通过给夹具方法加上形参request(固定写法),然后通过yield返回request.param(固定写法)返回给测试用例,达到数据驱动的目的4、idsdef read_data(): return ['1', '2', '3', '4'] @pytest.fixture(scope="function", params=read_data(), ids=['test1', 'test2', 'test3', 'test4'], autouse=True) def execute_sql(request): print("········夹具前置") yield request.param print("夹具后置········") @pytest.mark.newModel def test_baidu04(self, execute_sql): print(execute_sql) print("这是一条测试用例04") assert 1 == 1ids需要和params参数一起使用,ids是给params参数命名,ids参数的长度需要和params参数长度一致,一一对应,此作用是在执行用例时,能清晰看到每一条参数的名字,如下图所示。5、name@pytest.fixture(scope="function", params=read_data(), ids=['test1', 'test2', 'test3', 'test4'], autouse=True, name="fixture") def execute_sql(request): print("········夹具前置") yield request.param print("夹具后置········") def test_baidu04(self, fixture): print(fixture) print("这是一条测试用例04") assert 1 == 1⚠️name用于给夹具起别名,测试用例的参数需要使用此别名,否则报错。@pytest.mark.parametrize(参数名, 数据)用于修饰测试用例方法,参数名表示数据的别名,数据参数可放数组、字典、列表、元组等,使用此装饰器的测试用例方法形参中,必须添加此装饰器的参数名且一致,才能实现数据驱动。@pytest.mark.parametrize("name, test_id", [["哈哈哈", 1], ["呵呵呵", 2], ["嘻嘻嘻", 3]]) def test_baidu(self, name, test_id)结合Yaml,实现数据驱动,接口自动化def read_yaml(path): with open(path, mode="r", encoding="utf-8") as file: data = yaml.load(file, Loader=yaml.FullLoader) return data @allure.epic("项目名称:若依管理系统") @allure.feature("模块名称:登录模块") class TestApi: @allure.story("登录模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=2) @pytest.mark.smoke @pytest.mark.parametrize("data", read_yaml("./testcases/API/data.yaml")) def test_baidu01(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) res = requests.request(method=data["request"]["method"], url=data["request"]["url"], json=data["request"]["data"]) assert res.json()["msg"] == "操作成功"PS:读取Yaml文件的路径哪个文件调用就相对于哪个文件的相对路径,因此,建议使用一个工具类进行绝对路径拼接。(五)Pytest前后置器1、测试用例执行之前运行def setup(self)2、测试用例执行之后运行def teardown(self)3、测试类执行之前运行def setup_class(self)ps:常用于创建数据库连接、日志对象等只需要执行一次的操作4、测试类执行之后运行def teardown_class(self)5、模块执行之前运行def setup_module()6、模块执行之后运行def teardown_module()PS:前后置器只能作用于全局,不能指定测试用例,所以出现了更强大的装饰器—@pytest.fixture(六)conftest共享夹具:配合@pytest.fixture(scope="package/session")一起使用。在每一个模块文件里创建夹具,只能内部使用,无法达到共享,于是产生了conftest.pyimport pytest def read_data(): return ['1', '2', '3', '4'] @pytest.fixture(scope="package", params=read_data(), ids=['test1', 'test2', 'test3', 'test4'], autouse=True, name="fixture") def execute_sql(request): print("········夹具前置") yield request.param print("夹具后置········"),在测试用例包下创建conftest(固定名称),在其中添加夹具方法,在所有测试用例文件中,都能使用到此夹具,目录级别越高的conftest会优先执行,目录级别比较低的conftest会后面执行。当conftest的scope级别和用例文件内setup、teardown方法级别一致时,conftest会优先执行。(七)Assert断言assert xx:判断 xx 为真assert not xx :判断 xx 不为真assert a in b:判断 b 包含 aassert a == b :判断 a 等于 bassert a != b :判断 a 不等于 b(八)Allure报告1、Allure下载官网下载Allure,官网地址:Allure2.18.1下载2、配置解压文件后,将bin目录添加到系统变量path中3、验证命令行窗口中验证Allure是否安装成功allure --version4、安装allure-pytest检验Pycharm中是否安装allure-pytestpip list5、配置pytest.ini在pytest.ini中添加运行参数[pytest] addopts = -vs -m 'smoke or newModel' --alluredir=reports/temps --clean-alluredir--alluredir=DIR:指定allure报告所需json数据的文件夹;--clean-alluredir:如果已经存在报告,那就先清空,然后再生成新的测试报告;6、定制Logo进入E:\Allure\allure-2.18.1\config目录下修改allure.yml,末尾添加 - custom-logo-plugin进入E:\Allure\allure-2.18.1\plugins\custom-logo-plugin\static,将定制logo图标复制至此修改style.css,将内容修改为以下代码.side-nav__brand { background: url('logo.png') no-repeat left center !important; margin-left: 40px; height: 90px; background-size: contain !important; } .side-nav__brand-text{ display: none; }最终展示效果7、生成报告在run.py中生成测试报告 pytest.main() os.system("allure generate reports/temps -o reports/allures --clean")使用os.system()调用cmd命令行模式,allure generate <allure-result中间文件> -o 输出目录(默认路径:allure report)- -clean 清缓存。8、报告优化@allure.epic()项目名称,修饰类@allure.feature()模块名称,修饰类@allure.story()子功能名称,修饰方法@allure.title()测试用例名称,修饰方法,适用于单个用例单个名称allure.dynamic.title()测试用例名称,方法体内使用,适用于动态参数的测试用例,可结合@pytest.mark.parametrize一起使用,当@allure.title()和此方法一同使用时,此方法会覆盖掉@allure.title()@allure.epic("项目名称:若依管理系统") @allure.feature("模块名称:登录模块") class TestApi: @allure.story("登录模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=2) @pytest.mark.smoke @pytest.mark.parametrize("data", read_yaml("./testcases/API/data.yaml")) def test_baidu01(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) res = requests.request(method=data["request"]["method"], url=data["request"]["url"], json=data["request"]["data"]) assert res.json()["msg"] == "操作成功"@allure.severity()参数名参数级别对标测试用例级别allure.severity_level.BLOCKER最高,阻塞级致命:系统崩溃,内存泄漏等allure.severity_level.CRITICAL次高,严重级严重:功能未实现,服务器错误等allure.severity_level.NORMAL一般,正常级一般:逻辑错误,功能报错等allure.severity_level.MINOR较低,轻微级轻微:条件查询错误,界面UI错误allure.severity_level.TRIVIAL最低,建议级建议:错别字,样式优化等此装饰器,可以修饰测试方法,也可以修饰测试类,都是用于表现不同程度的级别@allure.description()修改测试用例描述信息,用于装饰测试方法,适用于单个用例allure.dynamic.description()修改用例描述信息,用于测试方法内部,适用于动态参数的测试用例,可结合@pytest.mark.parametrize 一起使用,当allure.description()和此方法一同使用时,此方法会覆盖掉allure.description()@allure.epic("项目名称:若依管理系统") @allure.feature("模块名称:登录模块") class TestApi: @allure.story("登录模块") @allure.severity(allure.severity_level.BLOCKER) @pytest.mark.run(order=2) @pytest.mark.smoke @pytest.mark.parametrize("data", read_yaml("./testcases/API/data.yaml")) def test_baidu01(self, data): allure.dynamic.title(data["name"]) allure.dynamic.description(data["description"]) res = requests.request(method=data["request"]["method"], url=data["request"]["url"], json=data["request"]["data"]) assert res.json()["msg"] == "操作成功"@allure.link(name="", url="")接口地址,name表示接口地址名称,URL表示链接地址@allure.issue(name="", url="")BUG地址,name表示BUG地址名称,URL表示BUG链接地址@allure.testcase(name="", url="")测试用例地址,name表示测试用例名称,URL表示测试用例链接地址@allure.step(title="")测试步骤,用于修饰方法,当该方法被测试用例调用时,会显示为测试步骤,该方法可传参with allure.step测试步骤,用于方法内部,会显示为测试步骤,该方法用于方法内部,无法传参@allure.epic("测试项目:百度") @allure.feature("模块名称:百度搜索") class TestApi: @allure.story("百度搜索框") @allure.title("能否根据关键词搜索正确结果") @allure.description("验证百度模块") @pytest.mark.smoke @allure.link(name="接口地址", url="https://www.colablog.top") @allure.issue(name="BUG地址", url="https://www.colablog.top") @allure.testcase(name="测试用例地址", url="https://www.colablog.top") def test_baidu(self): with allure.step("打开浏览器"): print("打开谷歌浏览器") with allure.step("输入关键词"): print("输入搜索的关键词") with allure.step("搜索查看结果"): print("搜索结果并查看是否正确") allure.dynamic.title("覆盖-能否根据关键词搜索正确结果") allure.dynamic.description("覆盖-验证百度模块") print("这是第二条测试用例") assert 1 == 1allure.attach附件,用法方法内部,测试报告会显示附件,需要配合with open使用with open("D:\\壁纸.jpg", mode="rb") as f: allure.attach(body=f.read(), name="错误截图", attachment_type=allure.attachment_type.JPG)
Selenium教程一、安装SeleniumWebdrive1、下载selenium webdrive2、下载地址:chromedrive3、找到与谷歌浏览器版本号一致的包,下载chromedriver_win32.zip可以通过浏览器右上角,点击设置,找到关于谷歌,即可查看本地谷歌浏览器的版本号随后在淘宝镜像网中,找到与浏览器版本号开头一致的数字包即可.4、配置环境变量,将chromedriver_win32.zip文件解压地址添加到Path变量Selenium驱动程序需要环境变量配置二、Pycharm安装selenium控制台Terminal输入安装命令pip install selenium三、测试Demofrom selenium import webdriver driver = webdriver.Chrome(port=8800) url = "https://www.colablog.top" driver.get(url)配置port端口可以用于多浏览器运行自动化,例如A电脑谷歌浏览器端口为8800B电脑谷歌浏览器端口为9900四、浏览器操作driver.back() 上一个页面driver.forward() 前进下一个页面driver.refresh() 刷新driver.minimize_window() 窗口最小化driver.maximize_window() 窗口最大化driver.fullscreen_window() 全屏在新窗口打开链接new_win = "window.open('https://taobao.com')" driver.execute_script(new_win)driver.get_screenshot_as_file() 截屏driver.quit() 退出from selenium import webdriver from time import sleep # 获得浏览器对象 driver = webdriver.Chrome(port=8800) # 访问的URL url = "https://www.colablog.top" # 浏览器对象进行访问 driver.get(url) sleep(2) # 跳转操作 driver.get("https://www.bilibili.com") sleep(2) # 上一个页面 driver.back() # 刷新 driver.refresh() # 前进下一个页面 driver.forward() # 窗口最小化 driver.minimize_window() # 窗口最大化 driver.maximize_window() # 在新窗口打开链接 new_win = "window.open('https://taobao.com')" driver.execute_script(new_win) # 截屏 driver.get_screenshot_as_file("E://PythonWorkSpace//SeleniumProject//screen.png") # 退出 driver.quit()五、Python、Selenium、Chrome之间的关系Python通过Selenium实例对象调用API去访问Webdriver接口,再通过Webdriver使用js代码去控制浏览器六、8种元素定位方式提醒:通过webdriver对象的find_element_by_xx(" ")(在selenium的4.0版本中此种用法已经抛弃,不推荐使用)4.0版本语法解析def find_element(self, by=By.ID, value=None) 或 def find_elements(self, by=By.ID, value=None)find_element 和 find_elements的区别如下:1、find_element查找元素返回的是一个对象,而find_elements返回的是一个列表2、当find_element找到多个元素时,返回第一个,find_elements返回一个列表3、当find_element找不到元素时,报错,find_elements返回空列表其中By.ID在源码中存在映射关系,关系如下,一共有八种定位方式。class By: """ Set of supported locator strategies. """ ID = "id" XPATH = "xpath" LINK_TEXT = "link text" PARTIAL_LINK_TEXT = "partial link text" NAME = "name" TAG_NAME = "tag name" CLASS_NAME = "class name" CSS_SELECTOR = "css selector"value传需要定位的元素,如xpath语句,最终调用find_element方法也可直接传参str类型的数据,代表元素查找方式input_element = browser.find_element('xpath', '表达式')当使用By.ID或者其他时,需要导入from selenium.webdriver.common.by import By最终返回一个WebElement对象,对象包含以下属性和方法:tag_name 获取对象标签名size 获得标签的宽高text 获取标签文本location 获得标签x,y轴坐标rect 获得标签宽高和坐标id 获取标签的标识text 获取标签文本内容click() 点击方法submit() 提交方法clear() 清空输入的内容is_selected() 是否被选中is_enabled() 是否可用send_keys() 输入内容is_displayed() 是否显示get_attribute() 该元素指定属性的值(一)、xpathinput_element = browser.find_element('xpath', '//*[@id="nav-searchform"]/div[1]/input')Xpath常用方法详解:基础语法:表达式描述举例node_name选取此节点的所有子节点。无/绝对路径匹配,从根节点选取。无//相对路径匹配,从所有节点中查找当前选择的节点,包括子节点和后代节点,其第一个 / 表示根节点。//li.选取当前节点。无..选取当前节点的父节点。无@选取属性值,通过属性值选取数据。常用元素属性有 @id 、@name、@type、@class、@tittle、@href。//a[@class="参数"]xpath通配符:通配符描述说明举例*匹配任意元素节点//li/*@*匹配任意属性节点//li/@*node()匹配任意类型的节点//li/node()多路径匹配xpath表达式1 | xpath表达式2 | xpath表达式3//ul/li[@class="book2"]/p[@class="price"]|//ul/li/@href进阶用法1、contains(包含某内容)contains通常配合text一起使用,//a[contains(text(), '关闭')][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BBbFEdL6-1661627512771)(http://colablog.top/imagechrome_M8vYQQuoaB.png)]2、starts-with(以某内容开头)3、or(或)4、and(与)5、text(包含某文本)contains通常配合text一起使用,//a[contains(text(), '关闭')]6、ancestor(查找节点前)首先查找到在ancestor声明之前的那个元素,然后将这个元素设为顶端节点,最后查找这个节点内所有符合规则的元素7、following(查找节点后)8、following-sibling(当前节点之后的所有同级节点)9、preceding-sibling(当前节点之前的所有同级节点)(二)、css# 通过css选择器 lis = driver.browser.find_element('By.CSS_SELECTOR', 'body > div > ul > li:nth-child(2)') print(lis.text) 选择器示例示例说明.class.intro选择所有class="intro"的元素*id#firstname选择所有id="firstname"的元素**选择所有元素elementp选择所有元素element,elementdiv,p选择所有元素和元素element elementdiv p选择元素内的所有元素element>elementdiv>p选择所有父级是 元素的 元素element+elementdiv+p选择所有紧跟在 元素之后的第一个 元素[attribute][target]选择所有带有target属性元素(三)、id# 找到id = username的元素 username = driver.find_element('By.ID',"username") # 输入值 张三 username.send_keys("张三") # 找到od = password的元素 password = driver.find_element('By.ID',"password") # 输入值 123 password.send_keys("123")(四)、class# =====通过 元素Class查找(仅返回匹配到的第一个) login_btn = driver.find_element('By.CLASS_NAME',"login") # 点击 login_btn.click() element在找到多个class为login时,只返回第一个,若有多个标签使用同一个class属性值,建议使用elements(五)、name# =====通过 元素name查找元素(仅返回匹配到的第一个) password = drive.find_element('By.NAME',"password") # =====输入值 123 password.send_keys("123")(六)、tag_name(标签名)# 通过元素标签(仅返回匹配到的第一个) p = driver.find_element('By.TAG_NAME',"p") # 打印元素的文本值 print(p.text)(七)、link_text(超链接文本—全量匹配)# 通过超链接的文本查找元素 news_text = driver.find_element('By.LINK_TEXT ',"新闻") print(news_text.text)(八)、partial_link_text(超链接文本—模糊匹配)# =====通过 超链接的文本查找元素(支持模糊匹配) news_text = driver.find_element('By.PARTIAL_LINK_TEXT',"新") print(news_text.text)七、三种等待方式强制等待time.sleep(10)停止代码运行,直到指定时间才继续运行代码,全局无限次数使用。隐性等待browser.implicitly_wait(10)隐性等待只能等待目标元素加载完毕,不能等待页面内容加载,即每一个driver.find_element或driver.find_elements寻找的元素,全局只能使用一次此方法。显性等待wait = WebDriverWait(browser, 10)基于强制等待和隐性等待的综合体,全局无限使用,在某个需要等待的元素后面添加即可,分三个步骤第一个步骤是获得显性等待器,传入两个参数,第一个是浏览器驱动对象,第二个是超时时长,在固定时间内如果超时,则不再等待。第三个为可选参数,每隔多长时长查看是否得到等待结果,poll_frequency=时长第二个步骤是设置等待器的等待条件,wait.until(),导入expected_conditions包,里面有等待条件的各种方法。如查询是否包含在标题中,wait.until(expected_conditions.title_contains()显性等待不仅可以等待页面内容的出现,也可以等待页面元素的加载,如下代码实现locator可传元组或者列表。locator = ['id', 'kw'] baidu_element = wait.until(expected_conditions.presence_of_element_located(locator))窗口等待案例:窗口等待方法为:expected_conditions.new_window_is_opened(current_handles)在窗口切换之前获得当前handlers,在源码中,判断当前与新开窗口的handles列表长度,如果长度大于打开之前的handles,则说明新窗口打开成功。 def test_setting(self): # 实例化浏览器对象 browser = webdriver.Chrome() # 隐性等待 browser.implicitly_wait(10) # 自定义URL url = "https://www.baidu.com" # 访问url browser.get(url) time.sleep(2) # 获取当前窗口句柄 current_handles = browser.window_handles # 鼠标操作 action = ActionChains(browser) setting = browser.find_element(By.ID, 's-usersetting-top') action.move_to_element(setting).perform() time.sleep(2) weather = browser.find_element('xpath', '//a[contains(text(), "隐私")]') action.click(weather).perform() # 新窗口显性等待 wait = WebDriverWait(driver=browser, timeout=5) wait.until(expected_conditions.new_window_is_opened(current_handles)) browser.switch_to.window(browser.window_handles[-1]) qr_code = browser.find_element('xpath', "//p[contains(text(),'用户名密码') and @class='pass-form-logo']") time.sleep(2) print(qr_code.tag_name)总结:使用等待优先级—隐形等待 > 强制等待 > 显性等待八、三种切换(一)、窗口切换browser.switch_to.window()窗口的切换根据窗口句柄进行切换,窗口句柄就是窗口的标识码,多个窗口通过browser.window_handles获得窗口列表,通过下标获取指定窗口。(二)、iframe切换browser.switch_to.frame()参数可传index(下标0开始),name(name属性值),以及iframe_element(通过find_element获得),表示具体切换的iframe # iframe切换 # 通过name browser.switch_to.frame('iframeResult') # 通过index browser.switch_to.frame(0) # 通过element iframe_element = browser.find_element('id', 'iframeResult') browser.switch_to.frame(iframe_element)(三)、alert切换alert = browser.switch_to.alertaccetp()1.先用switch_to_alert方法切换到alert弹出框上2.可以用text方法获取弹出的文本信息3.accept()点击确认按钮dismiss()1.先用switch_to_alert方法切换到alert弹出框上2.可以用text方法获取弹出的文本信息3.dismiss()相当于点右上角x,取消弹出框send_keys()— 仅限于prompt1.先用switch_to_alert方法切换到alert弹出框上2.可以用text方法获取弹出的文本信息3.send_keys()这里多个输入框,可以用send_keys()方法输入文本内容4.accept()点击确认按钮九、鼠标操作导包from selenium.webdriver import ActionChains初始化动作链条action = ActionChains(browser)ActionChains支持连续操作(链式调用),但是perfrom结尾将不能再进行连续操作,如:action.click(weather).click().move_to_element().send_keys().perform()单击操作()方式一:action.click(driver).perform()方拾二:browser.find_element(By.ID, 's-usersetting-top').click()区别:第一种方式可操作性更高,第二种方式简洁明了移动操作action.move_to_element(element_name).perform()双击操作action.double_click(element_name)右击操作action.context_click(element_name)拖拽操作action.drag_and_drop(element_name1, element_name2)十、键盘操作指定元素键盘操作—Keysfrom selenium.webdriver import Keys baidu_element.send_keys(Keys.ENTER)全局键盘操作—ActionChainsfrom selenium.webdriver import ActionChains baidu_element.send_keys(Keys.ESC).perform()十一、下拉元素选择操作第一种方式:直接选择元素 def test_choice(self): # 实例化浏览器对象 browser = webdriver.Chrome() # 隐性等待 browser.implicitly_wait(10) # 自定义URL url = "E:/PythonWorkSpace/SeleniumProject/test.html" # 访问url browser.get(url) time.sleep(2) select_ele = browser.find_element('xpath', '//option[@value="women"]') select_ele.click()方式二:通过Select导包:from selenium.webdriver.support.select import Select先找到下拉框元素ID,传入Select,再根据返回的对象去根据方法找对应的下拉元素 select_ele = browser.find_element('id', 'choice') select_ele2 = Select(select_ele) select_ele2.select_by_visible_text("女") time.sleep(2) select_ele2.select_by_value("man") time.sleep(2)十二、JS操作当下拉框元素不是select时,直接选择元素或通过Select将无法选择,可以通过JS操作browser.execute_script(dom_script) def test_dmo(self): # 实例化浏览器对象 browser = webdriver.Chrome() # 隐性等待 browser.implicitly_wait(10) # 自定义URL url = "https://www.12306.cn/index/" # 访问url browser.get(url) time.sleep(2) # 当下拉框为Select时,此方法生效 # elem = browser.find_element('id', 'train_date') # elem_select = Select(elem) # elem_select.select_by_visible_text('31') # time.sleep(2) # 当下拉框为其他元素,如div时,可用dom操作 dom_script = "let date = document.getElementById('train_date');date.readOnly = false;date.value = '2022-05-01'" browser.execute_script(dom_script) time.sleep(2)Python 与 JS 混用 # python 与 JS 混用 elem = browser.find_element('id', 'train_date') time.sleep(2) js_script = "arguments[0].readOnly = false; arguments[0].value = '2022-05-01'" browser.execute_script(js_script, elem) time.sleep(5)JS操作滚动条window.scrollTo(0, document.body.scrollHeight)十三、文件上传上传元素为input时 def test_file(self): # 实例化浏览器对象 browser = webdriver.Chrome() # 隐性等待 browser.implicitly_wait(10) # 自定义URL url = "http://www.zuohaotu.com/image-converter.aspx" # 访问url browser.get(url) time.sleep(2) file_elem = browser.find_element('id', 'newFile') file_elem.send_keys(r'D:\壁纸.jpg') time.sleep(2)元素为非input时,使用第三付库—win32gui# 打开上传网站 driver.get("https://tinypng.com/") paths = Path.cwd().parent # 触发文件上传的操作 driver.find_element_by_css_selector("section.target").click() time.sleep(2) # 一级顶层窗口 dialog = win32gui.FindWindow("#32770", "打开") # 二级窗口 comboBoxEx32 = win32gui.FindWindowEx(dialog, 0, "ComboBoxEx32", None) # 三级窗口 comboBox = win32gui.FindWindowEx(comboBoxEx32, 0, "ComboBox", None) # 四级窗口 -- 文件路径输入区域 edit = win32gui.FindWindowEx(comboBox, 0, "Edit", None) # 二级窗口 -- 打开按钮 button = win32gui.FindWindowEx(dialog, 0, "Button", None) # 1、输入文件路径 filepath = f"{paths}\\resources\\11.png" win32gui.SendMessage(edit, win32con.WM_SETTEXT, None, filepath) # 2、点击打开按钮 win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button)注意:当涉及文件路径时,有三种办法防止路径转义报错(一):使用正斜杠/(二):使用双反斜杠\\(三):前面加r参数多文件上传只需要写多几个send_keys方法即可十四、PO(POM)模式简介:PO模式是page object model的缩写,Page Object模式是Selenium中的一种测试设计模式,主要是将每一个页面设计为一个Class(封装在一个class类中),其中包含页面中需要测试的所有元素(按钮,输入框,标题等)的属性和操作,这样在Selenium测试页面中可以通过调用页面类来获取页面元素,这样巧妙的避免了当页面元素id或者位置变化时,需要改测试页面代码的情况。当页面元素id变化时,只需要更改测试页Class中页面的属性即可。(一)、为何使用PO模式?少数的自动化测试用例维护起来看起来是很容易的。但随着时间的迁移,测试套件将持续地增长脚本也将变得越来越臃肿庞大。如果变成我们需要维护10个页面,100个页面,甚至1000个呢?且页面元素很多是公用的,所以页面元素的任何改变都会让我们的脚本维护变得繁琐复杂,且变得耗时易出错。也就是说页面中有一个按钮 元素A"。该元素A在十个测试用例中都被用到了,如果元素A被前端更新了,我就要去修改这十个自动化用例所用道元素A的地方。如果有100个、1000个用例用到了元素A,那我可就疯了。而POM设计模式,会把公共的元素抽取出来,该元素被前端修改,只需要更新该元素的定位方式即可,用例不需要改动。换句话说,不管我多少测试用例,用到了该元素,我只重新修改元素的定位方式,重新能够获得该元素即可。(二)、PO模式的优势?代码冗余明显降低:二次封装Selenium方法和提取公共方法,提高代码复用性代码的阅读性明显提升:因为三层分级,将不同内容进行不同的封装,整体代码阅读性提升代码维护性明显提升:UI测试中,页面若经常变动,代码的维护量随之增多;因为三层分级,我们只需要修改页面对象的代码,如元素对象或者操作对象的方法,不用修改测试用例的代码,也不影响测试用例的正常执行降低代码耦合性(三)、PO模式三层详解基础层 : 二次封装Selenium的方法包括一些点击操作、find_element/find_elements操作,send_keys操作等此层主要是针对Selenium的方法进行二次封装,方便调用目前市面上的包括Helium、playwright等框架都是基于Selenium进行二次封装页面对象层: 用于放页面的元素和页面的动作一个页面即为一个Object对象,这个是PO模型的特征,一个页面对象里面包含有属性和方法,属性就是页面上的所有HTML标签,一个标签即理解为一个元素,一个find_element,HTML标签的点击事件则为一个方法测试用例层: 多个页面操作完成一个业务测试通过调用基础层(BasePage)对Selenium二次封装的方法,操作页面对象层中的动作,将多个动作连成一个业务测试流程,完成UI自动化测试。(四)、实战案例前言:以黑马的博学谷网站为例,使用PO模型,设计一个对于登录流程的UI自动化用例。(1)、项目整体架构Common:公共层,存放通用方法,如Selenium二次封装,即基础层,BasePagesConfig: 项目配置层,存放项目配置文件,如conf.yaml,db.iniData: 数据层,存放测试数据,或者数据驱动文件,关键字驱动等等Logs: 日志层,存放项目运行日志,按照日期区分Reports: 测试报告层,存放测试用例执行的HTML报告文件Pages: 页面层,一个网页即为一个页面.py文件,封装页面的元素及方法TestCases: 测试用例层,存放测试用例,按照pytest命名规则,以test_开头Utils: 工具层,封装工具类方法,如读取yaml,获取绝对路径,日志类封装等conftest.py: 夹具层,存放测试类或者测试方法的全局测试前置步骤和后置步骤run.py: 执行测试用例,生成测试报告(2)、编写BasePagefrom selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver import Chrome from Utils.GetPath import GetPath class BasePage: """selenium操作二次封装""" def __init__(self, driver: Chrome): self.driver = driver # 访问链接 def get_url(self, url, path=None): """ :param url: 后缀地址 :param path: 需要拼接的yaml文件地址 :return: """ url = GetPath.get_url(url, path) self.driver.get(url) return self # 点击操作封装 def click(self, locator: tuple): elem = self.driver.find_element(*locator) self.wait_elem(locator) # 此操作为强制点击 self.driver.execute_script("arguments[0].click({force:true})", elem) return self # 输入操作封装 def send_keys(self, locator: tuple, text): self.driver.find_element(*locator).send_keys(text) return self # 显性等待封装 def wait_elem(self, locator, timeout=10, poll=0.5): wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll) wait.until(expected_conditions.presence_of_element_located(locator)) return self(3)、编写Utils封装绝对路径:import os.path import yaml class GetPath: @classmethod def get_abs_path(cls, path): system_path = os.path.dirname(os.path.dirname(__file__)) data_path = os.path.oin(system_path, 'Config', path) return data_path @classmethod def get_url(cls, url, path): if path: path = cls.get_abs_path(path) with open(file=path, mode="r", encoding="utf-8") as file: url_data = yaml.load(stream=file, Loader=yaml.FullLoader) front_url = url_data[0]["host"] behind_url = url fill_url = front_url + behind_url return fill_url else: return url @classmethod def get_log_path(cls): system_path = os.path.dirname(os.path.dirname(__file__)) log_path = os.path.join(system_path, "Logs", "") return log_path日志类封装:import logging import datetime import os from Utils.GetPath import GetPath log_level_dict = { "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR, "critical": logging.CRITICAL } class LogUtil: def __init__(self, log_name, file_name, log_level, **kwargs): self.log_name = log_name self.file_name = file_name self.log_level = log_level def log(self): # 1、创建日志对象 my_log = logging.getLogger(self.log_name) # 2、设置日志级别 my_log.setLevel(log_level_dict[self.log_level]) # 3、创建控制台输出Handler,不存在才创建,存在直接用 if not my_log.handlers: handler = logging.StreamHandler() # 4.设置控制台Handler输出日志级别 handler.setLevel(log_level_dict[self.log_level]) # 5.设置日志输出格式 formatter = logging.Formatter('%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s') # 6.绑定handler输出格式 handler.setFormatter(formatter) # 7.添加到logger对象中 my_log.addHandler(handler) # 8.级别排序:CRITICAL >ERROR >WARNING >INFO >DEBUG # 9.写入文件 file_handler = logging.FileHandler(self.file_name, encoding='utf-8') # 10.设置日志文件级别 file_handler.setLevel(log_level_dict[self.log_level]) # 11.设置日志文件输出格式 file_handler.setFormatter(formatter) # 12.添加到logger对象中 my_log.addHandler(file_handler) return my_log def log_util(): log_path = GetPath.get_log_path() current_time = datetime.datetime.now().strftime("%Y-%m-%d") log_file = os.path.join(log_path + "AutoUI" + current_time + ".log") print(log_file) util = LogUtil("log", log_file, "debug").log() return util 读取Yaml封装:import yaml from Utils.GetPath import GetPath class ReadYamlUtils: @classmethod def read_data(cls, path, key): path = GetPath.get_abs_path(path) with open(file=path, mode="r", encoding="utf-8") as file: data = yaml.load(stream=file, Loader=yaml.FullLoader) return data[0][key] (4)、编写Pages以页面为对象把元素和方法进行封装from selenium.webdriver.common.by import By from Common.BasePage import BasePage from Utils.Logger import log_util class OnlineClass(BasePage): # 定义类属性 url = "/user/login?refer=https%3A%2F%2Fwww.boxuegu.com%2F" button_locator = (By.XPATH, "//span[text()='登录']") submit_locator = (By.XPATH, "//button[text()='登录']") username_locator = (By.XPATH, '//*[@placeholder="请输入手机号或邮箱" and @class="el-input__inner"]') password_locator = (By.XPATH, '//*[@placeholder="请输入密码" and @class="el-input__inner"]') # 登录操作封装 def login(self, username, password): """ :param username: 账号 :param password: 密码 :return: 对象本身 """ self.click(self.button_locator) locator = ['xpath', "//button[text()='登录']"] self.wait_elem(locator).send_keys(self.username_locator, username)\ .send_keys(self.password_locator, password)\ .click(self.submit_locator) log_util().debug("登录操作") return self # 加载 def load(self): self.get_url(self.url, path="conf.yaml") log_util().debug("加载操作") return self # 获取操作提示 def get_errmsg(self): tips = self.driver.find_elements(By.XPATH, '//div[@class="el-form-item__error"]') log_util().debug("提示符文字获取操作") return [index.text for index in tips] # 清除内容 def clear(self): log_util().debug("清楚输入框内容操作") self.driver.find_element(By.XPATH, '//*[@placeholder="请输入手机号或邮箱" and @class="el-input__inner"]').clear() self.driver.find_element(By.XPATH, '//*[@placeholder="请输入密码" and @class="el-input__inner"]').clear() (5)、编写Data为了方便获取参数,没有使用yaml文件,使用了py文件login_data = [ ('', '', ['请输入手机号或邮箱!', '请输入密码!']), ('13536237223', '', ['请输入密码!']), ('13536237223', '123456', ['请输入正确密码,连续输入错误密码,账号将被锁定']) ] (6)、编写Config此文件主要是控制项目的切换,可自行添加参数,包括项目环境,地址等等- host: "https://passport.boxuegu.com" enviorment: "TEST"(7)、编写TestCases由于Pages封装的方法返回值为Page类对象本身,所以在测试用例设计中,可以使用链式调用import pytest from Data import Login from Pages.OnlineClass import OnlineClass class TestClass: @pytest.mark.parametrize("login_data", Login.login_data) def test_login(self, get_driver, login_data): # 解包 username, password, result = login_data # 全局夹具对象 browser = get_driver # po模型对象 login_page = OnlineClass(driver=browser) # 链式调用 value = login_page.load().login(username, password).get_errmsg() assert value == result (8)、编写conftest.py当有多个不同身份登录时,可以写不同身份的夹具方法,如学生和老师,在登录测试用例中,对于老师和学生不同的登录,就可以按照不同的夹具实现,因为夹具里面可能会定义一些登录账号和密码,需要注意的是scope参数,如果是class,则一个类里面所有测试用例必须是在同一个页面进行操作,否则找不到元素,这是因为driver对应着一个页面,如果上一个页面跳转到其他页面,而其他用例的dirver还是原来的,那么肯定是找不到元素的import pytest from selenium import webdriver @pytest.fixture(scope="class") # 必须是测同一个页面才能用class,否则不要设置作用域,会找不到元素哦 def get_driver(): # 实例化浏览器对象 browser = webdriver.Chrome() # 隐性等待 browser.implicitly_wait(10) browser.maximize_window() yield browser browser.quit()
2023年06月
2022年10月
可靠性:数据库是应用程序的核心组件之一,因此可靠性是其最重要的特质之一。款优秀的数据库应该能够保证数据的安全性和可靠性,能够在各种情况下保持稳定运行。 性能:数据库的性能对于应程序的响应速度和用户体验至关重要。一款优秀的数据库应该能够提供高效的数据读写能力,能够快速响应用户请求。 可扩展性:随着业务的发展,数据库的数据量和访问量也会不断增加。一款优秀的数据库应该能够支持水平和垂直扩展,能够满足不同规模的业务需求。 安全性:数据库中存储着应用程序的核心数据,因此安全性是其必备的特质之一。一款优秀的数据库应该能够提供多层次的安全保障,包括数据加密、访问控制、审计等功能。 易用性:数据库的易用性对于开发人员和管理员来说都非常重要。一款优秀的数据库应该能够提供简单易用的管理界面和API,能够快速上手和使用。 兼容性:数据库需要与各种应用程序和工具进行集成,因此兼容性也是其必备的特质之一。一款优秀的数据库应该能够支持多种编程语言和操作系统,能够与各种应用程序和工具进行无缝集成。