自动化测试
浏览器驱动根据浏览器的不同进行安装pip安装客户端库只需要在命令行中输入。
软件测试
就业方向(功能测试+任一种)
python+测试工具
框架的完善
- 过程控制
- 日志记录
- 测试报告
- UI画面回放
技术栈
allure,yaml,jenkins,log
Charles,knife4j
工具类
自动化测试
-
WEB测试:Selenium,Robotramework:这是一个通用的自动化测试框架,支持关键字驱动的测试方法。它可以用于功能测试、验收测试和其他自动化测试任务,支持多种库和工具集成。
App测试:Appium(app自动化测试),熟悉airtest框架,掌握常用adb命令
-
接口测试工具:Postman,apifox,swagger
-
性能测试工具:JMeter,LoadRunner,
locust (用于分布式性能测试,能够模拟大量用户对Web应用程序进行负载测试。基于Python,支持分布式测试,易于编写测试脚本和查看结果。),grafana监控平台
-
缺陷管理工具:禅道,Azure DevOps,Jira
-
抓包工具:fiddler,Charles
熟悉测试流程,能够根据业务需求设计和输出测试用例,执行测试流程,并跟进缺陷修复;
熟练掌握python,java基础知识,能够使用pytest等测试相关框架编写自动化测试脚本;
熟悉Web测试,可以使用Selenium+Unittest+Python进行WEB端功能自动化测试及脚本维护;
熟悉接口测试,可以使用pytest+requests+allure报告进行接口测试;
熟悉性能测试,了解jmeter和locust性能测试和性能监控等命令
熟练使用Postman,apifox,fiddler,Charles,LoadRunner,Jenkins、禅道等测试及持续集成工具;
熟练使用XMind提取测试点,编写测试用例,保证较高的测试覆盖率;
熟悉Mysql数据库和对sql语句的编写,对事务,索引,锁都有进一步理解;
熟悉Linux常用的shell命令和git版本控制命令;
熟悉Spirng、SpringMVC、Mybatis等技术栈,拥有Springboot+MyBatisPlus的项目开发经验。
技术架构:Springboot+Mybatisplus+Springsecurity+Redis+Mysql+Vue+Swagger+Linux
项目介绍:宇你音乐是基于Springboot+Vue开发的音乐在线网站,项目分为客户端跟管理端。客户端拥有用户登录注册,歌曲跳转播放,歌曲收藏,歌词实时显示,歌单评论及点赞,歌单评分,个人信息修改等功能。管理端拥有管理员登录,用户管理,歌手管理,歌单管理,歌曲管理,评论管理等多个功能模块。
1. 使用MybatisPlus快速搭建对应控制层,业务实现层,数据访问层及实体类;
2. 利用 Redis 缓存用户信息,减少对数据库的频繁访问,提高查询效率;
3. 使用拦截器,用Cookie记录用户状态,解决 http 无状态问题,增强了系统安全性;
4. 利用spring提供的BeenUtils工具类实现集合元素映射以及单个对象映射;
5. 使用阿里云Oss对象存储服务上传所需的音乐文件,图片文件等;
6. 使用Hutool工具包生成图片验证码,增强用户登录安全性,防止恶意攻击;
7. 使用Springsecurity对数据库进行密码加密,确保密码存储在数据库中的安全性;
8. 将项目打成Jar包,在Linux上进行远程项目部署。
歌手模块自动化接口测试
使用knife4j生成的在线接口文档,详细分析Api接口并编写相关接口测试用例
使用selenium+pytest编写自动化测试用例,执行测试用例并重现测试中出现的bug
使用pytest的参数化功能,为测试用例提供多个用户数据,实现数据驱动测试
使用allure生成测试报告,包含测试用例的执行结果,断言详情等,便于缺陷的追踪与修复
有较强的自主学习能力,熟练使用ChatGPT、Google 等工具,能快速上手新技术; 工作细心,善于发现问题,有独立解决问题的能力; 对互联网行业有热情,善于总结,并时常发表博客记录学习; 抗压能力强,有较强的沟通技巧及团队合作精神。
考点
1引导问你会的
自我介绍+技术优势+总结尾
测试流程?
使用过的测试工具
为什么选择测试?
为什么写博客?博客的点击量有多少
在学校参加过的社团,哪个印象深刻,为什么?
1.退出的不同?
driver.close() # 关闭浏览器,窗口退出
driver.quit() # 关闭窗口
2.selenium底层原理
webdriver协议(Http接口),webdriver是浏览器的驱动
对一个不确定的功能测试,首先要明确需求
升级测试
回归测试考虑旧数据是否正常
怎么展开测试
- 需求分析:在测试开始前,深入了解软件的功能需求和业务目标,确保测试覆盖所有关键点。
- 测试计划:制定全面的测试计划,包括测试目标、范围、资源需求、时间表和风险评估。
- 测试设计:设计具体的测试用例,包含前置条件、预期结果和测试步骤。编写自动化测试脚本(如果适用)。
- 测试环境准备:搭建测试环境,确保测试与生产环境相似,配置相关的硬件、软件和网络条件。
- 测试执行:根据测试用例执行测试,记录实际结果,并与预期结果对比,发现并记录缺陷。
- 缺陷管理:报告缺陷并跟踪修复进展,确认问题解决后重新测试。
- 测试总结:编写测试报告,总结测试结果,评估软件的质量和测试覆盖度,并提供改进建议。
测试必备工具
fiddler
Jenkins(github actions,gitlab CI)
- 概述:Jenkins 是一个开源的自动化服务器,广泛用于持续集成和持续交付(CI/CD)。它可以自动化构建、测试、部署等软件开发流程。
- 特点:
- 插件系统:Jenkins 拥有丰富的插件生态系统,支持各种功能扩展,如代码质量分析、自动化部署等。
- 可定制性:通过配置文件和插件,可以高度定制 Jenkins 的工作流程。
- 分布式构建:支持分布式构建,可以将构建任务分配到多台机器上,提高构建效率。
allure
Robotramework
Jira
基础概念
测试分类
测试阶段
代码可见度
质量模型
功能,性能,兼容,易用,安全,可靠性,移植性,维护性。
测试流程
测试用例
功能点用例编写:
- 分为正向,逆向(功能点较小的情况下);
- 分为页面所拥有的属性(功能页面较大);
1.列出所有参数及其对应规则,包括必填参数及非必填参数(提取测试点)
2.编写正向,逆向测试用例
正向:…成功(必填参数/全部参数填写正确)
逆向:…失败(必填参数:空,长度不符,类型不符,规则不符)
作用:防止漏测,实施规范
提取测试点
设计测试用例
正向用例:一条尽可能覆盖多条(数测试模块中最多正向方向的)
逆向用例:每一条都是单独用例
用例设计编写格式:
测试方法
编写测试用例的时候,通常可以采用 条件组合、边界值、错误猜测 等方法。
4.1等价类划分(针对穷举场景)
适用场景:表单类页面元素测试使用(输入框、单选按钮、下拉列表)
步骤:
1.明确需求
2.确定有效等价(满足需求),无效等价(不满足需求)
3.根据上面设计数据,编写用例
案例:验证6-10位qq合法性
4.2边界值测试方法(针对边界限定问题)
边界范围结点(7个点)
步骤:
1.明确需求
2.确定有效等价,无效等价
3.确定边界值
4.根据上面设计数据,编写用例
优化(7点化5点)
上点,内点是必须的,离点按照开内闭外,开区间选择内部离点,闭区间选择外部离点
单个输入框常用的方式是:边界+等价类
4.3判定表法(针对多条件依赖测试)
步骤:
1.明确需求
2.画出判定表
- 列出条件桩和动作桩
- 填写条件项,
- 根据条件项组合确定动作项
- 简化,合并相似规则
3.设计数据,编写用例
4.4场景法
业务测试覆盖需要使用流程图
先测试业务,再测试单功能,单模块,单页面
流程图使用椭圆做开始结束,使用棱形表判断,使用方框表语句
4.5错误推荐法
缺陷
概念:软件中存在的各种问题不符合业务功能预期,称为缺陷,简称bug;
类型:
- 少功能
- 多功能
- 功能错误
- 隐形错误
- 易用性
产生原因:
缺陷类型:
- 功能错误
- ui页面错误
- 兼容性
- 数据库
- 易用性
- 架构缺陷
工作流程:
注册案例
缺陷编写
发现bug后首先要确保缺陷可以复现
描述缺陷
登录模块案例
3.1登录测试点
登录测试点分为
测试某一个点时(无论正向还是逆向),其他的点也应该默认是正向的
非功能主要是测试五大浏览器的兼容性,兼容与UI布局 还有滑块的拖动。
3.2登录用例编写
用例标题写法:期望结果(实现依据原因)
优先级成功的写p0
项目,前置条件一般不变,测试数据跟前置条件可以先写
测试步骤即实际操作过程,若是边界值要使用次数表明,预期结果根据预测而定
用例-模块-编号 | 用例标题 | 项目/模块 | 优先级 | 前置条件 | 测试步骤 | 测试数据 | 预期结果 |
---|---|---|---|---|---|---|---|
login-01 | 发送验证码成功(滑块到指定位置) | 滑块 | p0 | 1.输入手机号 2.打开滑块界面 | 1.拖动滑块到指定位置 | 1.手机号:正确手机号 | 手机号收到验证码成功 |
login-02 | 发送验证码失败(滑块一次未到指定位置) | 滑块 | p1 | 1.输入手机号 2.打开滑块界面 | 1.拖动滑块一次未到指定位置 | 1.手机号:正确手机号 | 1.手机号没有收到验证码2.滑块抖动效果1次3.回到初始位置 |
login-03 | 发送验证码失败(滑块三次未到指定位置) | 滑块 | p1 | 1.输入手机号 2.打开滑块界面 | 1.拖动滑块三次未到指定位置 | 1.手机号:正确手机号 | 1.手机号没有收到验证码2.滑块抖动效果3次3.回到初始位置 |
login-04 | 发送验证码失败(滑块五次未到指定位置) | 滑块 | p1 | 1.输入手机号 2.打开滑块界面 | 1.拖动滑块五次未到指定位置 | 1.手机号:正确手机号 | 1.手机号没有收到验证码2.滑块抖动效果5次3.回到初始位置 |
login-05 | 发送验证码失败(滑块错误次数达到6次) | 滑块 | p1 | 1.输入手机号 2.打开滑块界面 | 1.拖动滑块六次未到指定位置 | 1.手机号:正确手机号 | 1.手机号没有收到验证码2.滑块抖动效果5次3.滑块消失,提示重试 |
基础测试最后有总结,可以观看5-14-总结_哔哩哔哩_bilibili快速回顾
python
9/13
input("enter your name") # input函数只能输入str,所以输入数字时需要用int(str)强转
\ 表转义, # 表注释 ,e9表科学计数法中的10的9次方 , r '' 单引号中内容不转义 // 表整除
''' damage
... kewu ''' # 表多行内容
a = 'ABC',创建了字符串'ABC'和变量a,并把a指向'ABC'
b = a,创建了变量b,并把b指向a指向的字符串'ABC':
ord("A") 65 # ord()函数获取字符的整数表示,
chr(66) 'B'# chr()函数把编码转换为对应的字符
len('中文'.encode('utf-8')) 1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
'Hello, %s' % 'world' # 先正常写字符串,接着空格 % '替换字符'
'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) # :后面的.1f指定了格式化参数(即保留1位小数)
print(f"hello, {x}") # f-string格式化,这里就输出值
f"断言失败: {count=}" # 这里会将count变量及count值一起输出,是新特性
2.1list和tuple
python中一种内置的数据类型列表:list,list是可变的,而str是不可变,尽管指向可以改变
a = ['porter','damage'] # 通过a[-1] 或 a[len(a) - 1]获取到数组最后一个元素
a.append("fly") # 往数组末尾加元素,删除末尾元素用a.pop()
a.insert(1,"dd") #往指定索引处插入元素,删除指定索引元素a.pop(1),替换某个索引处元素直接赋值
p = ['asp', 'php'] s = ['python', 'java', p, 'scheme'] #要拿到'php'可以写p[1]或者s[2][1]
另一种有序列表叫元组:tuple,tuple一旦初始化就不能修改,因此比list更安全
t = (1, 2) # 基本定义 t = () 只有1个元素的tuple定义时必须加一个逗号,来消除歧义
# turple里面的元素也可以是列表,修改列表中元素是可以的,即指向元素不变
2.2条件判断
age = 3
if age >= 18: # 这里的: 相当于{},他会把下面的缩进内容一起执行了
print('adult')
elif age >= 6:
print('teenager')
else:
print("kid")
2.3循环与匹配
匹配
score = "B"
match score:
case 'A':
print("A")
case 'B':
print("B")
case _:
print("表其他任何情况")
循环(for in ; while)
在循环过程中,也可以通过
continue
语句,跳过当前的这次循环,直接开始下一次循环。
range(101) # 生成0-100的list列表
for x in [1,2,3]:
sum = sum + x;
print(sum)
sum = 0
n = 99
while n > 0:
sum = sum + n
n = n - 2
print(sum)
2.4字典
dict
字典即dict,即其他语言中的Map,使用键-值(key-value)存储,具有极快的查找速度。
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85} # 直接赋值,里面用逗号隔开
print(d['Michael'])
print("Bob"in d ) # 判断是否有该key
d.get('BOb',10) # dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:
set 也是一组key的集合,但不存储value.在set中,没有重复的key。
重复元素在set中自动被过滤:重复元素在set中自动被过滤
remove(key)
方法可以删除元素:
2.5函数
hes(254) # 可以将十进制254转为16进制0xfe 255/16 = 15余14
0x表16进制,余数先出来,先从右边开始,商大于16就一直除 15表f,14表e,从右边写起
定义函数
def my_abs(x): # 函数名(x):
if not isinstance(x, (int, float)): # 对参数类型做检查,只允许整数和浮点数类型的参数
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
空函数
def nop():
pass # pass 起到占位作用,可以让代码运行起来
返回多个值
Python的函数返回多值其实就是返回一个tuple ,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值
import math
def quadratic(a, b, c):
k = b*b - 4*a*c
x1 = (-b + math.sqrt(k)) / (2*a)
x2 = (-b - math.sqrt(k)) / (2*a)
return x1, x2
2.5.1函数参数
函数的参数(默认参数必须指向不变对象)默认参数就是在参数后面赋值,如果没有传参就默认使用该默认值 例如:def power(x, n=2): def add(desc:str = None): #desc:str=None,即desc只能是str类型,并且是默认参数,若没有传递就是None
# 假设参数定义成L = [] ,因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了
def add_end(L=None): # 这里的None就是不变对象,使用之后就可以避免多次调用后出现end添加多次
if L is None:
L = []
L.append('END')
return L
可变参数, 直接在参数前面加*,这里的可变具体是指参数的数量
def calc(numbers): # 这里的numbers可以变为*numbers,这样就可以直接calc(1,2,3)随意参数调用
sum = 0
for n in numbers:
sum = sum + n * n
return sum
calc([1, 2, 3]) # 组装一个list调用,假设已有一个l列表,我们也可以calc(*l)去调用该方法
calc((1, 3, 5, 7)) # 组装一个tuple调用
关键字参数,** kw
允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict(键值对)
可以扩展函数的功能,因此放于函数参数的最后一位
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
# 调用,假设已有一个现成字典d,那么可以则直接person('Adam', 45,**d)
person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'} # 输出
命名关键字参数 即使用*隔开,
*
后面的参数被视为命名关键字参数
def person(name, age, **kw):
if 'city' in kw: # 检测是否传入city参数
# 有city参数
pass
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错
def person(name, age, *args, city, job):
参数组合,顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
2.5.2递归函数
使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
针对尾递归优化的语言可以通过尾递归防止栈溢出。
2.5.3匿名函数
# 匿名函数(lambda)
add = lambda x, y: x + y #lambda后接参数,冒号后面接方法
print(add(5, 3))
2.6高级特性
2.6.1切片(几到几)
L[0:3]
表示,从索引0
开始取,直到索引3
为止,但不包括索引3
。即索引0
,1
,2
,正好是3个元素。还可以省略0,即L[:3]。
倒向切片:L[-3:] 表示取后面三个元素,由于倒数第一个元素的索引是-1
,所以L[-3:-1]即取索引为-3.-2的元素。
L[:10:2] # 前10个数,每两个取一个
L[::5] # 所有数,每5个取一个
L[:] # 列出所有数
'abcde'[:3] # 'abc' 对于字符串同样适用
def trim(s):
start = 0
while start<len(s) and s[start].isspace(): # 找到非空白字符的索引
start += 1
end = len(s) -1
while end >= start and s[end].isspace():
end -= 1
return s[start:end+1] # hello s[0:5],这里的end是索引
2.6.2迭代
通过collections.abc
模块的Iterable
类型判断一个对象是可迭代对象
from collections.abc import Iterable
isinstance(123, Iterable) # 整数是否可迭代
False
各种迭代
for key in dict # 遍历字典中的key
for value in d.values() # 遍历字典中的value
for k,v in dict.item() # 遍历字典中的key跟value
for ch in 'ABC': # 遍历字符串
for i, value in enumerate(['A', 'B', 'C']):
print(i, value) # 可打印索引跟列表元素
小案例
def findMinAndMax(L):
if not L: # 判断是否为空列表
return (None, None)
min_val = max_val = L[0] # 设置第一个元素为最值
for num in L[1:]: # 遍历接下来元素
if num < min_val:
min_val = num
if num > max_val:
max_val = num
return (min_val, max_val)
2.6.3列表生成器
2.9模块
功能模块我们可以在里面写方法,接着我们在要使用的类中引入该类,
2.9.1sys模块
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 以上两行运行程序在linux,mac,unix上运行,并且编码为utf-8
' a test module ' # 模块注释
__author__ = 'Michael Liao' # 作者
import sys
def test():
args = sys.argv # argv是sys中的一个属性,args则是一个列表,一般默认存储[hello,world]
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__': # 编译时,会把特殊变量_name_变为_main_,并调用上述方法,故此我们可以在这里面添加一些测试代码
test()
2.9.2安装第三方模块
通过python的包管理工具pip去安装各种第三方库
作用域
__xxx__ # 这样的变量是特殊变量
_xxx # 这样的函数或变量就是非公开的(private)
外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public。
2.10面向对象编程
2.10.1类和实例
类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;创建实例是通过**类名+()**实现的;
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量(private)
变量名类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量
不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
eg: self._color = Color,
def get_score(self):
return self.__score
def set_score(self, score):
self.__score = score # 通过get,set方法访问私有变量
2.10.2获取对象信息
type(123) # 直接获取类
isinstance('a', str) # 判断是否属于后面数据类型
hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
# 从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
2.12错误与异常处理d
2.12.1错误处理
概念:当我们认为某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行finally
语句块,至此,执行完毕。一般只需要在运行的程序处加上try except,他会层层往下调用,直至找到根源。
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e: # 错误类型
print('ValueError:', e) # pass则表直接退出程序
except ZeroDivisionError as e: # 如果其是上述子类,则该异常永远捕获不到,因被截胡
print('ZeroDivisionError:', e)
else: # 如果没有被上述异常捕获就会执行eles语句
print('no error!')
finally: # 这句是一定执行的,但可以没有
print('finally...')
print('END')
记录错误
Python内置的logging
模块,可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去
raise
语句如果不带参数,就会把当前错误原样抛出。由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
2.12.2调试
断言:python -O err.py,关闭后,你可以把所有的
assert
语句当成pass
来看。(这里的 O 是大写字母)assert condition, "error_message"
condition
是你希望测试的条件。它应该是一个布尔表达式(即True
或False
)。"error_message"
是一个可选的错误消息,当condition
为False
时,将会显示这个消息。
def foo(s):
n = int(s)
assert n != 0, 'n is zero!' # 断言n!=0为true,如果fasle则会报assertError:n is zero!
return 10 / n
logging
logging
的好处,它允许你指定记录信息的级别,有debug
,info
,warning
,error
等几个级别,当我们指定level=INFO
时,logging.debug
就不起作用了。
另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和日志文件
import logging
logging.basicConfig(level=logging.INFO) # 设置级别
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
pdb(启动Python的调试器pdb):让程序以单步方式运行,可以随时查看运行状态
pdb.set_trace() # 运行到这里会自动暂停
p n # 查看某个变量的值
c # 继续执行下一步
2.12.3单元测试
2.13IO编程d
2.16快速回顾
# 列表
fruits = ["apple", "banana", "cherry"]
fruits.append("orange") # 添加元素
print(fruits)
# 元组
point = (10, 20)
print(point)
# 集合
numbers = {1, 2, 3, 4}
numbers.add(5) # 添加元素
print(numbers)
# 字典,直接 对象[key] 就可以获得value
person = {"name": "Alice", "age": 30}
print(person["name"])
# 文件读写,content = file.read()
with open(filepath,'rb') as f: # r是read,w是write,b是二进制形式
json.dump(data,f,indent = 4) #将data以JSON格式写入到 file 对象中,并且使用 4 个空格缩进来美化格式
content = json.load(file) #加载文件中的json数据,并且转化成python对象
Selenium
- 9/12
5.1原理与安装
原理:
- 自动化程序调用Selenium 客户端库函数(比如点击按钮元素)
- 客户端库会发送Selenium 命令 给浏览器的驱动程序
- 浏览器驱动程序接收到命令后 ,驱动浏览器去执行命令
- 浏览器驱动程序获取命令执行的结果,返回给我们自动化程序
- 自动化程序对返回结果进行处理
总结:python------>selenium----->浏览器的驱动------>浏览器
安装浏览器驱动及pip安装客户端库
浏览器驱动根据浏览器的不同进行安装
pip安装客户端库只需要在命令行中输入pip install selenium
5.2选择元素
选择元素时需要选择到最深层的,如输入就是input,点击就是button
from selenium.webdriver.common.by import By
# 初始化代码 ....
# wd 是webdriver wd.find_element(By.XPATH,'') 获得的是webElement
wd.find_element(By.ID, 'username').send_keys('byhy')
wd.find_elements(By.CLASS_NAME, 'password').send_keys('sdfsdf')
wd.find_element(By.TAG_NAME, 'div').send_keys('sdfsdf')
wd.find_element(By.CSS_SELECTOR,'button[type=submit]').click()
使用 find_elements
选择的是符合条件的 所有
元素, 如果没有符合条件的元素, 返回空列表
使用 find_element
选择的是符合条件的 第一个
元素, 如果没有符合条件的元素, 抛出 NoSuchElementException 异常
5.3等待元素
5.3.1隐式等待
隐式等待也叫全局等待,后续所有find_element都会遵循这个
隐式等待是指 WebDriver 在查找元素时,如果元素没有立即出现,则会等待最多指定的时间来查找元素
wd.implictly_wait(0) # 暂时取消,先取到页面元素
wd.implicitly_wait(10) # 如果找不到元素, 每隔 半秒钟 再去界面上查看一次, 直到找到该元素, 或者 过了10秒 最大时长。
5.3.2强制等待
time.sleep(1)
webdriverwait(driver,10).untile(EC.element_to_be_clickable((By.ID,'') # 最多等待十秒除非
5.4操作元素
element.clear() # 清除输入框中内容
element.send_keys('dd') #输入框中输入内容
element.text # 获取元素 展示在界面上的 文本内容
element.get_attribute('value') # 获取输入框中内容,value替换为innerText 和 textContent 的区别是,前者只显示元素可见文本内容,后者显示所有内容(包括display属性为none的部分)
element.get_attribute("class")# 获取元素的属性值
element.get_attribute('outerHTML')# 整个元素对应的HTML文本内容,内部的HTML文本内容则innerHTML
wd.quit() # wd是webdrive对象,关闭浏览器窗口
from selenium.webdriver.common.action_chains import ActionChains
ac = ActionChains(driver) # 获取鼠标对象
# 鼠标移动到元素上
ac.move_to_element(
driver.find_element(By.CSS_SELECTOR, '[name="tj_briicon"]')
).perform() # 执行上面的移动动作
5.5css表达式
直接子元素是直接包含在标签里面(只有一层),后代元素就是在标签里面。
在浏览器中的element中按住ctrl+f可以通过.class属性
来验证选中的元素。
css 选择器支持通过任何属性来选择元素,语法是用一个方括号 []
。
a[href*="miitbeian"] # 选择a节点,里面的href属性包含了 miitbeian 字符串
a[href^="http"] #选择a节点,里面的href属性以 http 开头
a[[href$='.com'] #选择a节点,里面的href属性以 gov.cn 结尾
div[class=misc][ctype=gun] #选择标签有多个属性的
5.5.1组选择
elements = wd.find_elements(By.CSS_SELECTOR, 'div,#BYHY') #选择div标签中id为BYHY
wd.find_element(BY.CSS_SELECTOR,'.animal','plant') # 选择所有class 为 plant 和 class 为 animal 的元素
5.5.2按次序选择子节点
span:nth-child(2) # span标签下的第二个节点
p:nth-last-child(2) # p标签下的倒数第二个节点
span:nth-of-type(1) # 按类型
p:nth-last-of-type(2) # 按类型倒数
p:nth-child(even) # 父元素的偶数结点
p:nth-of-type(odd) # 父元素的奇数结点
5.6frame窗口切换
iframe 元素非常的特殊, 在html语法中,frame 元素 或者iframe元素的内部 会包含一个 被嵌入的 另一份html文档。
在我们使用selenium打开一个网页是, 我们的操作范围 缺省是当前的 html , 并不包含被嵌入的html文档里面的内容。
切换元素选择范围
wd.switch_to.alert # 切换到弹出框
wd.switch_to.frame(wd.find_element(By.TAG_NAME, "iframe")) # 切换到该层窗口
wd.switch_to.default_content() # 切换为默认html标签中
切换窗口
mainWindow = wd.current_window_handle # mainWindow变量保存当前窗口的句柄
wd.switch_to.window(mainWindow) # 切回老窗口
for handle in wd.window_handles: # 遍历所有已经打开的窗口
# 先切换到该窗口
wd.switch_to.window(handle)
# 得到该窗口的标题栏字符串,判断是不是我们要操作的那个窗口
if 'Bing' in wd.title:
# 如果是,那么这时候WebDriver对象就是对应的该该窗口,正好,跳出循环,
break
5.7选择框
select_by_value('')
select_by_index(0)
select_by_visible_text('') # 根据选项的 可见文本 ,选择元素。
deselect_by_value() # 根据选项的value属性值, 去除 选中元素,value可替换为index...
deselect_all # 去除选中的所有元素
多选
# 导入Select类
from selenium.webdriver.support.ui import Select
# 创建Select对象
select = Select(wd.find_element(By.ID, "ss_single"))
# 清除可选框
select.delect_all();
# 通过 Select 对象选中小雷老师
select.select_by_visible_text("小雷老师")
select.select_by_value("小红老师")
5.8实战技巧
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.implicitly_wait(5)
driver.get('https://www.baidu.com/')
from selenium.webdriver.common.action_chains import ActionChains
ac = ActionChains(driver) # 获取鼠标对象
# 鼠标移动到 元素上
ac.move_to_element(
driver.find_element(By.CSS_SELECTOR, '[name="tj_briicon"]')
).perform() # 执行上面的移动动作
调整元素到可见区
driver.execute_script("arguments[0].scrollIntoView({block:'center',inline:'center'})", job)
# js对象的 scrollIntoView 方法,就是让元素滚动到可见部分
#block:'center' 指定垂直方向居中 inline:'center' 指定水平方向居中
冻结窗面(在控制台输入后,过五秒会进入debugger模式,这五秒内可以把鼠标放在会消失的元素上)
setTimeout(function(){debugger}, 5000)
5.8.0案例
# 让页面最大化的方法
driver.maximize_window()
# 获取页面的大小 {'width': 1552, 'height': 840}
print(driver.get_window_size())
# 获取当前网站路径 https://www.bilibili.com/
print(driver.current_url)
# 获取所有窗口代数,并存储在列表中
print(driver.window_handles)
print(driver.current_window_handle) # 当前窗口
driver.get_screenshot_as_file('1.png') # 保存当前图片
time.sleep(3)
# 先定位到上传文件的 input 元素
ele = wd.find_element(By.CSS_SELECTOR, 'input[type=file]')
# 再调用 WebElement 对象的 send_keys 方法(可多次)
ele.send_keys(r'h:\g02.png')
入门案例
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Edge() # 启动浏览器
driver.get("https://baidu.com") # 进入测试页面
A = driver.find_element(By.XPATH,'//*[@id="kw"]') # 输入框元素
B = driver.find_element(By.XPATH,'//*[@id="su"]') # 按钮元素
A.send_keys("北京时间") # 输入内容,还可以导入Keys包,里面封装了ctrl键
B.click() # 点击按钮
time.sleep(1) # 强制等待网页渲染
html = driver.page_source # 获取网页源码
count = html.count('damag') # 统计次数
assert count > 1,f"断言失败:{count=}"
driver.get_screenshot_as_file('page.png') # 截屏
driver.close() # 关闭浏览器
测试页面(web自动化框架封装)
Pom(page object model)+Pytest
对页面进行类的封装,页面中有三个元素,一个登录操作,故class类中有三个属性及一个行为
属性需要我们去先选中元素,然后对其实例化,实例化是为了后面能更好的使用
pages.py
class LoginPage():
ipt_username = LazyElement(BY.CssSelector, '#TANGRAM__PSP_11__userName')
ipt_password = LazyElement(By.XPath,'//*[@id="TANGRAM__PSP_11__password"]')
# 开始时不检查是否存在该元素,而且该元素时有参数变化,所以在可以在控制台术定位该元素$x('//p[@id='']')
btn_login = LazyElement(By.XPATH,'//*[@id="TANGRAM__PSP_11__submit"]',check_on_init=False)
def login(self,username,password):
self.sendkeys(self.ipt_username,username) # 操作封装在一起,这里是以ipt_username.sendkeys(username),即在sendkeys中先写调用对象,后接传递内容信息
self.sendkeys(self.ipt_password,password)
self.click(self.btn_login)
test_web
from selenium import webdriver
from selenium.webdriver.common.by import By
from pages import LoginPage # 从文件中引入类名(class)
dirver = webdriver.Edge()
dirver.get("http://baidu.com")
dirver.find_element(By.XPATH, '//*[@id="s-top-loginbtn"]').click()
def test_login(): # test_开头表测试方法
page = LoginPage(dirver) # 实例化登录对象
page.login("damage","123456") # 对象调用方法行为
input("please enter")
5.8.1对话框
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.implicitly_wait(5)
driver.get('https://cdn2.byhy.net/files/selenium/test4.html')
# --- alert ---
driver.find_element(By.ID, 'b1').click()
# 打印 弹出框 提示信息
print(driver.switch_to.alert.text)
# 点击 OK 按钮 ,如果是取消就是accept改为dismiss()
driver.switch_to.alert.accept()
Prompt 弹出框:需要用户输入一些信息,提交上去
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.implicitly_wait(5)
driver.get('https://cdn2.byhy.net/files/selenium/test4.html')
# --- prompt ---
driver.find_element(By.ID, 'b3').click() # 点击后弹出
# 获取到弹出的alert 对象
alert = driver.switch_to.alert
# 打印 弹出框 提示信息
print(alert.text)
# 输入信息,并且点击 OK 按钮 提交
alert.send_keys('web自动化 - selenium')
alert.accept()
5.9XPATH
绝对路径:从根结点,按照/分割找到目标元素
相对路径:// div 表所有标签名为
div
的元素
选择 所有的 div 元素里面的 直接子节点 p , xpath,就应该这样写了 //div/p
如果使用CSS选择器,则为 div > p
,
通配符 *
eg: //div/* 表div标签下的所有直接子节点
5.9.1根据属性选择
标签名[@属性名=‘属性值’]
// *[@id = 'west'] # 根据id属性选择
// p[@class="capital huge-city"] p标签中的类
xpath也有组选择, 是用 竖线 隔开多个表达式
要选所有的 class 为 single_choice 和 class 为 multi_choice 的元素,可以使用
//*[@class='single_choice'] | //*[@class='multi_choice']
等同于CSS选择器
.single_choice , .multi_choice
要选择 class 为 single_choice 的元素的 所有
前面的兄弟节点,这样写
//*[@class='single_choice']/preceding-sibling::*
要选择后续节点中的div节点, 就应该这样写 //*[@class='single_choice']/following-sibling::div
要在某个元素内部使用xpath选择元素, 需要 在xpath表达式最前面加个点
elements = china.find_elements(By.XPATH, ‘.//p’)
5.10快速回顾
创建页面对象类,封装页面元素和操作
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
# 初始化 WebDriver(这里使用 Chrome 浏览器)
driver = webdriver.Chrome()
# css selector可以通过 .引用类,#引用id
# 直接子元素(直接包含)在引用时用箭头如 e1 > e2 表引用 e2
# 后代子元素(间接包含)在引用时用空格如 e1 e2 表引用 e2
# 根据属性选择元素,[]这个在 css表达式中可以引用任何一种属性
element = wd.find_element(By.CSS_SELECTOR, '[href="http://www.miitbeian.gov.cn"]')
# p:nth-chile(3) 子元素的第3个
# span:nth-last-of-type(2) 倒数父元素的第二个类型
try:
# 打开网页
driver.get('https://example.com')
# 查找元素(使用 ID 定位)
element = driver.find_element(By.ID, 'element_id')
element = driver.clear() #清除 文本框中内容
# 输入文本
element.send_keys('text')
# 点击元素
element.click()
# 获取元素的文本
text = element.text
print(f"Element text: {text}")
# 获取元素的属性值
attribute = element.get_attribute('attribute_name')
print(f"Element attribute: {attribute}")
#获取整个元素对应的 html
outer = wd.find_element(By.CSS_SELECTOR,'div[id="inner11"] > span').get_attribute('outerHTML')
print(outer) # 输出<span>内层11</span>
#获取整个元素对应的 内部html
element.get_attribute('innerHTML')
#input输入框的元素
element.get_attribute('value')
#显示所有内容(包括display属性为none的部分)
element.get_attribute('textContent')
# 等待元素出现(最长等待 10 秒)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, 'element_id')))
# 执行 JavaScript 脚本(例如:滚动到底部)
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
# 文件上传
upload_element = driver.find_element(By.ID, 'upload')
upload_element.send_keys('/path/to/file')
# 处理弹窗
alert = driver.switch_to.alert
alert.accept() # 确认弹窗
# 选择框操作(选择下拉菜单中的选项)
from selenium.webdriver.support.ui import Select
select = Select(driver.find_element(By.ID, 'select_id'))
select.select_by_visible_text('Option Text')
# 截图
driver.save_screenshot('screenshot.png')
# 切换到新的窗口或标签页
driver.switch_to.window(driver.window_handles[1])
# 关闭当前窗口
driver.close()
except Exception as e:
print(f"An error occurred: {e}")
finally:
# 关闭浏览器并释放资源
driver.quit()
pytest
打卡次数
- 9/12
安装pytest与pytest-html
pip install pytest
pip install pytest-html
注意事项
导入pytest,自动执行__init__.py文件
执行pytest,自动执行__main__.py文件
pytest插件中有hock,fixture,类和函数
入门坑点
self
参数是类实例方法中的参数,用于表示类的实例。在fixture中不应该定义self,但是如果在类中定义fixture函数,他又会提示你应该添加self参数。一般写测试.py不要随便定义类,如果不定义类,则方法不用写self
1.基本用法
1.1测试用例代码
- 文件必须以必须以
test_
开头,或者以_test
结尾,例如以下的:test_错误登录.py - 类名必须以
Test
为前缀的类
- 用例对应的必须以
test
为前缀的方法或函数
def test_C001003(self): # 函数
print('\n用例C001001')
assert 1 == 2
class Test_错误密码:
def test_C001001(self): # 类的内部就是方法
print('\n用例C001002')
assert 1 == 1
def test_C001003(self):
print('\n用例C001003')
assert 3 == 2
1.2运行测试
运行测试时方法:
- 在终端运行pytest cases (cases表目标测试文件目录)
- 在cmd中进入项目根目录后输入pytest cases
python pytest cases - sv # s表输出测试用例中的打印语句,而v则是更详细的包含方法名称
pytest --maxfail=1 在第一个失败后停止测试。
pyteset -m <marker> # 运行指定标记
1.3allure测试报告
python -m pytest cases --html=report.html --self-contained-html #会生成一个html文件,里面记录了详细信息
allure-pytest下载
命令行:pip install allure-pytest
下载后:pip show allure-pytest
allure环境下载与配置
需要有jdk的前提下,在国内一个allure网站下载版本。之后把bin目录添加到系统变量上去。
2.初始化清除
不是很重要,是unnitest方法的清除。
- 模块级别:
模块级别
的初始化、清除 分别 在整个模块的测试用例 执行前后执行,并且只会执行1次
。 - 类级别:在
类
执行前后执行,但是第二个类时直接连在清除类之后,而不是再次初始化类 - 方法级别:在方法执行前后都执行,但只限于该方法初始化方法有写在对应class类上
- 目录级别:在对应测试文件的目录下新建一个conftest.py文件
import pytest
@pytest.fixture(scope='package',autouse=True)
def st_emptyEnv():
print(f'\n#### 初始化-目录甲')
yield
print(f'\n#### 清除-目录甲')
def setup_module():
print('\n *** 初始化-模块 ***')
def teardown_module():
print('\n *** 清除-模块 ***')
class Test:
@classmethod
def setup_class(cls):
print('\n === 初始化-类 ===')
@classmethod
def teardown_class(cls):
print('\n === 清除 - 类 ===')
def setup_method(self):
print('\n --- 初始化-方法 ---')
def teardown_method(self):
print('\n --- 清除 -方法 ---')
def test_b(self): # 类的内部就是方法
print('\n用例C001002')
assert 1 == 1
def test_c(self):
print('\n用例C001003')
assert 3 == 2
class Test2:
def test_2(self):
print('\ndamage')
assert 1 == 1
def test_3(self): # 类的内部就是方法
print('\n用例3')
assert 1 == 1
3.挑选用例执行
平时写的pytest前面省略了python -m ,
-m
这种执行模块的方式运行python, 会自动把当前工作目录作为模块搜索路径,避免Python解释器 搜索不到 库文件的问题
python -m pytest cases\登录\test_错误登录.py #指定模块
pytest cases1 cases2\登录 #指定多个目录
pytest cases\登录\test_错误登录.py::Test_错误密码::test_01 # 指定模块中类时,在模块后面直接加::,指定类中方法也是::
pytest -k "not 错误 or not 01" -s #指定名字,名字间有并列关系
pytest cases -m webtest -s #根据标签mark指定,其直接在对应类,方法上加上@pytest.mark.XXX
例如
@pytest.mark.网页测试
class Test_错误密码2:
def test_C001021(self):
print('\n用例C001021')
assert 1 == 1
# 定义一个全局变量 pytestmark 为 整个模块文件 设定标签
import pytest
pytestmark = pytest.mark.网页测试
4.案例实战
封装登录逻辑
from selenium import webdriver # 每个项目都得重新pip install selenium及pytest
from selenium.webdriver.common.by import By
import time
def loginAndCheck(username,password):
driver = webdriver.Edge()
driver.get('http://localhost/mgr/sign.html')
driver.implicitly_wait(3) # 等待元素出现的最长时间
if username is not None:
driver.find_element(By.ID,"username").send_keys(username)
if password is not None:
driver.find_element(By.ID,"password").send_keys(password) driver.find_element(By.XPATH,'/html/body/div/div[2]/div[1]/div[3]/div/button').click()
time.sleep(2) # 确保弹出消息有足够时间加载
alertText = driver.switch_to.alert.text # 驱动转到alert,并获得弹出信息
print(alertText)
driver.quit() #退出窗口
return alertText # 返回信息
测试用例调用模块方法
from lib.login import loginAndCheck
class Test:
def test_UI_0001(self): # 类的内部就是方法
print('\n用例UI-0001')
alerttext = loginAndCheck(None,'88888888')
assert alerttext == "请输入用户名"
def test_UI_0002(self): # 类的内部就是方法
print('\n用例UI-0002')
alerttext = loginAndCheck('poter',None)
assert alerttext == "请输入密码"
数据驱动
可优化上述步骤:把测试数据从测试用例代码中
分离
开来,以后增加新的测试用例,只需要修改数据。
class Test_错误登录:
@pytest.mark.parametrize('username, password, expectedalert', [
(None, '88888888', '请输入用户名'),
('byhy', None, '请输入密码'),
('byh', '88888888', '登录失败 : 用户名或者密码错误'),
('byhy', '8888888', '登录失败 : 用户名或者密码错误'),
('byhy', '888888888', '登录失败 : 用户名或者密码错误'),
]
)
def test_UI_0001_0005(self, username, password, expectedalert):
alertText = loginAndCheck(username, password)
assert alertText == expectedalert
直接新建一个json文件,然后再列表中放入一个对象,即[{‘username’:‘porter’},{}]
import json
# 读取json文件
def build_data(json_file):
test_data = [] # 定义空列表
with open(json_file,'r') as f: #打开json文件
json_data = json.load(f) #加载数据
for case_data in json_data:
# 转换数据格式[{},{}] => [(),()]
username = case_data.get("username")
status = case_data.get('status')
message = case_data.get('message')
test_data.append(username.staus,message) #存入空列表
return test_data
@pytest.mark.parametrize('username, status, message',build_data(json_file='../login.json'))
def test_01_login(username,status,message):
login_data = {'username': username,'status': status}
5.调试
配置
测试
6.fixture
6.1使用fixture
import pytest
# 定义一个fixture函数
@pytest.fixture
def createzhangSan():
# 这里代码实现了使用API创建用户张三账号功能
print('\n *** createzhangSan ***')
zhangSan = {
'username' : 'zhangsan',
'password' : '111111',
'invitecode' : 'abcdefg' # 这是系统创建用户产生的其它信息
}
return zhangSan
# 这里测试张三账号登录的功能
def test_A001001(createzhangSan):
print('\n用例 A001001')
print('\ninvitecode is', createzhangSan['invitecode'])
运行结果
若测试用例中有fixture函数,会先执行fixture函数, 然后把fixture函数返回的结果 作为参数的值,又叫依赖注入
如果在运行之后清除数据,则可把return zhangsan 修改为 yield zhangSan print('\n *** 执行清除张三账号的代码 ***')
修改之后会在测试用例执行完后,执行yield 后语句
6.2fixture参数
多个参数的情况下可以使用数据驱动,来创建多个参数(需要加上indirect=True条件,)
- indirect=True,表示这些参数应该传递给
createUser
fixture 而不是直接传递给测试函数。- request是一个字典
# fixture 函数用于创建用户,并通过参数化的方式为测试用例提供不同的用户数据。
@pytest.fixture
def createUser(request):
# 这里代码实现了使用API创建用户账号功能
print('\n *** createUser ***')
user = {
'username' : request.param[0],
'password' : request.param[1],
'invitecode' : 'abcdefg' # 这是系统创建用户产生的其它信息
}
return user
# 将测试用例参数化。
@pytest.mark.parametrize("createUser",
[("zhangsan", "111111"),("lisi", "111")],
indirect=True) # 表示这些参数应该传递给 createUser fixture 而不是直接传递给测试函数。
# 测试用例
def test_A00100X(createUser): #检查到有fixture函数名一致的,先执行函数内容
print('\nusername is', createUser['username'])
print('测试行为')
总结:
createUser
fixture 用于创建用户信息,并作为参数传递给测试用例。pytest.mark.parametrize
用于为测试用例提供多个用户数据。- 测试用例
test_A00100X
通过createUser
fixture 接受不同的用户数据,并输出相关信息。
6.3scope范围(清除元素)
scope = ‘function’ # class ,module,session(整个测试环节)
目录级别则是新建一个
conftest.py
的文件
@pytest.fixture(scope='function',autouse = true)
def createzhangSan():
print('\n *** 创建 ***')
zhangSan = {
'username' : 'zhangsan',
'password' : '111111',
}
yield zhangSan
print('\n *** 清除 ***')
7.Elife实战项目
7.1登录功能
登录.py
from lib.webUI_smp import smp
from selenium.webdriver.common.by import By
import pytest,time
class Test01:
def test_SMP_login_001(self,clearAlert): # 正确用例测试
smp.login('byhy','sdfsdf') # 直接调用
nav = smp.driver.find_element(By.TAG_NAME, 'nav') # 1.通过标签寻找nav
assert nav != []
# count = driver.page_source.count('nav') # 2.通过源码查找是否有对应字段
# assert count > 1 , f'登录失败,{count=}'
@pytest.fixture()
def clearAlert(self):
yield # 前是方法执行前,后是方法执行后
try: # 异常处理:在系统出现问题,弹框没有出现时,也不会报错
smp.driver.switch_to.alert.accept() # 点击
except Exception as e:
print(e)
# '参数1,参数2' ,[(),()]
@pytest.mark.parametrize('username,password,expectedalert',[ #列表里面放turple
(None,'sdfsdf','请输入用户名'),
('byhy', None,'请输入密码'),
('byhy', 'sdfsdfa','登录失败: 用户名或者密码错误'),
('byhy', 'sdfsd','登录失败: 用户名或者密码错误'),
('byhys', 'sdfsdf','登录失败: 用户名不存在'),
('byh', 'sdfsdf','登录失败: 用户名不存在')
])
# 异常用例测试
def tests_002_007(self,username,password,expectedalert,clearAlert): #这里clearAlert参数与fixture方法一致,故会在方法执行后点击'确认'按钮
smp.login(username,password)
time.sleep(1) # 等待提示框出现
alert = smp.driver.switch_to.alert.text
assert alert == expectedalert
web_UI.py(放通用方法)
from selenium import webdriver
from selenium.webdriver.common.by import By
from cfg import *
import time
class SMP_UI:
def __init__(self): # 魔法方法
self.driver = webdriver.Edge() # 启动浏览器并设置隐式等待时间
self.driver.implicitly_wait(5)
def login(self,username,password):
self.driver.get(SMP_URL_LOGIN) # 登录地址放在cfg配置类中
time.sleep(1)
if username is not None:
self.driver.find_element(By.ID, 'username').send_keys(username)
if password is not None:
self.driver.find_element(By.ID, 'password').send_keys(password)
time.sleep(1)
self.driver.find_element(By.ID, 'loginBtn').click()
smp = SMP_UI() # 直接在类中实例化对象,后面直接引入该实例
7.2设备型号
webUI.py(自动化测试脚本)
def addDevice(self, devType, model, desc):
try:
time.sleep(2) # 确保页面按钮出现
# 点击添加
addBtn = smp.driver.find_element(By.CSS_SELECTOR, 'body > main > div.add-one-area > span')
if addBtn.text == '添加':
addBtn.click()
# 选择框,创建Select对象
select = Select(smp.driver.find_element(By.ID, "device-type"))
select.select_by_visible_text(devType)
# 让驱动最长等待10s,除非期望元素变成可点击元素
ele = WebDriverWait(smp.driver, 10).until(EC.element_to_be_clickable((By.ID, "device-model")))
ele.clear() # 清除残留内容
ele.send_keys(model)
ele = smp.driver.find_element(By.XPATH, '//*[@id="device-model-desc"]')
ele.clear()
ele.send_keys(desc)
time.sleep(2)
smp.driver.find_element(By.XPATH, '/html/body/main/div[1]/div/div[4]/span').click()
except Exception as e:
print(e)
def get_first_device(self):
time.sleep(1) # 暂停1秒,确保页面完全加载或有足够时间等待异步操作完成
self.driver.implicitly_wait(0) # 关闭隐式等待,立即获取元素,不等待
values = self.driver.find_elements(By.CSS_SELECTOR, '.field-value') # 查找所有具有'class'为'field-value'的元素
self.driver.implicitly_wait(10) # 恢复隐式等待,设置等待时间为10秒,以便处理后续操作
deviceModels = [] # 初始化空列表,用于存储设备信息
for idx, value in enumerate(values): # enumrate生成迭代元素的序号及值
if (idx + 1) % 3 == 0: # 每三个元素为一组
deviceModels.append(
[values[idx-2].text, values[idx-1].text, values[idx].text] # 将当前元素及其前两个元素的文本值作为一组添加到列表中
)
return deviceModels # 返回包含设备信息的列表
def del_first_item(self) -> bool: # 表返回的bool值
self.driver.implicitly_wait(0) # 临时禁用隐式等待以确保立即获取元素
# 查找删除按钮,使用指定的 XPath
delBtn = smp.driver.find_elements(By.XPATH, '/html/body/main/div[3]/div[1]/div[2]/span[1]')
self.driver.implicitly_wait(5) # 恢复隐式等待,设定为5秒
if not delBtn: # 如果没有找到删除按钮,则返回 False
return False
delBtn[0].click() # 点击找到的第一个删除按钮
# 处理弹出的确认对话框并接受
smp.driver.switch_to.alert.accept()
return True # 返回 True,表示删除操作成功
设备.py,测试流程如下:
- 登录进去之后跳转到device_page(登录跳转到测试页面)方法执行之前初始化
- 点击添加按钮
- 输入信息并提交(添加过程)
- 验证页面第一行是否为刚输入的信息(断言)
- 验证之后,删除添加的数据(删除)方法执行之后初始化
from cfg import *
from lib.webUI_smp import smp
import pytest, time
@pytest.fixture(scope='module')
def inDevicePage():
print("isDevicePage setup")
smp.login('byhy', 'sdfsdf')
smp.driver.get(SMP_URL_DEVICE_PAGE) # 登录进去之后跳转到device_page
yield
print("isDevicePage teardown")
return
@pytest.fixture(scope='function')
def delAddedDevice():
print("删除添加的测试数据 setup")
yield
print("删除添加的测试数据 teardown")
smp.del_first_item() # 删除添加的数据,保证测试前后数据的一致
# 参数写成字符串形式,后面跟列表,列表里面是一个个turple()
@pytest.mark.parametrize('devType, model, desc',
[
("汽车充电站", '10-20-40', '南京e生活'),("gaoshiqing", 'kkk', '上号'), ("存储柜", '李' * 100, 'colorful')
])
# 数据驱动(参数之后,注意要登录及初始化清除)
def test_SMP_device_model_001_003(devType,model,desc,inDevicePage, delAddedDevice):
smp.addDevice(devType,model,desc)
devices = smp.get_first_device() # 获取添加的元素列表
assert devices[0] == [devType,model,desc] # 断言
def test_SMP_device_model_001(inDevicePage, delAddedDevice):
smp.addDevice("汽车充电站", '10-20-40', '南京e生活') # 添加
devices = smp.get_first_device() # 获取添加的元素列表
assert devices[0] == ['汽车充电站', '10-20-40', '南京e生活'] # 断言
def test_SMP_device_model_002(inDevicePage, delAddedDevice):
smp.addDevice("存储柜", '李' * 100, 'colorful') # 添加
devices = smp.get_first_device() # 获取添加的元素列表
assert devices[0] == ["存储柜", '李' * 100, 'colorful'] # 断言
7.3业务规则
# 选择框,创建Select对象
select = Select(smp.driver.find_element(By.XPATH, '//*[@id="rule_type_id"]'))
select.select_by_visible_text(ruleType)
if ruleType != '后付费-上报业务量':
ele = smp.driver.find_element(By.XPATH,'/html/body/main/div[1]/div/div[3]/input')
ele.clear() # 清除上次的
if mimFee: #如果存在的话发送
ele.send_keys(mimFee)
for one in feeRate: #one 即feeRate中的元素。若feeRate是[[a,b]],则one为[a,b]
self.driver.find_element(By.CSS_SELECTOR,'.fee-rate-list button').click() # 点击添加费率
entry = self.driver.find_element(By.CSS_SELECTOR, 'div.fee-rate:nth-last-child(2)')# 获取新增的行输入对象,由于是可变的
# 业务码
entry.find_element(By.CSS_SELECTOR, 'input:nth-of-type(1)').send_keys(one[0]) # 新对象中找元素输入框,直接标签加第几个元素
7.4allure报告
1.在项目根目录下新建pytest.ini配置文件
[pytest]
addopts = -s --alluredir report
testpaths=./cases/登录 #测试用例目录
python_files=test*.py
python_classes = Test* # 如果没有类,可以省略
python_function=test*
2.在终端直接输入pytest运行,如果报错module not found ,原因是python跟pytest冲突
可以直接再新建一个配置文件conftest.py
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
3.在终端输入allure serve report
生成测试报告html
8.高级用法
主要是mark,fixture,hook。
[pytest] markers = api,ui,ut # 注册标记
@pytest.mark.api #使用
@pytest.fixture() #用户测试环境的构建与销毁
hook #改变pytest原有的运行方式,运行我们进入核心
yaml文件
test_name
steps:
- a: 2
b: 3
c: 5 #a+b
9.pytest框架封装
框架封装,
9.1日志文件
在pytest.ini中添加以下代码
log_file = ./pyteset.log
log_file_level = info
log_file_format =
10.快速回顾
- 基本用法:
- 使用
def test_function()
定义测试函数。 - 运行测试:
pytest
或pytest <filename>.py
。
- 使用
- 断言:
- 使用
assert
语句验证条件,例如assert a == b
。
- 使用
- 测试夹具 (Fixtures):
- 使用
@pytest.fixture
装饰器定义夹具,提供测试所需的数据或状态。 - 通过函数参数传递夹具,例如
def test_func(fixture_name)
。
- 使用
- 标记 (Markers):
- 使用
@pytest.mark.<marker>
标记测试,例如@pytest.mark.slow
。 - 运行特定标记的测试:
pytest -m <marker>
。
- 使用
- 参数化:
- 使用
@pytest.mark.parametrize
传递多个参数进行测试,例如@pytest.mark.parametrize("input,expected", [(1, 2), (3, 4)])
。
- 使用
- 测试套件 (Test Suites):
- 通过创建测试类并将测试方法添加到类中组织测试。
- 跳过和预期失败:
- 使用
@pytest.mark.skip
跳过测试。 - 使用
@pytest.mark.xfail
标记预期会失败的测试。
- 使用
- 命令行选项:
pytest -v
以详细模式运行测试。pytest --maxfail=1
在第一个失败后停止测试。pytest --tb=short
自定义跟踪回溯的格式。
- 生成测试报告:
- 使用
pytest --html=report.html
生成 HTML 格式的测试报告。
- 使用
- 测试用例的组织:
- 将测试函数组织在不同的模块或类中,以保持测试的结构清晰。
接口测试(hytest三集)
接口文档首先要执行业务接口,再接着测试单个接口(后端慢写好的情况下,整体已完成就先测业务)
测试业务接口:
- 根据业务流程图梳理业务路径
- 设计测试用例覆盖每一条业务路径
- 使用软件工具,直接通过消息接口 对 被测系统 进行消息收发
- 验证被测系统行为是否正确。
接口测试步骤:
- 理解 API 文档: 阅读 API 规格文档,理解接口的输入、输出、请求方法(GET, POST, PUT, DELETE等)及其参数。
- 构建测试用例: 编写测试用例以验证接口功能、边界条件、错误处理等。
- 设置测试环境: 配置测试环境,包括 API 服务器、数据库等。
- 发送请求: 使用工具或编程语言发送请求到 API。
- 验证响应: 检查 API 返回的响应是否符合预期,包括状态码、响应时间、返回数据等。
- 处理异常: 捕捉并处理接口调用中的异常情况,如网络问题、API 错误等。
1.HTTP
HTTP 协议 全称是 超文本传输协议(网页、视频、图片),通讯双方 分为 客户端
和 服务端
。
请求信息:请求行,请求头,请求体
Post /mgr/login.html HTTP/1.1 # 添加资源 到 服务器地址 /api/medicine , 使用的 协议 是 HTTP/1.1
Host: www.baiyueheiyu.com # 请求头
User-Agent: Mozilla/6.0 (compatible; MSIE5.01; Windows NT)
Content-Type: application/x-www-form-urlencoded
Content-Length: 51
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
name=qingmeisu&sn=099877883837&desc=qingmeisuyaopin # 空行之后就是请求体,一般是json,xml,www-form
响应信息:状态行,响应头,响应体
状态码:2xx :成功,3xx:重定向,4xx:前台客户端问题,5xx:后台服务端问题
HTTP/1.1 200 OK # 状态行由 协议版本+状态码+描述信息
Date: Thu, 19 Sep 2019 08:08:27 GMT # 响应头包含时间
Server: WSGIServer/0.2 CPython/3.7.3
Content-Type: application/json
Content-Length: 37
X-Frame-Options: SAMEORIGIN
Vary: Cookie
{"ret": 0, "retlist": [], "total": 0} #响应体跟请求体差不多
2.request库
pip install requests # 安装
pip show requests # 验证
requests.get/put(url,params=None,data=None,json=None,headers=None,files = None)
2.1fiddler
简介:fiddler 是 代理式
抓包。设置自己作为系统代理,监听在 8888 端口上。
HTTP客户端需要设置 fiddler 作为代理, 把HTTP请求消息 发送给 fiddler, fiddler再转发HTTP消息给服务端。
requests程序抓包
import requests
proxies = { # 启用代理
'http': 'http://127.0.0.1:8888',
'https': 'http://127.0.0.1:8888',
}
response = requests.get('http://mirrors.sohu.com/', proxies=proxies)
print(response.text)
手机抓包
首先在fidder的option中connections中allow remote computers to connect.接着再保证手机跟电脑同个网络下,手机连接wifi开启代理,输入电脑ip地址与fidder代理服务器端口即可
2.2构建http请求
url参数(两种方式)params
response = requests.get('https://www.baidu.com/s?id = 1')
urlpara = {
'wd':'iphone&ipad',
'rsv_spt':'1'
}
response = requests.get('https://www.baidu.com/s',params=urlpara)
请求头headers
headers = {
'user-agent': 'my-app/0.0.1',
'auth-type': 'jwt-token'
}
r = requests.post("http://httpbin.org/post", headers=headers)
请求体:data = payload
xml格式消息体
payload = ''' # 直接用‘’‘把xml包起来
<?xml version="1.0" encoding="UTF-8"?>
<WorkReport>
<Overall>良好</Overall>
<Progress>30%</Progress>
<Problems>暂无</Problems>
</WorkReport>
'''
r = requests.post("http://httpbin.org/post",
data=payload.encode('utf8'))
print(r.text)
urlencoded 格式消息体
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.post("http://httpbin.org/post", data=payload)
print(r.text)
json格式消息体 data=json.dumps(payload)
import requests,json
payload = {
"Progress":"30%",
"Problems":[
{
"No" : 1,
"desc": "问题1...."
},
{
"No" : 2,
"desc": "问题2...."
},
]
}
r = requests.post("http://httpbin.org/post", data=json.dumps(payload))
obj = response.json(r) # 即可将json形式转为对象
2.3检查http响应
print(response.status_code) # 状态码
print(response.headers['Content-Type']) # 存储的是字典,可以直接获取响应头的值response.encoding='utf8'
print(response.text)
2.4session
用户使用客户端登录,服务端进行验证(比如验证用户名、密码)。验证通过后,服务端系统就会为这次登录创建一个session对话。session就是一个数据结构,保存该客户这次登录操作相关信息。
服务端是通过 HTTP的响应头 Set-Cookie
把产生的 sessionid 告诉客户端的。
客户端的后续请求,是通过 HTTP的请求头 Cookie
告诉服务端它所持有的sessionid的。
2.5接口手动测试
import requests
# 打印HTTP响应消息的函数
def printResponse(response):
print('\n\n-------- HTTP response * begin -------')
print(response.status_code)
for k, v in response.headers.items():
print(f'{k}: {v}')
print('')
print(response.content.decode('utf8'))
print('-------- HTTP response * end -------\n\n')
# 创建 Session 对象
s = requests.Session()
# 通过 Session 对象 发送请求
response = s.post("http://127.0.0.1/api/mgr/signin",
data={
'username': 'byhy',
'password': '88888888'
})
# 通过 Session 对象 发送请求
response = s.get("http://127.0.0.1/api/mgr/customers",
params={
'action' : 'list_customer',
'pagesize' : 10,
'pagenum' : 1,
'keywords' : '',
})
printResponse(response)
我们可以把上述用到的方法封装到一个webapi.py库中,使用的时候直接调用,例子如下
def mgr_login(self,username,password):
self.s = requests.Session() # 获取session对象
response = self.s.post("http://127.0.0.1/api/mgr/signin",
data={
'username': username,
'password': password
}
) #网址后加json格式消息体
self._printResponse(response)
return response
新增课程案例
1.接口类封装(api)
2、测试脚本(script)
class TestCourse # 测试类
def setup(self): # 前置处理
self.course_api = CourseAPi() #实例化对象
def test_add_course(self):
add_data = {'name':'success','price':100}
response = self.course_api.add_course(test_data=add_data,token = TestCourse.token)
3.配置类(config.py)
import os
BASE_URL = 'http://www.' # 设置项目环境域名,使用时导入config.BASE_URL + '' 进行字符串拼接
BASE_PATH = os.path.dirname(__file__) # 获取根项目路径
3.postman
3.1自动关联
假如其他接口调用时需要用到某个值,而该值又得其他接口生成。那么可以将两者关联起来。
- 首先要创建一个environment,并且命名,以便项目可以找到该环境;
- 接着在值生成的接口中的tests中添加以下代码获取值
var jsonData = pm.response.json() #获取响应数据
pm.environment.set("token",jsonData.uuid) #放进环境变量中,之后便可以在所用处调用
- 接着在调用处直接{{}},调用{{uuid}}
3.2批量执行
只需要将接口放在一个文件夹下面,最后运行测试集就可以了。
3.3断言
3.4参数化
快速回顾
app测试
1.原理
appium(客户端库)->appium server -> device
Appium Server 会在手机上 安装一个 自动化代理程序, 代理程序会等待自动化指令,并且执行自动化指令
执行过程
- 自动化程序 调用客户端库相应的函数, 发送
点击元素
的指令(封装在HTTP消息里)给 Appium Server - Appium Server 再转发这个指令给 手机上的自动化代理
- 手机上的自动化代理 接收到 指令后,调用手机平台的自动化库,执行点击操作,返回点击成功的结果给 Appium Server
- Appium Server 转发给 自动化程序
- 自动化程序了解到本操作成功后,继续后面的自动化流程
2.环境
百度网盘链接:https://pan.baidu.com/s/19C9fGmoXne8DgfXhrTB2TQ
提取码:kgwb
pip install appium-python-client #安装client库
# 安装Appium Server,注意选中对应的操作系统
# 安装1.8jdk,并设置进环境变量
# 安装 Android SDK,解压androidsdk.zip,并配置一个ANDROID_HOME
# 添加adb所在目录进环境变量
# 连接手机(注意要用数据线,即插入后,手机会提示授予权限)
进入 手机设置 -> 关于手机 ,不断点击 具体版本号Miui 菜单(7次以上),
退出到上级菜单,在开发者模式中,启动USB调试
3.入门案例
注意:一定要先运行 Appium Server,还有开发者权限中的禁止权限监控的设置启用
from appium import webdriver
from selenium.webdriver.common.by import By
from appium.webdriver.extensions.android.nativekey import AndroidKey
from appium.options.android import UiAutomator2Options
desired_caps = {
'platformName': 'Android', # 被测手机是安卓
'platformVersion': '13', # 手机安卓版本,如果是鸿蒙系统,依次尝试 12、11、10 这些版本号
'deviceName': '努比亚', # 设备名,安卓手机可以随意填写
'appPackage': 'tv.danmaku.bili', # 启动APP Package名称
'appActivity': '.MainActivityV2', # 启动Activity名称
'unicodeKeyboard': True, # 自动化需要输入中文时填True
'resetKeyboard': True, # 执行完程序恢复原来输入法
'noReset': True, # 不要重置App
'newCommandTimeout': 9000,
'automationName': 'UiAutomator2'
# 'app': r'd:\apk\bili.apk',
}
# 连接Appium Server,初始化自动化环境
driver = webdriver.Remote('http://localhost:4723/wd/hub',
options=UiAutomator2Options().load_capabilities(desired_caps))
# 设置缺省等待时间
driver.implicitly_wait(5)
# 如果有`青少年保护`界面,点击`我知道了`
iknow = driver.find_elements(By.ID, "text3")
if iknow:
iknow.click()
# 根据id定位搜索位置框,点击
driver.find_element(By.ID, 'expand_search').click()
# 根据id定位搜索输入框,点击
sbox = driver.find_element(By.ID, 'search_src_text')
sbox.send_keys('白月黑羽')
# 输入回车键,确定搜索
driver.press_keycode(AndroidKey.ENTER)
# 选择(定位)所有视频标题
eles = driver.find_elements(By.ID, 'title')
for ele in eles:
# 打印标题
print(ele.text)
input('**** Press to quit..')
driver.quit()
4.查找
4.1发现元素
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'byhy') # appium特有的查找方式
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, code)
driver.find_element(By.ID,'username') # 基础的跟selenium的一样
# 这样也可以根据ID
wd.find_element(AppiumBy.ID, 'username').send_keys('byhy')
4.2查找package,activity
apk(Android Package Kit):Android 操作系统中用于分发和安装应用程序的文件格式,即相当于window的exe安装包。
4.2.1没有apk
adb shell dumpsys activity recents | find "intent={" # 进入到应用中后执行该命令
cmp=tv.danmaku.bili/.MainActivityV2 # 这个是运行上面语句后出现于第一句中最后面的
# 应用的package名称就是 tv.danmaku.bili
# 应用的启动Activity就是 .MainActivityV2
4.2.2有apk
如果你已经获取到了 apk,在命令行窗口执行
d:\tools\androidsdk\build-tools\29.0.3\aapt.exe dump badging d:\tools\apk\bili.apk | find "package: name="
输出信息中,就有应用的package名称
package: name='tv.danmaku.bili' versionCode='5531000' versionName='5.53.1' platformBuildVersionName='5.53.1' compileSdkVersion='28' compileSdkVersionCodename='9'
在命令行窗口执行
d:\tools\androidsdk\build-tools\29.0.3\aapt.exe dump badging d:\tools\apk\bili.apk | find "launchable-activity"
输出信息中,就有应用的启动Activity
launchable-activity: name='tv.danmaku.bili.MainActivityV2' label='' icon=''
5.定位元素
5.1界面元素查看工具
查看界面元素的特征:web是通过开发者工具F12;
常用的app查看元素工具是: Android Sdk包中的 uiautomateviewer
和 Appium Desktop 中的 Appium Inspector
Appium Inspector:可以直接验证 选择表达式是否能定位到元素
5.2定位元素方法
五个定位方式
- 根据id
- 根据CLass NAME
- 根据ACCESSIBILITY ID
from appium.webdriver.common.appiumby import AppiumBy
driver.find_element(AppiumBy.ID, 'expand_search') # 这里的id是resource-id
driver.find_element( # 一般是多个,相当于tagName
AppiumBy.CLASS_NAME, 'android.widget.TextView')
性能测试
性能测试是:验证 在 各种性能负载场景下
,系统的表现是否符合预期。(往往需要模拟 多种 用户行为, 每种行为的用户有多少个。)一个性能测试场景为 test plan
,中文翻译为 测试计划
。
作用:评估系统的能力,识别系统的瓶颈,检查系统的稳定性与可靠性,检查系统的隐藏问题。
线程组:把相同的用户行为合并为一组,一个线程代表一个用户。
`Ramp-Up 时间` 意思是 所有用户上线的总时间,以秒为单位。 5 即总共耗时5秒,如果是 5个用户,那就意味着 每隔1.25秒上线一个: 5秒/(5-1)
`循环次数` 意思是 每个用户 做线程组里面定义的动作行为 `多少轮` 。 缺省就做一轮,缺省即默认。
TPS用于衡量系统在一定时间内能够处理的事务数 = 总的事务数/总时间
QPS每一秒的查询率,如果单个接口就是一个业务,那其还等于Rps
资源使用率:服务器,cpu,内存,磁盘,网络。
吞吐量:衡量网络成功传输的数据量
1.GUI模式运行
运行JMeter 有2种运行模式: GUI 图形界面模式
和 CLI 命令行模式
,前者是开发调试用的,后者是压力测试用的
添加Listener监听器,最常用的就是查看结果树,显示HTTP请求具体的细节
Load time:4 #从发出请求前 到 接收完所有响应的时间
Connect Time:0 #jmeter和被测建立TCP连接的时间,包括3路握手时间,如果连接复用, 值为0
Latency:3 # 从发出请求前到接收完第一个响应的时间
Size in bytes:22591 # 整个响应消息大小 = Headers size in bytes + Body size in bytes
Sent bytes:357 # 请求消息大小
Headers size in bytes:287
Body size in bytes:22304
Sample Count:1
Error Count:0
Data type ("text"|"bin"|""):text
Response code:200
Response message:OK
Http请求默认值
设置取样器的默认服务器访问地址及使用的网络协议;直接在
项目实战
金融项目
6.1梳理(p15)
基础知识
业务模块
6.2提取测试点
借款业务
投资业务
6.3测试流程
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)