函数的定义与调用
函数,其实就是已经组织好,可以重复去使用的一种实现单一功能或者关联功能的代码块。通过函数的名称来对函数进行调用。通过函数的应用,我们可以极大简化我们的代码。可以降低代码冗余。可以提升代码的维护性。同时还可以提升代码的复用性。在Python中已经帮助我们封装有非常多的函数。我们在自定义封装函数时,一定要避免重名系统自带的函数名称。我们自己也可以通过自定义函数的内容定义,再基于函数的名称实现调用。从而达到与系统自带函数的调用的同样效果。
函数的参数传递规则
在python中,所有的函数,在传入参数的时候,参数其实都会变成*args和**kwargs两种形态。
-
*args表示以元组的形态。(固定写法)- * 表示参数基于元组的形态进行接收和解析。根本上其实就是拆包。*将元组中的元素拆包成不同的参数,依次传入
- * 可以接收无限长度的参数。因为所有的参数都存储在一个元组之中。
- 通过*解包来实现参数的解析。解析元组之后,按照元组顺序依次进行传参。无法修改传参的顺序。
- * 叫做不定长不定值传参。不定义参数的长度,也不定义参数的对应值的方式进行传参。
- *只能用于解析元组。*在哪里就在哪里解析元组。
*的作用是将可迭代对象(如元组、列表)拆分为多个独立的参数,与函数的形参一一对应,从而避免参数数量不匹配的错误。
-
**kwargs表示以字典的形态。(固定写法。)所有的函数的参数本身都是已经提前固定好的。所有的参数其实都可以根据参数的名称来实现指定参数的值传入。类似于键值对的形态key表示形参,value表示实参。在实际函数调用时,可以指定key,传入对应value,实现参数的定值传入。
- ** 表示基于字典的形态来进行解包和传参
- ** key一定要与形参保持一致。value表示对应形参的实参
- ** 必须要将所有的参数以及对应的值进行定义,但是顺序可以打乱。
- ** 定值不定长传参。
- **在哪里,解包就在哪里
- 就是把
key:value解包成key=value
类属性与类方法
类的定义与使用:
- 类是基于关键字class来进行定义的。如果类之中没有任何内容,我们可以通过pass来实现。
- 类属性和类方法:
- 属性: 其实就是变量。单独使用的叫做变量,而在类之中定义的叫做类属性。
- 方法: 其实就是函数。单独封装的叫做函数,在类中封装的叫做方法。 在类之中所定义的方法,默认都会存在有self参数。self参数表示他自己。其实就是实例化对象。 也有意味着,包含有self参数的为实例化方法。想要在外部调用这些方法必须通过实例化对象才可以。 静态方法,就是不需要通过self即可实现调用。也就是不需要实例化,可以直接通过类名来调用的方法。 类属性无法通过实例化对象直接修改。
类的调用: 类中所有的属性和方法,默认都是基于实例化的行为来实现调用的。 类的方法和属性,都是基于名称来实现调用。通过类名或者对象名称,加上.属性 或者 .方法()来实现 区别在于属性调用不用在末尾加(),而方法需要
类的构造方法: 每个类都有自己的默认的构造方法。你可以定义也可以不定义。 构造方法是基于__init__(self)来定义的。 每一个类之中只会存在一个构造方法。
类的作用域: 类中所定义的所有属性和方法,都只在类之中有效。在类以外只能通过实例化对象或者类自己来实现调用。否则无法使用
类属性vs实例属性
- 类属性:直接定义在类中(不在
__init___等方法中),通过类名.属性名访问,属于类本身。 - 实例属性:通常定义在
__init__方法中,通过self.属性名声明,属于类的每个实例。
类方法vs实例方法
- 类方法:第一个参数是
cls,需用@classmethod装饰器声明,可以通过类名或实例调用,(类名.方法名(),实例名.方法名()),调用时,python会自动将类作为cls参数传入。 - 实例方法:第一个参数是
self,无装饰器,只能通过实例调用(实例名.方法名()),调用时,python会自动将类作为self参数传入。
继承、封装、多态
-
继承:子类继承父类
- 子类可以将父类中所有已有的可被继承的内容。全部都继承在子类之中。
- 继承的方式就是子类类名(父类类名)
- 不可被继承的就是私有属性和私有方法,通过__来定义
- 可以多继承。也就是继承多个父类
- 多继承时,不同的父类用,进行分割,如果父类之间存在有相同的属性或者方法时。根据继承的顺序,
- 从左至右来确定最终继承的内容,先继承谁就是谁的值。
-
封装:封装是一种概念,不是某种特定的代码格式。
为了降低代码的冗余,提升代码复用性的一种形式。
-
多态:程序的多种形态
- 一个事务是具备有多种不同的形态。在类的属性和方法上可以进行多态的定义。
- 方法的重写:将父类之中的已被子类所继承的方法,在子类中重新进行定义。由此,不同对象的同一个方法,可以实现不同的效果
- 方法的重载(Python不支持):同一个方法,基于定义的参数数量不同,来实现不同的效果
异常
一个 try 块后有多个 except 块时,如果前面的 except 捕获并处理了异常,后面的 except 块不会会再执行。这是因为异常处理遵循 “匹配到第一个合适的 except 后就终止处理” 的规则。子类异常必须放在父类异常之前。
raise Except:手动抛出异常;raise 单独使用必须在except,不会创建新异常,而是复用当前捕获的异常对象。
traceback.print_exc() 是 Python 中 traceback 模块提供的一个函数,主要作用是打印当前异常的详细堆栈跟踪信息(包括异常类型、异常消息以及导致异常的代码调用栈),帮助开发者定位和调试错误。
具体功能
当程序发生未捕获的异常时,Python 会自动打印堆栈跟踪信息;但如果异常被 try-except 捕获,默认不会显示详细信息。此时在except里使用 traceback.print_exc() 可以在 except 块中主动打印完整的异常堆栈,包括:
- 异常的类型(如
ZeroDivisionError、ValueError); - 异常的描述信息(如
division by zero); - 异常发生的具体代码行、文件名以及函数调用链(从异常发生点向上追溯的所有调用层级)。
自定义异常类
class MyException(Exception):
pass
'''
try...except...语法应用:
1. 基本的语法结构:
try:
try代码块 # 正常代码,但是可能会产生异常。所以放在try之中。
except:
except代码块 # 当try代码块出现报错时,则立即进入except之中,执行此处代码。就是用于处理异常
程序一旦报错,会在报错的位置直接终止运行。在控制台抛出异常信息。但是try...except加上之后,程序不会终止运行。
我们会将可能出错的代码放在try之中,如果出错则进入except进行异常处理,否则不会进入except。程序正常运行。
try...except是用于处理程序中的异常情况的。所以当出现异常时,因为有try的存在。所以程序默认错误已经被处理了。所以不会再
继续报错。
try...except本质其实和if...else没有太大区别。只是说try语句是专门用于处理异常的。而if语句可以适应的场景更多。
try...except是固定存在的。是一套完整的代码结构,无法独立存在。try语句是可以嵌套的。
Exception对象:
所有的异常和错误,都是继承于BaseException类来实现的。
所有的异常和错误都有独属于自己的类。而except语法可以捕获指定的异常和错误。
except可以存在多个。用于处理不同的异常情况下。
Exception对象还可以自定义异常类。实现自定义的异常
Else...Finally...语法结构:
try:
可能出错的代码块
except:
出错后的处理手段
else:
如果未出错则进入此处
finally:
无论是否出错,最终都会执行
finally是在try模块中非常常用的关键字。因为无论是否报错都会执行的代码块。所以常见于释放资源相关操作
如果需要做代码关联,则会应用到else,用于关联try之中的代码,作为逻辑的延续。而其余场景很少使用
else...finally一定是关联try语句块来使用的。
raise关键字:用于手动抛出异常。
当遇到我们不想(不能)处理的异常时,可以对其进行抛出的操作。让程序产生异常,从而交由后续调用的人去解决。
raise抛出异常后,异常依旧存在。不是解决异常的手段。是相当于你制造了一个异常出去。或者对已经存在的异常
进行抛出的处理。所以异常依旧存在。
raise可以抛出指定异常,或者不加异常直接抛出。则会提示RuntimeError
Traceback模块:Python官方库。不需要安装,直接导入使用即可。
所有的异常信息,都是基于Traceback来显示在控制台之中的。
当我们使用了try...except之后,就不会在控制台显示任何的异常详细信息了。
如果调用Traceback模块,就可以在try语句块中正常显示详细的报错信息了。用于提高我们解决问题的效率。
调用traceback.print_exc()实现异常信息的详细内容在控制台打印的效果。
整个异常处理的一节课,除去最基本的异常处理行为之外,更多你们需要懂得如何去控制异常,从而控制你的代码逻辑。
'''
# try...except示例
try:
1 / 0
print('123')
except ValueError as ve:
print(ve)
print(2)
except ZeroDivisionError as zd:
print(zd)
print(3)
except Exception as e: # Exception表示任意异常。 as表示起别名。 e表示别名
print(e) # 打印的是捕获的异常
print(1)
print(Exception) # 打印的是Exception类的信息
文件操作
-
file=open(file=file,moode=mode,encoding="")操作模式:
- r 表示只读模式,允许对文件内容进行读取。但是无法编辑修改。
- w 表示写入模式,允许对文件的写入操作。但是会将文件原有的内容全部清空,以覆盖的形态实现文件的内容写入
- a 表示追加模式,允许对文件的追加写入,默认都是在文件的末尾进行追加
- b 表示二进制模式。一般用于非文本文件内容的操作(图片、视频等)
文件操作函数:
- file.read():读取文件所有内容
- file.readline():读取一行内容
- file.readlines():获取文件的全部内容,每一行都是一个单独的元素。最终返回list
- file.write(str=str):文件写入字符串,针对不存在的文件进行写入或者追加,默认会生成对应的文件之后,再继续执行你的写入和追加操作。但是文件夹必须存在
- file.close():关闭文件
-
with open
with open(filename=filename,mode=mode,encoding="") as 别名:
文件操作代码块
通过with open的语法结构,在文件操作结束之后,会自动帮你close掉操作的对应文件。无需再调用close方法
反射
反射机制,是通过字符串来驱动代码的机制。基于字符串来找到对应的模块(类)之中的方法或者属性的一种模式。通过反射机制可以极大地降低我们的代码冗余,可以简化我们的代码逻辑。也可以让我们的程序具备有一定的动态能力(在代码执行过程中自我修改的能力)。
- getattr(类或模块,属性或方法):获取类中的属性或方法,getattr()(方法参数):加括号是调用获取到的方法
- setattr(类或模块,属性,修改后的值):存在修改属性,不存在新增
- hasattr(类或模块,属性或方法):判断类的属性或者方法是否存在,有返回True,否则返回False
- delattr(类或模块,属性或方法):删除类中存在的属性,删除类中的方法,该方法继续存在,但无法被调用。
反射的应用场景
动态调用方法
根据用户输入或配置文件的字符串,动态调用对应的方法:
# 假设用户输入的操作是字符串 "say_hello"
action = input("请输入操作:") # 输入 say_hello
if hasattr(p, action):
func = getattr(p, action)
func() # 动态调用 say_hello 方法
Yield
Yield关键字:
yield是Python中的特殊关键字,是一个生成器
迭代,其实就是循环的概念:
- 可迭代对象,其实就是可以通过循环来实现操作的对象。
- 迭代器,属于可迭代对象,一次只能取一个值,一直取值到全部取完为止(程序终结为止)
- 生成器,属于特殊的迭代器。只能通过yield关键字来定义。
yield关键字只能应用在函数或者方法之中。
yield类似于函数中的return关键字,都是属于返回值的操作行为。return返回之后,函数终止运行。而yield在调用后,一次会生成一个值,一直到所有值生产完,在运行期间,会处于挂起状态,随时等待下一次调用
- iter(可迭代对象):将可迭代对象转为迭代器。
- next(迭代器):next读取迭代器中的值,每次读取一个值,光标移至下一行。
用处 测试时,使用yield返回测试数据
Logging库
日志等级
debug<info<warning(default)<error<critical
import logging
# logging的示例
# 创建日志对象,进行对应的设置
logging.basicConfig(
level=logging.DEBUG, # 默认为warning等级,可自行修改。
# 日志的格式显示:整体的设置就是基于str内容来定义。levelname表示日志等级,asctime表示时间,
#filename表示执行文件,lineno表示执行行数,message表示执行文本信息
format='%(levelname)s %(asctime)s %(filename)s %(lineno)s : %(message)s',
encoding='utf-8',
filename='./log.log' # 设置日志的保存路径,日志文件需要用.log来进行保存
)
# 调用日志对象实现日志的输出
logging.debug('这是debug')
logging.info('这是info')
logging.warning('这是warning')
logging.error('这是error')
logging.critical('这是critical')
logging库的四大组件:
- logger记录器:是日志系统的入口,负责定义日志的名称、设置日志的级别、并提供日志记录的方法。
- 过滤日志:根据设置的日志级别过滤日志,只处理级别不低于设定值的日志。
- 颁发日志:将过滤后的日志传递给handler处理器,由处理器完成实际的输出(打印到控制台或写入文件)
- handler处理器: 负责将 Logger 传递的日志进行具体输出,也就是控制台或者文件。
- 定义输出目的地:不同的
Handler对应不同的输出方式(StreamHandler输出到控制台,FileHandler写入文件)。 - 二次过滤日志:每个
Handler可以单独设置日志级别,进一步过滤Logger传递过来的日志(只输出级别不低于自身设定的日志)。 - 格式化日志:配合
Formatter定义日志的输出格式(如包含时间、日志级别、模块名等)
- 定义输出目的地:不同的
- filter过滤器:提供更加细化的内容控制
- formatter格式化器:提供日志的所有输出格式。
#创建logger
logger=logging.getLogger('root')
#logger处理不低于DEBUG的日志
logger.setLevel(logging.DEBUG)
#创建handler (控制台输出)
console_handler=logging.StreamHandler()
#仅输出INFO及以上的日志
console_handler.setLevel(logging.INFO)
#创建handler(文件输出)
file_handler=logging.FileHandler('./log_config.log')
#仅输出WARNING及以上的日志
file_handler.setLevel(logging.WARNING)
#定义日志格式
formatter=logging.Formatter("%(levelname)s %(asctime)s %(filename)s %(lineno)s : %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
#给logger注册handler
logger.addHandler(console_handler)
logger.addHandler(file_handler)
#输出日志
logger.debug('debug message')
logger.info('info message')
logger.warning('warning message')
日志配置文件
file=pathlib.Path(__file__).parents[0].resolve() / 'log_config.ini',parents查找父类,通过下标来查找,0是上级,1是上上级...。定位到当前文件的父目录,resolve()是转化成绝对路径。/拼接日志配置文件。
def get_logger():
# 配置文件的路径
# file = './log_conf.ini'
# parents[num]的num=0表示上一级,1则为上一级的上一级,以此类推
file = pathlib.Path(__file__).parents[0].resolve() / 'log_config.ini'
print(file)
# 基于ini文件实现日志的配置项
logging.config.fileConfig(file, encoding='utf-8')
# 创建日志记录器
logger = logging.getLogger()
return logger # 一定要return日志记录器,否则无法使用。
log = get_logger()
log.info('这个是配置文件的info')
log.debug('这个是配置文件的debug')
ini配置文件
主要结构为sections和key-values
[sections]
key1=value1
key2=value2
一个ini文件可以存在多个section,section之间不可嵌套。
logging配置文件
[loggers]
keys = root
[handlers]
keys = fileHandler,streamHandler
[formatters]
keys = simpleFormatter
[logger_root]
level = DEBUG
handlers = fileHandler,streamHandler
[handler_fileHandler]
class = FileHandler
level = DEBUG
formatter = simpleFormatter
args = ('./log_config.log','a','utf-8')
[handler_streamHandler]
class = StreamHandler
level = DEBUG
formatter = simpleFormatter
[formatter_simpleFormatter]
format = %(levelname)s %(asctime)s %(filename)s %(module)s %(funcName)s %(lineno)s : %(message)s
pymysql
# 链接数据库所需的配置信息
db_info = {
'host': '127.0.0.1', # 数据库的ip地址
'port': 3306, # 数据库端口号
'user': 'root', # 账号
'password': 'root', # 密码
'database': 'world' # 连接的数据库名称
}
# 基于配置信息连接数据库
conn = pymysql.connect(**db_info) # 定值不定长传参方式。
# 创建游标
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) # 将数据结果以字典的格式进行返回。
# 为了避免sql注入,需要用到另外一种占位符:%s
sql="select * from table where id=%s"
cursor.execute(sql,参数)
results=cursor.fetchall()#获取氯碱有的查询结果,并返回为list数据类型
results=cursor.fetchmany(n)#获取 n行数据
conn.commit()#sql提交
conn.rollback()#sql回滚
cursor.close()/conn.close()#关闭游标和数据库连接
# 数据库的增删改
try:
sql = 'update city set Name="huangcaicai" where Name="Kabul"'
cursor.execute(sql)
# conn.rollback() # 修改前撤回
conn.commit() # 二次确认sql的修改行为
# conn.rollback() # 修改后撤回无效
except Exception as e:
# 撤回。专业叫做回滚操作。取消之前所执行的所有对应修改操作行为。但是对已经commit的行为无效
conn.rollback()
finally:
# 关闭游标和数据库连接
cursor.close()
conn.close()
数据库ini配置文件
[TEST_ENV]
host = 127.0.0.1
port = 3306
user = root
password = 123456
database = test_db
[ONLINE_ENV]
host = www.baidu.com
port = 3306
user = root
password = 123456
database = test_db
读取数据库配置文件
import configparser
import pathlib
# 获取ini配置文件中的对应配置项,基于section进行获取。project_name = section
def read(project_name):
# 获取pymysql的配置文件
file = pathlib.Path(__file__).parents[0].resolve() / 'mysql_conf.ini'
# 创建configparse实例,用于读取ini配置文件
conf = configparser.ConfigParser()
# 读取整个文件
conf.read(file)
# conf.items(section)读取文件中某个section的所有键值对
values = dict(conf.items(project_name))
# 当获取到key为port的时候,value就需要转型为int类型。其余的key不需要做任何修改操作
for key, value in values.items():
if key == 'port':
values[key] = int(value) # values['port'] = int(value)
return values
# print(read('ONLINE_ENV'))
#连接配置文件中的指定环境
conn = pymysql.connect(**read('TEST_ENV')) # 定值不定长传参方式。
自动化测试
目前行业自动化测试就是UI自动化和接口自动化两大类。但是理论知识有三层:
- Unit层:单元测试层。说白了就是白盒测试。是测试颗粒度最小的测试层级。白盒测试,在国内主要由开发人员来完成。
- Service层:说白了就是接口测试层。一般是由开发人员和测试人员共同完成的。接口测试是目前市场非常重要的一个环节。如果你具备有一定的测试能力,学好接口自动化对于找工作会非常有帮助。
- UI层:所有的一切测试行为都是基于系统界面来进行的。可以细分为WebUI自动化和APPUI自动化两类。属于最接近实际用户操作行为的层级。
浏览器开发者工具
Source模块
当要定位临时出现的元素,例如下拉元素,可以下拉的时候按F8,打断点,再到Element里查看源代码。
在临时出现的元素右键选中检查也可以定位到临时元素
Selenium
元素定位
- 通过id进行定位:
driver.find_element('id','') - 通过name属性进行定位:
driver.find_element('name','') - 通过class name进行定位:
driver.find_element('class name','') - 通过css selector进行定位:
driver.find_element('css selector','#kw') - 通过xpath进行定位:
driver.find_element('xpath','//*[@id="kw"]')
xpath选择器
相对路径 //
寻找文档中的所有元素 //*
绝对路径 /
查找父节点 ..
当标签存在多个相同时,last()可以定位到最后一个 //标签名[last()] //span//ul//li[last()]
定位到倒数第二个//span//ul//li[last()-1]
属性查找 @ //标签名[@元素名称='元素值']
定位到id元素: //input[@id='kw']
and表达式定位同一标签下多个元素
//标签名[@元素名称='元素值' and @元素名称='元素值']
or元素满足其中一个条件就可以定位到
//标签名[@元素名称='元素值' or @元素名称='元素值']
查找元素值不等于某个值的内容
//标签名[@元素名称!='元素值']
模糊匹配
//标签名[contains(text(),"xxx")]匹配text()包含xxx
//标签名[text(),"xxx"] 匹配标签的文本内容为xxx
使用position位置定位
定位到th标签下的第一个
//th[@class='c-id' and position()=1]
或者//th[@class='c-id' and position()<2]
定位多个li中的第n个,下标从1开始
//li[n]
选取若干路径
//book/title | //book/price 获取book元素的所有title和price元素
常规操作
"""
Selenium中常见的操作行为:
1. 操作行为分为浏览器操作和元素操作。基本上所有的操作行为都属于这两大类。
浏览器操作行为是针对浏览器本身
元素操作行为是针对已获取的元素对象来执行。
2. 元素操作的行为都是基于已经获取到元素之后才可以进行的。
"""
from time import sleep
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.select import Select
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service)
# 常规操作行为
# 浏览器最大化
driver.maximize_window() # 属于一个操作步骤,会有明显的浏览器大小变化。如果要使用一般是放在最开始的位置。
# # 设置浏览器尺寸
# driver.set_window_size(200, 800) # 没有必要与浏览器最大化一起使用。
# 访问URL
driver.get('http://www.baidu.com') # url一定要完整的内容,缺少前缀则无法识别,代码会报错。
# 获取浏览器title信息
print(driver.title) # 一般在调试的时候用的会更多一些
# 元素操作都是在find_element之后进行的。
# 元素的输入:send_keys。该方法只能应用于input标签。
# driver.find_element('id', 'kw').send_keys('黄财财')
# 元素的文件上传:send_keys。只能对input标签实现文件的上传。上传文件时,需要传入文件的完整路径及文件名和后缀名
# sleep(1)
# driver.find_element('xpath', '//span[@class="soutu-btn"]').click()
# sleep(1)
# driver.find_element('xpath', '//input[@value="上传图片"]').send_keys(r'D:\SF.jpg')
# # 元素的点击:click。实现对元素进行单击操作行为。
# driver.find_element().click()
# 鼠标悬停:ActionChains类可以帮助我们实现悬停操作,操作过程中记得不要移动鼠标
# ActionChains对象 ,用于封装一系列鼠标/键盘操作(如点击、悬念、拖拽)。driver是当前的浏览器驱动实
# 例,表示要在这个浏览器上执行操作。.perform()是执行封装好的操作。
# ActionChains(driver).move_to_element(
# driver.find_element('xpath', '//span[@id="s-usersetting-top"]')).perform()
# sleep(3)
# driver.find_element('link text', '高级搜索').click()
'''
下拉列表框的操作:推荐的方式是两次click来实现下拉列表框的值的选择。
第一次点击下拉列表框
第二次点击下拉列表框的值
Select标签的下拉列表框选择(技术相对老旧,一般少见。)
<select id="hcc" name="123">
<option selected="true;" value="17">财财</option>
<option value=""> 新地址 </option>
</select>
'''
# sleep(1)
# driver.find_element('xpath', '//span[@id="adv-setting-gpc"]/div').click()
# driver.find_element('xpath', '//p[text()="一年内"]').click()
# select下拉列表框的操作
# select = Select(driver.find_element('id', 'hcc'))
# select.select_by_index(1) # 基于选项的下标选择。默认从1开始,也就是财财选项
# select.select_by_value("17") # 基于选项的value属性进行选择。也就是财财选项
# select.select_by_visible_text("新地址") # 基于选项的文本进行选择,也就是新地址选项
# 浏览器的关闭:Selenium默认在程序结束时会调用driver.quit()实现资源的释放。但是还是建议各位在程序末尾手动添加quit方法的调用
# driver.close() # 关闭当前标签页
# driver.quit() # 关闭整个浏览器并释放资源。结束当前driver生命周期。
# 当调用quit之后,driver对象无法再继续使用,因为已经结束了。如果还需要调用driver,则记得新建一个新的driver对象再继续操作。
# driver.get('http://www.jd.com') # 此处会报错
sleep(10)
句柄(handles)(浏览器标签页)
# 获取所有窗口句柄
all_handles = driver.window_handles
# 输出类似: ['CDwindow-xxx', 'CDwindow-yyy']
print("所有窗口句柄:", all_handles)
# 获取当前窗口句柄
current_handle = driver.current_window_handle
# 输出当前活跃窗口的 handle
print("当前窗口句柄:", current_handle)
# 句柄切换
from selenium.webdriver.support.ui import WebDriverWait
# 1. 记录操作前的所有窗口句柄(可能包含多个)
original_handles = set(driver.window_handles) # 转为集合,方便求差集
print("操作前的窗口句柄:", original_handles)
# 2. 执行打开新窗口的操作(如点击链接/按钮)
driver.find_element(By.ID, "open_new_window").click()
# 3. 等待新窗口出现(直到窗口数量增加)
WebDriverWait(driver, 10).until(
lambda d: len(set(d.window_handles) - original_handles) > 0
)
# 4. 计算新窗口句柄(原集合中不存在的那个)
new_handles = set(driver.window_handles) - original_handles # 差集运算
new_handle = new_handles.pop() # 取出唯一的新窗口句柄(若多个新窗口,需循环处理)
# 5. 切换到新窗口
driver.switch_to.window(new_handle)
print("切换到新窗口,句柄为:", new_handle)
# 切换句柄
handles = driver.window_handles # 获取当前所有的句柄
print(handles)
driver.close() # 关闭旧的标签页
driver.switch_to.window(handles[1]) # 切换到新的句柄页
frame窗体
'''
frame窗体:内嵌窗体。相当于是在原有的html的页面中内嵌了一个独立的html页面。frame中的元素无法被直接访问和操作。
如果想要访问frame中的元素,则需要先切换至frame之中,再操作该元素。
注意,切换进入frame之后,frame以外的元素则无法操作。需要切换出来才可以继续操作frame以外的元素。
'''
driver.get('https://wx.mail.qq.com/')
# 进入第一层iframe
driver.switch_to.frame(driver.find_element('xpath', '//div[@id="QQMailSdkTool_login_loginBox_qq"]/iframe'))
# 进入第二层iframe
driver.switch_to.frame(driver.find_element('id', 'ptlogin_iframe'))
driver.find_element('link text', '密码登录').click()
sleep(5)
# 如果要操作iframe以外的元素,则需要先切换至默认窗体
driver.switch_to.default_content() # 切换至默认窗体的操作方法。
driver.find_element('link text', '基本版').click()
sleep(10)
浏览器操作行为
'''
浏览器操作行为:
1. 自动生成新的浏览器或者标签页
Selenium默认情况下创建的浏览器是带有数据隔离的。也就是沙箱功能。
主要用于解决多用户业务流程的自动化执行。
2. 注意事项:
1. 必须是Selenium4+的版本才支持该方法。
2. python版本必须在3.10+才支持
3. 不管是new window还是new tab,都可以通过句柄来控制。
4. 生产新的tab或者window后,句柄会自动切换到新的对象。
'''
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service)
driver.get('http://www.baidu.com')
# 创建一个新的浏览器或者标签页,通过参数type hint来实现,tab表示标签页,window表示浏览器,自动切换句柄到新生成的标签页或者浏览器
# driver.switch_to.new_window('tab') # 创建一个新的标签页
driver.switch_to.new_window('window') # 创建一个新的浏览器
driver.get('http://www.jd.com')
print(driver.title)
sleep(5)
等待与断言
自动化过程中,在页面访问和请求时,所需要的时间,程序是不会考虑进去的。因为系统内容的加载需要时间,但程序执行速度非常快,所以在加载页面内容的时候,我们需要让程序等一下。于是乎有了等待的概念。通过等待机制,可以让我们的自动化有更加好的稳定性。
三类等待机制
强制等待
代码在执行到强制等待时,不论在做什么,都必须停下来,一直到等待时间结束为止。再继续执行后续的代码。
time库下的sleep方法来实现。标准写法:
time.sleep(int: 等待时间)
sleep(int: 等待时间)
等待时间单位为秒。强制等待非常适合学习过程中进行使用。或者调试代码的时候以及一些特殊场景会应用强制等待,平时基本不用。因为代码冗余会过高,且容易产生不必要的浪费时间。
隐式等待
# 隐式等待:一般都是在创建driver对象之后,就直接设置定义。
driver.implicitly_wait(10) # 设置10秒的最大隐式等待时间。
是driver中的一种设置项。设置driver对象的全局等待时间。针对整个driver的生命周期都有效。每一次driver的操作,都会调用隐式等待。一旦可以正常执行操作时,就会直接执行代码,不会产生多余的等待时间。
如果等待的元素一直到最大等待时间还未出现,则会直接执行代码。不再继续等待。
隐式等待是一种只需要设置一次,即可全局生效的非常便利的等待方式。但是一旦等待不到元素,则会持续等待最大时间,再抛出异常,从某种意义上而言,会比较浪费时间。
显式等待
专门用于等待指定元素而进行的等待机制。类似于强制等待。如果获取到元素,则直接进行后续的操作。否则会一直等待到最大时长。然后抛出超时的异常。
显式等待的方法相对复杂一些,对新手不太友好。但是可以满足针对单元素进行等待的效果。
包含有两种方法,效果完全相反。搞懂一种另一种自然就懂了:
- until,等待某个元素,一直到条件满足为止
- until_not,等待某个元素,一直到条件不满足为止。
# 显式等待:基于lambda会返回一个等待成功的元素对象,后续可以直接使用。
# 第二个参数:表示最长等待时间(单位:秒),如果超过这个时间,等待条件仍未满足,会抛出timeoutException异常
# 第三个参数:表示轮询间隔时间(单位:秒),即每隔多久检查一次条件是否满足
# 等待 element表示的对象出现,同时返回给el,el表示WebElement对象,供后续操作使用
el = WebDriverWait(driver, 5, 0.5).until(
lambda element: driver.find_element('xpath', '//*[@id="1"]/div/h3/a'),
message='元素获取失败'
)
显式等待可以有效针对某个特定的元素进行等待的操作。但是因为用法相对复杂,所以学习成本会更高。
一般都是隐式等待为主,同时结合显式等待来实现等待机制的完善。如果在显示等待时报错,则抛出异常timeout,并且等待的最大时间是取决于最长的等待设置。
官网有明确的说明:不建议显式等待和隐式等待一起使用,可能会产生不可预期的问题。但是目前而言我从来没有遇到过。所以可以忽略。
加载策略
'''
页面加载策略的修改:自动化测试执行的提升效率的手段之一。但是使用此方法需要慎重,因为可能会产生未知的风险。
Selenium总计有三种不同的页面加载策略:
1. normal:默认加载策略,所有内容全部加载完后才会继续后续的代码执行
2. eager:只加载基本的dom树,不考虑任何静态资源
3. none: 只加载基本的页面结构,不考虑其他任何东西
'''
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
# 页面加载策略的修改
# 创建浏览器设置对象
options = webdriver.ChromeOptions() # 你是什么浏览器就创建什么类型的options对象
# 页面加载策略
# options.page_load_strategy = 'eager' # 设置为eager等级
# options.page_load_strategy = 'none' # 设置为none等级
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service, options=options) # 如果要让设置生效,记得传入options对象作为参数
# 隐式等待:一般都是在创建driver对象之后,就直接设置定义。
driver.implicitly_wait(10) # 设置5秒的最大隐式等待时间。
driver.get('http://www.baidu.com')
driver.find_element('id', 'chat-textarea').send_keys('黄财财')
driver.find_element('id', 'chat-submit-button').click()
sleep(10)
断言
在自动化测试中,每一个测试行为都需要一个回馈机制,通过回馈机制来确认我们的整个测试的结果是否通过。回馈机制就是断言,断言就是自动化测试中的预期结果与实际结果的对比。
在自动化测试执行中,UI自动化执行的是系统的固定业务流程。如果要进行断言,则直接根据流程结果时的最终结果进行断言即可。
断言校验的实现:
断言基本都是基于assert关键字来实现的。assert 表达式,message
- 表达式: 当断言表达式为True,则断言通过,程序无任何问题。为False时,程序抛出断言异常。显示message
- Message:显示报错信息,自定义str内容。
断言最常见的手段就是判断某个元素的文本信息。除此之外,还可以使用元素是否存在于页面来进行断言。所以显式等待除了做常规等待之外,也可以适用于断言的特定场景。
python中将assert断言放在try except中,失败断言运行后的结果也会是成功,需要在except中添加raise将异常抛出。
class Test01():
def test01(self):
try:
assert 1==2
except AssertionError as e:
print(e)
raise e
from time import sleep
import ddddocr
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.wait import WebDriverWait
# erp的断言示例
# 准备环境与测试数据
user = 'jsh'
password = '123456'
company = '公司test' # 断言数据
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service)
driver.implicitly_wait(10)
driver.maximize_window()
# 访问url
driver.get('http://39.101.122.147:3000/user/login')
# 输入账号密码
driver.find_element('id', 'loginName').send_keys(user)
driver.find_element('id', 'password').send_keys(password)
# 基于ddddocr实现验证码的处理
# 1. 获取验证码图片
file = driver.find_element('xpath', '//img[@data-v-4f5798c5]').screenshot_as_png
# 2. 基于ddddocr解析验证码图片,生成验证码的值,将验证码图片内容解析成文字信息
driver.find_element('id','inputCode').send_keys(ddddocr.DdddOcr(show_ad=False).classification(file))
# 点击登录按钮
driver.find_element('xpath', '//button[@data-v-4f5798c5]').click()
sleep(3)
断言示例
获取实际结果
reality = driver.find_element('xpath', '//span[@class="company-name"]').text
# text是获取指定元素的文本信息基于日志进行断言信息的输出方式之一。
try:
assert company == reality, f'''
你的预期结果是{company},你的实际结果是{reality},两者不相等
预期结果为:{company},
实际结果为:{reality},
断言结果:{company} != {reality}
'''
# 输出正常通过的日志
except:
# 输出错误日志
raise
# 显式等待用于断言的场景。
WebDriverWait(driver, 5, 0.5).until(
lambda element: driver.find_element('xpath', '//span[text()="欢迎您,测试用户"]'),
message='登录失败,元素不存在'
)
sleep(10)
显式等待用于断言的场景。
# 登录成功了,出现相应的文本,登录失败则没有相对的文本
WebDriverWait(driver, 5, 0.5).until(
lambda element: driver.find_element('xpath', '//span[text()="欢迎您,测试用户"]'),
message='登录失败,元素不存在'
)
sleep(10)
Docement和js执行器
开发者模式的控制台输入。
'''
document对象操作:
1. getElementById(),通过id获取到指定元素的定位方法
2. setAttribute(属性,值),设置指定元素的属性值,如果属性不存在,则为新增属性,如果属性存在,则为修改属性值
3. removeAttribute(属性),将指定元素的指定属性删除。
4. innerHTML="值",为指定元素进行文本的添加,若元素本身已有文本,则为修改文本内容。并将文本值返回
js操作滚动条。已弃用。因为有更好的更简单的选择。通过ActionChains类来实现。
'''
from time import sleep
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.service import Service
# js执行器的调用示例
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service)
driver.implicitly_wait(5)
driver.set_window_size(200, 500)
driver.get('http://www.baidu.com')
# js执行器调用
# js语句定义,个人不推荐的使用方法,因为获取元素的手段太过单一。
# js = 'document.getElementById("chat-textarea").setAttribute("hcc","oioioi")'
# 如果定义的js需要考虑将结果返回,则一定在语句前添加return关键字
# js = 'return document.getElementById("chat-textarea").innerHTML="iuiuiuiu"'
# js = 'return document.getElementById("chat-textarea")'
# 个人推荐的使用方式:更加实用。
js = 'arguments[0].setAttribute("hcc","oiiiiii")' # arguments[0]简单理解为占位符
# js执行器,arguments[0]表示的webelement通过xpath定位
text = driver.execute_script(js, driver.find_element('xpath', '//*[@id="chat-textarea"]'))
# text = driver.execute_script(js,) # js执行器
# print(text)
# ActionChains操作页面滚动条
ActionChains(driver).scroll_to_element(driver.find_element('id', 'chat-submit-button')).click(driver.find_element('id', 'chat-submit-button')).perform()
sleep(50)
关键字驱动
关键字驱动设计模式介绍
掌握自动化测试技术,一定是掌握自动化测试框架才算真正意义上掌握自动化技术。最核心的框架技术之一,就是设计模式。
主流的设计模式分为两类:
- 关键字驱动设计模式
- 相对较为传统的设计模式。早期的关键字驱动形态就是基于测试工具演变而来的。
- 其实就是编程思维逻辑的体现。也就是对于代码的封装和调用的处理手段。
- POM设计模式:全新的一种设计模式。
所以关键字驱动其实就是函数的封装。一般常见的是封装各类操作行为。可以满足各种不同业务类型的测试需求。当然,也可以基于操作流程来实现封装。所有的封装一定会考虑到代码的复用性问题。如果是一次性的代码,完全是没有必要进行封装的。同时关键字驱动实现,可以极大降低代码的冗余。提升自动化测试的维护性以及代码的灵活度。
关键字驱动设计模式,虽然相对比较传统,但是因为其具备有跨业务的能力。也就是所谓的多个不同的业务系统,都可以基于同一套关键字驱动设计的自动化测试,来实现测试效果。关键字驱动就是典型的一种以一对多的测试框架技术。
设计模式不是测试框架的全部,只是其中的一个核心技术点而已。基于设计模式可以定下框架的主调。
'''
WebUI关键字驱动类的实现:
1. 本身属于逻辑代码类
2. Selenium的二次封装。优先封装基于你的业务需求或者最常见的操作行为,以此作为关键字来进行使用。
访问url
点击
输入
查找元素
关闭
。。。。。。。
关键字驱动类不需要十全十美。因为实际业务场景下是多变的。只需要考虑系统需要什么,我们就封装什么。
如果有缺失,自己再补就可以了。
逻辑代码本身只负责代码逻辑的封装,运行文件不会有任何的作用。逻辑代码一定是在调用时才会产生实际价值。
'''
from time import sleep
import ddddocr
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from options_demo.options import options
# 关键字驱动类的示例
# 创建浏览器对象
def open_browser(type_):
# if type_ == 'Chrome':
# driver = webdriver.Chrome()
# elif type_ == 'Safari':
# webdriver......
# 基于反射实现浏览器生成
try:
if type_ == 'Chrome':
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service, )
else:
driver = getattr(webdriver, type_.capitalize())()
# type_ = 'safari'
# webdriver.Safari()
except:
driver = webdriver.Chrome()
return driver
# 基于匹配机制实现浏览器生成:具体方案内容,大家课后可以尝试补足逻辑
# browsers = {
# 'Chrome': ['gc', 'google chrome', 'chrome', 'Chrome', '谷歌浏览器'],
# 'Edge': ['Edge', 'edge']
# }
# for key, value in browsers.items():
# if type_ in value:
# driver = getattr(webdriver, key)()
# return driver
class WebKeys:
# 临时的driver对象
# service = Service('../chromedriver.exe')
# driver = webdriver.Chrome(service=service, options=options())
# 构造方法
def __init__(self, type_):
self.driver = open_browser(type_)
self.driver.implicitly_wait(5)
# 访问url
def open(self, url):
self.driver.get(url)
# 定位元素
def locator(self, by, value):
return self.driver.find_element(by, value)
# 输入
def input(self, by, value, txt):
self.locator(by, value).send_keys(txt)
# 点击
def click(self, by, value):
self.locator(by, value).click()
# 关闭
def quit(self):
self.driver.quit()
# 强制等待
def wait(self, time_):
sleep(int(time_))
# 断言
def assert_text(self, expected, by, value):
reality = self.locator(by, value).text
assert expected == reality, f'''
预期结果为{expected},实际结果为{reality}。
{expected} != {reality}
'''
# 提取验证码
def get_code(self, by, value):
# 保存验证码为png图片
file = self.locator(by, value).screenshot_as_png
# ddddocr实现对验证码内容的识别
return ddddocr.DdddOcr(show_ad=False).classification(file)
基于关键字驱动实现自动化测试
'''
基于关键字驱动类封装的关键字实现自动化测试效果。属于测试代码
测试代码其实就是人类按照系统业务流程,进行的流程操作步骤。从而实现自动运行的效果。
说白了就是测试代码是测试用例。
'''
from web_keys.keys import WebKeys
# 实例化关键字类,进行自动化操作
driver = WebKeys('Chrome')
driver.open('http://www.baidu.com')
driver.input('id', 'chat-textarea', '黄财财关键字')
driver.click('id', 'chat-submit-button')
# driver.wait(20)
driver.assert_text('预期结果', 'xpath', '//*[text()="实际结果"]')
driver.quit()
为了能够更好地在整个自动化中对测试数据进行管理。以及说后续的测试维护行为。我们需要将测试数据单独提取出来,以文本的形态来进行管理和存放。通过文件管理的方式可以实现数据与代码的完全分离。从而提升到测试效率。这就是所谓的数据驱动的形态。(其实就是基于文件来管理测试的执行)
常见的数据驱动形态:
- Excel数据驱动:
- 适合从0开始搭建自动化测试的团队。以及接口自动化测试
- 学习成本非常低,代码实现的需求相对而言技术难度较低。针对整体技术实力相对比较弱的团队,是可以快速上手的一种数据驱动技术。
- 维护成本相对较高。数据的管理相对而言会更麻烦一些。
- Yaml、Json、Py数据驱动:
- 数据管理相对灵活,维护性较好。整体的维护成本很低。
- 学习成本太高。
- 适用于有一定自动化测试积累的团队或者有自动化测试团队的公司来进行使用。
- mysql数据驱动
- 自主创建测试数据的第三方库
openpyxl
常用方法
'''
openpyxl的基本应用:
1. excel文件操作:
创建/打开excel文件
找到对应的sheet页
找到对应的cell
对cell进行编辑或者读取
2. 可以实现对文件的读取和写入。包括excel的各类样式的配置
不要在pycharm中直接创建excel文件,否则会出现文件损坏的情况。所有的excel文件都在你的本地创建。
'''
import openpyxl
# Openpyxl示例
# 读取excel中的内容
# 1. 找到excel文件
excel = openpyxl.load_workbook('./demo.xlsx') # 读取工作簿
# 找到对应的sheet页
sheet = excel['Sheet3'] # 获取sheet页是以字典的方式来获取的。
# 获取对应单元格的内容
print(sheet['A1'].value)
# 获取整个sheet页的单元格内容
print(sheet.values)
for value in sheet.values:
print(value) # 每一行都是一个元组
# excel写入操作
# 找到指定的单元格,对其输入你要写入的内容
sheet['A3'] = '这是新写入的内容' # 写入方式1,如果只是简单的写入推荐此方法。
sheet.cell(row=1, column=3).value = '这是复杂的写入方式2'
# 如果有写入操作一定要保存,否则不会生效
excel.save('./demo.xlsx')
# 文件操作结束后一定要关闭文件
excel.close()
基于excel驱动自动化测试操作行为
'''
基于excel驱动自动化测试操作行为
'''
import traceback
import openpyxl
from web_keys.web_keys import WebKeys
def arguments(data):
temp_data = {}
# 参数解析:要么为None,要么为str
if data: # 判断为None则不处理
str_temp = data.split(';') # 将str基于分号分割,生成list
for temp in str_temp:
t = temp.split('=', 1) # 基于=进行二次分割,并且每个元素只分割一次
temp_data[t[0]] = t[1] # 将二次分割后的list中,0为key,1为value,赋值到字典之中。
'''
例如:type_=Chrome
解析后变为['type_','Chrome']
赋值后变为temp_data['type_'] = 'Chrome'
也就意味着temp_data={
'type_': 'Chrome'
}
'''
return temp_data
# 读取excel文件
excel = openpyxl.load_workbook('./demo.xlsx')
sheet = excel['Sheet2']
# 获取用例内容
for value in sheet.values:
# print(value)
# 基于用例编号来判断用例正文
if type(value[0]) is int:
# print(value)
print('当前正在执行操作:' + value[3])
# 解析测试参数:将用例中的str变为操作方法的参数格式
print(value[2])
data = arguments(value[2]) # 基于**kwargs定值不定长传参逻辑,实现对参数的二次处理
print(data)
'''
操作步骤其实就分为以下类型:
1. 实例化
2. 常规操作行为
3. 断言,因为需要对excel文件进行额外的写入操作
'''
if value[1] == 'open_browser': # 实例化
wk = WebKeys(**data)
elif 'assert' in value[1]: # 所有断言函数都是基于assert来命名
# 通过与否是基于断言是否报错来决定的。
try:
getattr(wk, value[1])(expected=value[4], **data)
# 写入pass
sheet.cell(row=value[0] + 3, column=6).value = "PASS"
except:
traceback.print_exc()
# 写入failed
sheet.cell(row=value[0] + 3, column=6).value = "Failed"
finally:
excel.save('./demo.xlsx')
else:
getattr(wk, value[1])(**data) # 操作行为基于反射机制实现
基于excel驱动自动化测试操作行为
'''
基于excel驱动自动化测试操作行为
'''
import traceback
import openpyxl
from conf.get_logger import get_logger
from excel_driver.excel_styles import pass_, failed_
from web_keys.web_keys import WebKeys
# 日志对象获取
logger = get_logger()
# 测试结果记录
success = 0
failed = 0
failed_cases = []
def arguments(data):
temp_data = {}
# 参数解析:要么为None,要么为str
if data: # 判断为None则不处理
str_temp = data.split(';') # 将str基于分号分割,生成list
for temp in str_temp:
t = temp.split('=', 1) # 基于=进行二次分割,并且每个元素只分割一次
temp_data[t[0]] = t[1] # 将二次分割后的list中,0为key,1为value,赋值到字典之中。
return temp_data
# 测试结果输出
def sum_info():
logger.warning(f'''
成功用例数:{success}条,
失败用例数:{failed}条,
失败用例信息:{failed_cases}
''')
def run(file):
global success, failed
try:
# 读取excel文件
excel = openpyxl.load_workbook(file)
logger.info(f'启动{file}测试用例文件,开始读取内容...')
# sheet = excel['Sheet1']
for name in excel.sheetnames: # 获取excel中所有的sheet页
sheet = excel[name]
logger.info(f'正在执行{name}页中的测试用例...')
# 获取用例内容
try:
for value in sheet.values:
# 基于用例编号来判断用例正文
if type(value[0]) is int:
# print(value)
logger.info('当前正在执行操作:' + value[3])
# 解析测试参数:将用例中的str变为操作方法的参数格式
data = arguments(value[2]) # 基于**kwargs定值不定长传参逻辑,实现对参数的二次处理
if value[1] == 'open_browser': # 实例化
wk = WebKeys(**data)
elif 'assert' in value[1]: # 所有断言函数都是基于assert来命名
# 通过与否是基于断言是否报错来决定的。
try:
getattr(wk, value[1])(expected=value[4], **data)
# 写入pass
pass_(sheet.cell(row=value[0] + 3, column=6))
success += 1
except:
traceback.print_exc()
# 写入failed
failed_(sheet.cell(row=value[0] + 3, column=6))
failed += 1
failed_cases.append(file + ':' + name)
finally:
excel.save(file)
else:
getattr(wk, value[1])(**data) # 操作行为基于反射机制实现
except:
logger.error(traceback.format_exc())
failed += 1
failed_cases.append(file + ':' + name)
except:
logger.error(traceback.format_exc())
finally:
excel.close()
# sum_info()
PASS与Failed样式定义
'''
PASS与Failed样式定义
'''
from openpyxl.styles import PatternFill, Font
# 成功:绿底,加粗。
def pass_(cell):
# 单元格的值
cell.value = 'PASS'
# 设置单元格的样式和颜色
cell.fill = PatternFill('solid', 'AACF91') # 单元格绿底
cell.font = Font(bold=True) # 字体加粗
# 失败:红底,加粗。
def failed_(cell):
# 单元格的值
cell.value = 'FAILED'
# 设置单元格的样式和颜色
cell.fill = PatternFill('solid', 'FF0000') # 单元格绿底
cell.font = Font(bold=True) # 字体加粗
程序主入口,所有一切都从此开始。
import os
import threading
from excel_driver.excel_driver import run, sum_info
# 程序主入口,所有一切都从此开始。
if __name__ == '__main__':
# run('./test_data/demo.xlsx')
# 获取指定路径下的所有测试用例
cases = [] # 测试用例集
# 获取到test_data路径下的所有测试用例:必须是xlsx文件才算测试用例
# os.walk()可以有效识别指定路径下的文件、子文件夹以及路径
for path, dirs, files in os.walk('./test_data'):
# 判断获取的files是否为测试用例,基于文件后缀名判断
for file in files:
filenames = os.path.splitext(file) # 基于文件名称解析文件名与后缀名
# if filenames[1] == '.xlsx' and '_new' in filenames[0]:
if filenames[1] == '.xlsx':
cases.append(path + '/' + file) # 添加用例至用例集
# print(cases)
# 用例的执行
# for case in cases:
# run(case)
# 用例并发处理
'''
当用例需要执行的数量过多时,会极大降低我们的工作效率。所以我们需要有特定的手段来解决问题
解决方案:
1. 基于分布式测试框架部署来实现测试的效率提升。主体技能基于Selenium grid。
整体基于可联网模式下,完成的M/S分布式结构实现。
2. 基于多线程的结构形态来实现用例并发处理。只需要单点服务即可完成。
一个进程拥有多个线程。基于不同的测试用例,启动不同的线程来独立完成整个测试过程。
'''
# threading来实现多线程的处理
# 建立线程组
th = []
# 基于用例创建对应线程
for case in cases:
thread = threading.Thread(target=run, args=[case]) # 创建线程,分派对应任务给到线程
th.append(thread) # 线程添加至线程组
# 启动线程
for t in th:
t.start()
# 单独运行测试结果数据统计,记得放在最后。
# sum_info()
'''
同步与异步?
所有程序默认都是同步运行的。也就是依次运行。可以满足业务的关联性。但是效率太低。
异步,让所有的程序可以同时运行,不再基于依次的顺序来执行。
'''
ChromeOptions设置项
浏览器本身具备有非常多的设置项,在实际测试过程中我们需要调整浏览器的设置来实现更好的测试行为。
为了更好满足UI自动化测试的效果,所以针对不同的浏览器,Selenium提供有不同的Options类。专门用于对浏览器进行相关的设置。
浏览器的设置很多,课堂上只会讲解到基本的应用方式和常见的设置项,如果有特殊需求可以百度搜索一下。
- 封装自己的Options函数/类,自行决定内容如何封装
- 实现对erp系统的业务自动化流程操作。自行选择一个流程执行即可。
- erp url:http://39.101.122.147:3000/user/login
- 账号密码:jsh,密码是123456
- 解决流程自动化,关联如何处理验证码以及断言校验。
- 封装自己的关键字驱动类基于已封装的关键字驱动类,实现erp的业务流程自动化执行。
options基础项
'''
Chrome浏览器配置项:
所有的配置项都是基于ChromeOptions类来实现的。想要让配置项生效,则一定在初始化driver对象的时候将
option对象添加进去。
不同的浏览器有不同的options对象,根据你的需求自行选择。
'''
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from options_demo.options import options_func
# option示例
option = webdriver.ChromeOptions()
# 页面加载策略
# option.page_load_strategy = 'normal'
# 设置各类内容,只会调用两个方法
# option.add_argument() # 设置常规项,用于定义已经稳定的普通的设置项
# option.add_experimental_option() # 试验性质的设置项,设置的内容可能存在有不稳定的状态。
# 这两项设置无法与页面最大化一同使用
# # 设置窗体在指定坐标启动
# option.add_argument('window-position=500,500')
#
# # 设置窗体的初始尺寸
# option.add_argument('window-size=1000,200')
# 页面最大化
option.add_argument('start-maximized')
# 去掉黄条警告
# option.add_experimental_option('excludeSwitches', ['enable-automation'])
# option.add_experimental_option('disable-infobars') # 无效方案,如果看到网上有这个代码,直接关掉网站即可。
# 无头模式:浏览器不以界面形态启动,而是在后台静默运行。节省硬件资源损耗。但是依旧可以正常执行所有的操作内容。
# option.add_argument('--headless')
# 账号密码的弹窗屏蔽
# prefs = {
# 'credentials_enable_service': False,
# 'profile.password_manager_enable': False
# }
#
# option.add_experimental_option('prefs', prefs)
# 加载本地缓存
'''
selenium默认启动的浏览器是无缓存浏览器,与常规手动启动的浏览器有很大区别。
webdriver启动的浏览器默认不会加载本地的任何数据。默认为一个全新的浏览器。
首先,Selenium不处理任何的验证码。所以一旦自动化中遇到验证码一定记得交由开发协助处理。不要去考虑如何破译
验证码的问题。加载本地缓存可以作为处理验证码的手段之一。本质解决方法就是通过加载缓存,绕过登录,从而实现对
验证码的处理。
如果要加载本地缓存,记得程序执行前先关闭所有的浏览器。否则会启动失败。
慎用,会有问题出现
'''
# option.add_argument(r'--user-data-dir=C:\Users\15414\AppData\Local\Google\Chrome\User Data')
# 去除控制台的多余日志信息。
option.add_experimental_option('excludeSwitches', ['enable-logging'])
# # 如果常规日志去除无效果,则可以调用以下方法
option.add_argument('--log_level=3')
option.add_argument('--disable-gpu')
option.add_argument('--ignore-certificate-errors')
# 创建driver对象
service = Service('../chromedriver.exe')
# driver = webdriver.Chrome(service=service, options=option) # 将option对象添加作为参数。
driver = webdriver.Chrome(service=service, options=options_func()) # 将option对象添加作为参数。
driver.implicitly_wait(5)
driver.get('http://www.baidu.com')
driver.find_element('id', 'chat-textarea').send_keys('黄财财自动化')
driver.find_element('id', 'chat-submit-button').click()
sleep(3)
print(driver.title)
sleep(10)
ChromeOption封装
from selenium import webdriver
# options封装示例
def options_func():
option = webdriver.ChromeOptions()
# 页面加载策略
# options.page_load_strategy = 'normal'
# 设置各类内容,只会调用两个方法
# options.add_argument() # 设置常规项,用于定义已经稳定的普通的设置项
# options.add_experimental_option() # 试验性质的设置项,设置的内容可能存在有不稳定的状态。
# 这两项设置无法与页面最大化一同使用
# # 设置窗体在指定坐标启动
# options.add_argument('window-position=500,500')
#
# # 设置窗体的初始尺寸
# options.add_argument('window-size=1000,200')
# 页面最大化
option.add_argument('start-maximized')
# 去掉黄条警告
option.add_experimental_option('excludeSwitches', ['enable-automation','enable-logging'])
# options.add_experimental_option('disable-infobars') # 无效方案,如果看到网上有这个代码,直接关掉网站即可。
# 无头模式:浏览器不以界面形态启动,而是在后台静默运行。节省硬件资源损耗。但是依旧可以正常执行所有的操作内容。
# options.add_argument('--headless')
# 账号密码的弹窗屏蔽
# prefs = {
# 'credentials_enable_service': False,
# 'profile.password_manager_enable': False
# }
#
# options.add_experimental_option('prefs', prefs)
# 加载本地缓存
'''
selenium默认启动的浏览器是无缓存浏览器,与常规手动启动的浏览器有很大区别。
webdriver启动的浏览器默认不会加载本地的任何数据。默认为一个全新的浏览器。
首先,Selenium不处理任何的验证码。所以一旦自动化中遇到验证码一定记得交由开发协助处理。不要去考虑如何破译
验证码的问题。加载本地缓存可以作为处理验证码的手段之一。本质解决方法就是通过加载缓存,绕过登录,从而实现对
验证码的处理。
如果要加载本地缓存,记得程序执行前先关闭所有的浏览器。否则会启动失败。
'''
# option.add_argument(r'--user-data-dir=C:\Users\15414\AppData\Local\Google\Chrome\User Data')
# 去除控制台的多余日志信息。
# option.add_experimental_option('excludeSwitches', ['enable-logging'])
# 如果常规日志去除无效果,则可以调用以下方法
option.add_argument('--log_level=3')
option.add_argument('--disable-gpu')
option.add_argument('--ignore-certificate-errors')
return option # 封装成函数后,一定记得添加return
UnitTest介绍
目前市场,UnitTest依旧还是主流。UnitTest本身只是测试框架中用于管理测试用例和实现测试报告的一个模块而已。并不能代表一个完整的框架。可以更好地帮助我们管理测试用例。
UnitTest四大核心组件:
- 前后置条件:在整个测试执行过程中,我们需要提前准备测试前的内容,这叫做前置条件。在测试结束之后需要执行的相关操作叫做后置条件。
- 测试用例:所有的测试流程的内容,基于测试用例统一进行管理。UnitTest对于测试用例有自己独特的规范和要求定义。
- 断言机制:UnitTest内部封装有非常多的断言函数,可以直接调用。
- 套件与运行器:
- 套件:用于批量管理测试用例的模块
- 运行器:用于生成测试报告的模块
Python中,UnitTest属于官方自带库,可以直接导入使用,不需要额外安装。
'''
UnitTest的语法规则与基本应用:
1. 所有的测试用例文件,必须以test_开头。格式:test_*.py
2. 所有的UnitTest相关内容必须要写在class之中。class必须要继承于UnitTest.TestCase类。类名必须以Test开头
3. 测试用例:
1. 都是基于方法的结构形态来实现的。方法名称必须以test_开头作为命名。否则无法识别为测试用例
2. 所有的测试用例,在执行的时候都是基于main之中,通过unittest.main()方法来运行。
3. UnitTest不需要实例化,而且用例之间虽然可以相互调用。但是没有任何意义。
4. 用例在执行的时候,基于先后顺序不需要考虑单个用例的逻辑补足。每一个用例在执行时彼此都会产生基本关联。
在基于UnitTest实现用例定义时,我们在设计时必须要考虑尽可能降低用例之间的关联性
5. Unittest之中,用例的执行有它自己的默认排序规则:
规则定义是0-9,a-z,A-Z的排序规则。规则固定不变的(除非你修改UnitTest的源代码)
个人推荐的用例命名规范:(以公司编码规范要求为首要定义。)
test_编号_业务名称()
test_01_login()
6. 测试用例虽然是方法的结构,但是不推荐使用return。避免用例之间的相互调用。如果需要关联到用例产生的
数据,则建议以成员属性赋值的方式来完成。
7. 用例总计有三种不同的状态:
1. PASS 通过
2. Failure 失败 用例中的断言执行失败。
3. Error 错误 用例执行时代码出现报错(非断言报错),error状态只有测试报告会显示
用例成功与否,是基于用例代码是否报错来界定的。所以我们在用例之中基本不会使用try...except语法。
如果非要加异常处理,可以在except之中添加raise关键字
8. 断言机制:
所有的断言都是基于self.assert*()来实现
4. 前置与后置条件的使用:
1. 测试场景:
准备测试数据,准备测试环境
基于数据与环境,实现用例的执行
基于测试结果,进行断言校验
清空脏数据,还原环境。
2. 前置
前置的方法名称是固定的。不可以修改。相同作用域下的前置方法只能有一个。
作用域分为:用例级和类级。
用例级:每一个测试用例都会执行
类级:每一个测试类只会执行一次
3. 后置
与前置相同
每一个UnitTest类,都是独立存在的个体,类彼此之间不会有任何关联。类作为用例管理的唯一单位。
测试业务管理:
1. 基于类来实现流程的管理。
1. 基于一个用例实现完整的业务流程
2. 基于不同的用例实现不同的子流程,基于用例执行顺序最终拼接成完整业务流程。
个人推荐此方法,因为可以细化测试用例。定义好前后置条件后,方便用例的管理和维护
2. 一个py文件有多个类和一个py文件关联一个类
推荐第二种,便于代码的管理和维护
'''
import unittest
# unittest使用规范定义
class TestDemo(unittest.TestCase):
# 属性定义
demo = None
# 类级前置与后置
@classmethod # 定义装饰器
def setUpClass(cls):
print('这是类级前置')
@classmethod
def tearDownClass(cls):
print('这是类级后置')
# 用例级前置条件
def setUp(self):
print('这是用例级前置')
# 用例级后置条件
def tearDown(self):
print('这是用例级后置')
# 测试用例
def test_demo01(self):
# self.test_demo()
print('这是第二条测试用例01')
1 / 0
def test_demo(self):
print('这是一条测试用例00')
TestDemo.demo = 1 # 属性赋值的方式实现数据的互通,但是要注意用例的执行先后顺序
def test_demo03(self):
try:
print('这是一条测试用例03')
assert 1 == 0
except:
raise
# print('这是异常')
# print('这是一条测试用例03')
# assert 1 == 0
def test_demo02(self):
self.hcc_func()
print('这是一条测试用例02')
print(self.demo)
self.dd(12)
# 非测试用例
def hcc_func(self):
print('这不是一条测试用例')
def dd(self, a):
try:
assert 1 == a
except:
print('处理异常')
raise
class TestDemo01(unittest.TestCase):
def test_01(self):
print('new test case')
# self.assert*()
if __name__ == '__main__':
unittest.main()
# TestDemo().test_demo01()
单元测试erp
'''
用例文件的定义和管理:
基于erp系统实现客户记录的新增与删除功能
整个实操案例:
1. 登录erp系统
2. 获取客户信息
3. 新增一条客户记录
4. 修改/删除新增的客户记录信息
5. 断言校验流程的正确性。
用例的设计:
1. 拆分子流程
2. 封装不同的子流程
3. 进行最终业务结果的断言校验
4. 考虑前置与后置条件的相关应用
1. 前置条件和后置条件不参与任何的操作行为。
2. 子流程业务必须完整,且前面的子流程可以不断言,可以断言。根据你的需要。
3. 每一个用例之间一定要尽可能地低耦合状态。
'''
import unittest
from time import sleep
from web_keys.web_keys import WebKeys
class TestAdd(unittest.TestCase):
# 定义前置条件。
@classmethod
def setUpClass(cls):
cls.driver = WebKeys('Chrome')
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def test_01_login(self):
self.driver.open('http://39.101.122.147:3000/user/login')
self.driver.input('id', 'loginName', 'jsh')
self.driver.input('id', 'password', '123456')
code = self.driver.get_code('xpath', '//img[@data-v-4f5798c5]')
self.driver.input('id', 'inputCode', code)
self.driver.click('xpath', '//button[@data-v-4f5798c5]')
sleep(5)
def test_02(self):
self.driver.click('xpath', '//span[contains(text(),"退出登录")]')
sleep(5)
if __name__ == '__main__':
unittest.main()
测试套件
所有的测试用例都是基于class来生成和管理的。在默认情况下,UnitTest对于测试用例的执行管理相对是比较单一和固定的。测试套件就是可以更好实现对用例的批量灵活管理的手段。在原有的UnitTest运行机制下,实现对用例的批量化管理的效果。
测试套件,本质上就是一个list。
测试套件
'''
测试套件:
1. 单独新建一个py文件用于编写套件代码的相关逻辑。
2. 套件就是用于添加测试用例的。执行套件其实也就是执行添加的指定套件中的所有测试用例。
3. 套件的运行必须关联运行器。
4. 添加套件的测试用例,在执行时,不会遵循UnitTest的排序规则。
'''
import HTMLTestReportCN
import HTMLTestRunner
import os.path
import unittest
from suite_runner.unit_demo import TestDemo, TestDemo01
# 套件添加测试用例的示例
# 创建套件
suite = unittest.TestSuite() # 相当于创建一个空的list
# 添加测试用例
# 1. 基于测试用例的名称来实现对用例的添加
# suite.addTest(TestDemo('test_demo03')) # 添加TestDemo类下的test_demo03测试用例至套件之中
# suite.addTest(TestDemo('test_demo02'))
# suite.addTest(TestDemo('test_demo'))
# 2. 批量实现对用例的添加
# cases = [TestDemo('test_demo02'), TestDemo01('test_01')] # 本质上还是基于用例名称来实现用例的添加
# suite.addTests(cases)
# 3. 基于class来实现对用例的添加。用例执行的顺序还是遵循UnitTest的默认用例排序规则。
# cases = [unittest.TestLoader().loadTestsFromTestCase(TestDemo)]
# suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDemo))
# suite.addTests(cases)
# 4. 基于py文件来实现对用例的添加
# cases = ['unit_demo1', 'unit_demo'] # 基于文件名称生成list,默认会读取整个py文件所有的class
# suite.addTests(unittest.TestLoader().loadTestsFromNames(cases))
# 5. 基于discover来实现用例的添加。个人推荐方式。
path = './' # 用例获取的路径
discover = unittest.defaultTestLoader.discover(start_dir=path, # 获取用例的起始路径
pattern='unit_*.py' # 用例获取规则
) # 默认返回一个测试套件
# 套件的运行:需要基于运行器来实现
# runner = unittest.TextTestRunner(verbosity=2) # 默认运行器。控制台的文本测试报告展示。verbosity=2表示最详细的信息记录
# runner.run(suite) # 基于运行器执行测试套件
# runner.run(discover) # 基于运行器执行测试套件
# 第三方测试报告,本质上也就是一个运行器。用来执行测试套件的。
# 测试报告配置
report_dir = './report/' # 测试报告保存路径
report_file = report_dir + 'report2.html' # 测试报告名称,有需要可以自行添加时间戳
report_title = '这是测试报告标题'
report_description = '这个是测试报告的描述'
report_tester = '黄财财' # 测试者,reportCN中额外的参数
# 判断测试报告保存路径是否存在
if not os.path.exists(report_dir):
os.mkdir(report_dir)
# 测试报告生成
with open(report_file, 'wb') as file:
# runner = HTMLTestRunner.HTMLTestRunner(stream=file,
# title=report_title,
# description=report_description,
# verbosity=2)
runner = HTMLTestReportCN.HTMLTestRunner(stream=file,
title=report_title,
description=report_description,
tester=report_tester,
verbosity=2)
runner.run(discover)
'''
1. 如果想要实现更深层次的用例管理,一定会关联到套件的应用。
2. 一般而言套件和测试报告的使用,我们会提前封装成函数的形态来执行,也可以关联邮件自动发送或者第三方办公软件的api接入。
3. 测试报告的命名和管理一般是基于时间戳来实现的。
但是,如果你是基于多线程来实现不同套件的并发用例效果。时间戳一定要精确到最少毫秒级别
'''
YAML文件
- yaml是一种缩进敏感的文件形式。所有的上下级关系都是基于缩进来进行控制的。
- list数据,- 表示list数据类型。一定要记得-后面添加一个空格,否则无法识别
- dict数据,: 表示字典格式,一定要记得:后面添加一个空格,key在左边,value在右边
列表
- a
- b
- 1
- 2
- 3
相当于[a,b,[1,2,3]]
字典
key: value
key1: value1
key2: value2
key3:
key3.1: value3.1
key3.2: value3.2
key3.3:
key3.3.1: value3.3.1
读取yaml文件
import yaml
def read_yamml(file):
with open(file=file,mode='r',encoding='utf-8') as f:
values=yaml.safe_load(f)
写入yaml文件
import yaml
data=[{
'name':'John Smith',
'age':30
}
]
filename='data.yaml'
with open(filename,'w') as f:
documents=yaml.safe_dump(data,f)
解决在相同场景下,有大批相同数据的情况中,对于数据的管理和维护。
- 锚点:
&定义。类似于定义一个变量的效果 *表示引用。也就是所谓的变量的调用<<表示合并引用的所有键值对
-
common: &common
url: http://www.baidu.com
input:
by: xpath
value: //textarea[@id="chat-textarea"]
click:
by: id
value: chat-submit-button
wait: 5
txt: xxx
-
common:
<<: *common
txt: xxx2
DDT数据驱动
DDT全称叫做data driver tests。数据驱动测试。提供有基本的数据驱动能力,更多是应用在数据驱动的过程中,实现对数据文件的读取和数据传递到测试用例的功能。在UnitTest体系下非常推荐使用ddt来实现你的数据驱动相关的效果。同样也只限于UnitTest中进行使用。其余地方不要应用ddt。
ddt是基于装饰器对模块实现应用,只对调用ddt的class有效。只能对单个class产生作用,需要在测试类上面加上@ddt装饰器,使用装饰器指定测试数据。
- 在测试用例上方使用@data('xxx')可以将数据传入测试用例,装饰器有多组数据(基于
,分组),会执行相应的次数。 - 当测试用例有多个参数时,
@data(['xxx','bbb'])里需要将多个变量打包成列表,然后使用@unpack装饰器将列表解包。 - 测试用例使用ddt后会把用例的名称后面加上参数,因此会改变测试用例的名称。
- 当数据多时,可以使用读取文件的方式读取数据。
@file_data装饰器专门解析yaml文件。
导包
import unittest
from ddt import ddt, data, unpack, file_data
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from ddt_yaml.web_keys import WebKeys
def read_file():
values = []
with open('./1.txt', 'r', encoding='utf-8') as file:
for line in file.readlines():
values.append(line)
return values
# ddt示例
@ddt # 声明后,当前类中所有用例都可以使用ddt进行数据驱动和管理。
class TestDDT(unittest.TestCase):
'''
@data装饰器基于参数中有多少组数据,则对应的用例会执行相应的次数。
在装饰器中,基于,进行数据的解析,最终基于数据组数,执行对应次数的测试用例。
@unpack装饰器,实现对数据的二次解包。用于解包list或者元组
@data(['huangcaicai', '小助理'],['hcc', 'xzl'])
@unpack
@data基于,进行数据的第一次拆解,将,解析后获取两组不同的list。
@unpack进行数据的二次解包,基于,进行数据的二次拆分。
['huangcaicai', '小助理']拆分为'huangcaicai'和'小助理'两个不同的数据
然后基于用例所需参数,依照参数的顺序依次传入用例之中。
在UnitTest中,通过@data和@unpack可以管理简单的测试数据。如果数据一旦多起来,则不推荐使用。
在用例调用ddt之后,测试用例执行时会因为ddt而修改用例的名称。所以套件添加用例的方法里,无法基于测试用例名称实现对用例的添加。
'''
# @data('huangcaicai','小助理')
# @data(['huangcaicai', '小助理']) # 用于管理测试数据
# @unpack # 二次解包数据
# def test_01_login(self, name, pwd):
# # def test_01_login(self, name,):
# print('这是login测试用例')
# print(name)
# print(pwd)
# 基于ddt实现文件读取操作:本质上是通过read_file方法实现的文件内容读取,而非@data实现的。
@data(*read_file())
def test_02_read(self, value):
print(value)
@file_data('./search_old.yaml') # 专用于解析yaml文件内容
# @file_data('./login.yaml') # 专用于解析yaml文件内容
# def test_01_search(self, **kwargs):
def test_01_search(self, common, txt):
driver = WebKeys('Chrome')
driver.open(common['url'])
driver.input(**common['input'], txt=txt)
driver.click(**common['click'])
driver.wait(common['wait'])
# print(kwargs)
# print(kwargs['login']['user'])
# print(kwargs['login']['pwd'])
if __name__ == '__main__':
unittest.main()
POM设计模式
POM,全称叫做PageObject Model。也就是页面对象模型。简称PO、POM、PO模型等等等等,是一种自动化测试框架的设计模式。专门用于实现UI自动化测试的一种模型。业内公认最佳的设计模式。
关键字驱动设计模式。是一种以一对多的自动化测试实现。POM是针对单一系统的自动化测试实现。POM的优势就是以一对一的形态,实现对系统的量身定制的效果。满足被测系统更加细致的测试覆盖,以及更为灵活的维护性。
POM是针对单一系统完成的自动化测试框架定制。所以对于其他系统而言,是无法实现有效迁移的。但是对于被测试的系统,具备有更好的维护性,可以做到更细致的测试覆盖。在后期代码的维护和管理过程中,可以更加便捷。
对于实际的测试需求而言,一定记住合适的才是最好的。
POM设计思路
从登录到修改个人信息业务的测试执行。
常规的测试思路:我们所关注的测试点其实是每一个操作步骤之中,我们所需要操作的元素以及对应的数据。
- 进入登录页
- 输入账号密码,点击登录按钮
- 点击个人中心按钮,进入个人信息页
- 选择需要修改的内容,进行内容编辑与修改
- 点击保存按钮
- 确认修改是否成功。
POM,全称叫做PageObject Model。也就是页面对象模型。简称PO、POM、PO模型等等等等,是一种自动化测试框架的设计模式。专门用于实现UI自动化测试的一种模型。业内公认最佳的设计模式。
关键字驱动设计模式。是一种以一对多的自动化测试实现。POM是针对单一系统的自动化测试实现。POM的优势就是以一对一的形态,实现对系统的量身定制的效果。满足被测系统更加细致的测试覆盖,以及更为灵活的维护性。
POM是针对单一系统完成的自动化测试框架定制。所以对于其他系统而言,是无法实现有效迁移的。但是对于被测试的系统,具备有更好的维护性,可以做到更细致的测试覆盖。在后期代码的维护和管理过程中,可以更加便捷。
对于实际的测试需求而言,一定记住合适的才是最好的。
POM+UnitTest自动化测试实现
依旧遵循代码与数据分离,逻辑代码与测试代码分离的基本概念。
POM的核心结构分为四个层级(在不同的公司可能会有四个或者更多个层级。但是不管是多少层,一定是基于我们所讲的这四个层级来实现的。):
- 基类,BasePage:类似于关键字驱动类,用于封装Selenium的底层逻辑代码。基于行为进行封装,但是和关键字驱动有些许差别。
- 页面对象类,PageObject:POM的核心层级。主要用于封装和管理不同的页面对象。包括页面中的核心元素及操作子流程
- 测试用例类:TestCases:测试代码类,用于编写测试用例和管理维护用例。通过UnitTest或者Pytest都可以。
- 测试数据类:TestData:用于保存和管理维护测试过程中所需要用到的数据。
Pytest
- 所有的测试文件(.py)必须满足
test_*.py的命名格式来实现。 - 所有的测试类和测试用例都必须以test开头。类名就是
Test*,用例名称就是test_* - 在pytest中,有很多用例增强的功能。这些内容绝大部分都是基于装饰器的形态来实现的。如果需要有任何用例增强的需要,优先考虑基于装饰器来实现。
- pytest本身是基于指令来执行的。
Pytest的类前置和类后置
添加@classmethod,因为前置和后置方法是因级别的共享资源(所有实例都要用到)参数为cls(类本身),如果不加会被认为实例方法(需要通过类的实例调用),但在测试框架执行类的前置操作时,还未创建任何实例,调用此实例方法会导致错误。
# 类前置:在类中所有测试方法执行前运行一次
@classmethod
def setup_class(cls):
pass
@classmethod
def teardown_class(cls):
pass
'''
pytest基本应用:
1. 基于类和方法来实现对用例的管理。可以通过类来管理,也可以直接通过方法来进行管理。具体看你是如何设计用例结构的。
2. 不需要继承任何内容,直接运行即可。
3. pytest中没有用例的排序。运行顺序就是谁先定义谁先运行。
4. pytest是基于指令来运行的:
可以在main()中,通过pytest.main()来实现执行。也可以在cmd中通过指令来执行
1. -s 表示将print的内容显示在控制台之中
2. -v 表示将运行的日志显示在控制台有更加详细完整的内容。
3. 指定文件/类/测试用例执行
'test_hcc.py::TestDemo::test_func03',test_hcc.py文件下的TestDemo类下的test_func03用例
常规情况下的文件路径依旧是基于/来区分。只有类和方法的层级是基于::进行区分
4. -k 表示执行包含有指定字符串的测试用例。类似于模糊查找的概念
5. -q 静默运行
6. -x 表示当前用例报错了,本次运行直接终止,未运行的测试用例不再执行。
7. --maxfail=num,如果用例执行过程中报错总数达到num值时,本次运行结束。
8. -n num,表示用例的多线程执行。num表示启动的线程数量
默认情况下pytest不支持-n指令。需要额外安装插件pytest-xdist来实现支持。
指令一般分为字母和单词的形式,字母指令是单个-,而单词指令是--。
5. pytest在执行时需要指定路径。根据路径来获取当前符合条件的所有测试用例文件及用例内容。
6. pytest下的断言都是基于assert来实现的。
7. 所有pytest运行指令,都可以基于控制台指令来实现,也可以基于pytest.main([])方式来实现。
推荐指令运行pytest
'''
import pytest
# pytest示例
class TestDemo:
def test_func01(self):
print('这是测试用例1')
assert 1 == 0
def test_func03(self):
print('这是测试用例3')
def test_func02(self):
print('这是测试用例2')
class TestDemo1:
def test_function01(self):
print('这是测试用例2.1')
if __name__ == '__main__':
pytest.main(['-s', '-v', '--maxfail=1'])
Mark装饰器
对测试用例进行标记
'''
mark装饰器的使用:
1. @pytest.mark 实现对用例进行标记
@pytest.mark.标记名称
2. 可以实现对用例进行标记分类的作用
3. 运行指定的标记测试用例,需要调用-m指令,指令支持逻辑运算符
-m 标记名称 执行指定标记名称的所有用例
-m "login or hcc" 执行login标记或者是hcc标记的测试用例
-m "login and hcc" 执行标记为login,并同时为hcc的测试用例
-m "not login" 执行标记不为login的测试用例
4. -m指令下的标签名称选择,一定要用双引号括起来,否则识别会出问题。
5. 标签名称不要太复杂,尽可能简单。方便标签的管理
'''
import pytest
# mark装饰器使用示例
# 多个标记的使用,是不同标记名称都分别使用pytest.mark来进行标记
@pytest.mark.xzl
@pytest.mark.login
def test_func01():
print('这是一条基本的测试用例')
@pytest.mark.hcc
def test_func02():
print('这是二条基本的测试用例')
def test_func03():
print('这是三条基本的测试用例')
def test_func04():
print('这是三条基本的测试用例')
def test_func05():
print('只有3.10以上python才可以运行')
if __name__ == '__main__':
pytest.main(['-sv', './test_mark.py', '-m hcc'])
Parametrize装饰器
@pytest.mark.parametrize(*args,[],ids=[])
- 形参:与用例的参数名一致
- 列表:实参,通过列表向形参传参
- ids:每个用例的描述输出到控制台,有几组数据就要几个描述
- ids不支持中文,要支持中文,新建
conftest.py文件
# conftest.py是文件固定名称,不允许修改。否则不生效。所有的hook函数都是写在conftest之中的。
# ids解析中文,正常显示中文内容的设置定义,通过hook函数来实现。所有代码内容都是固定的,不需要做任何修改
def pytest_collection_modifyitems(items):
for item in items:
item.name = item.name.encode('utf-8').decode('unicode_escape')
item._nodeid = item._nodeid.encode('utf-8').decode('unicode_escape')
'''
Parametrize装饰器的应用
1. 基于@pytest.mark.parametrize来实现调用的。
2. 参数介绍:
参数1:必须与用例形参保持完全相同
参数2:必须为list格式
3. 多参数下,如果通过多个Parametrize实现数据传递,则pytest会采用交集的计算方式时间数据的传入
4. 多参数下,如果通过一个Parametrize传入所有数据,则会基于合并之后的数据进行传入和解析。
5. 所有的数据,都是基于Parametrize中定义的形参顺序依次传入的。
6. 如果从外部获取数据:
1. 定义一个list的数据变量,进行传入
2. 获取文件内容进行传入
'''
import pytest
import yaml
# Parametrize装饰器使用示例
@pytest.mark.parametrize('a', [1, 2, 3])
def test_func00(a):
print(a)
# @pytest.mark.parametrize('a', [1, 2, 3])
# @pytest.mark.parametrize('b', ['a', 'b', 'c'])
# @pytest.mark.parametrize('b,a', [(11, 22), (33, 44)])
# def test_func01(a, b):
# print(a)
# print(b)
# 从外部变量获取数据
# data = [1, 2, 3, 4, 5]
#
#
# @pytest.mark.parametrize('a', data)
# def test_func02(a):
# print(a)
# ids默认不支持中文的显示
ids = ['基于hcc数据组实现的操作行为', '基于xzl数据组实现的操作行为']
# 从外部文件获取数据,也可以封装一个yaml文件读取的函数,在此处调用。
@pytest.mark.parametrize('data', yaml.safe_load(stream=open('./demo.yaml', 'r', encoding='utf-8')), ids=ids)
def test_func03(data):
print(data)
# parametrize特定参数组的添加
@pytest.mark.parametrize('a,b', [(1, 2),
pytest.param(11, 22, id='特殊数据')],
ids=['第一轮', '第二轮'])
def test_func04(a, b):
print(a)
print(b)
pytest断言机制
'''
assert断言机制:
1. pytest中都是使用assert实现断言。
2. assert本身的内容定义,在pytest会有完整的信息展示,所以不需要自己额外去进行定义更详细内容
3. 用例在执行失败之后,pytest具备有用例失败重跑机制。
通过指令--lf来实现。全称叫做 last failures
将上一次失败的用例全部重新执行一次
--ff指令。failures first,将所有的用例全部执行,但是失败的用例优先执行。
--count=num,用例执行次数。在执行测试用例的时候,通过手动设置num数值,来让用例执行规定的总次数
一般是结合--lf一同使用。实现所谓的用例重跑机制。失败用例多次执行。
count指令如果要使用,需要提前添加插件:pip install pytest-repeat
--lf和--ff指令,是否能够应用,优先取决于你的用例设计结构上来定义。
'''
def test_01():
assert 1 == 2,'这是断言失败后的message'
def test_02():
assert 1 == 0
def test_03():
assert 1 == 1
def test_04():
assert 1 == 4
flaky装饰器
@pytest.mark.flaky() 是 pytest-flaky 插件提供的一个装饰器,用于在使用 pytest 进行测试时,自动重试失败的测试用例。它是一种非常实用的“失败重运行机制”,特别适用于那些偶尔会因为环境不稳定(如网络波动、浏览器加载慢、第三方服务抖动等)而失败的 UI 自动化或集成测试
安装flaky插件pip install pytest-flaky
import pytest
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_example():
import random
assert random.choice([True, False]) # 50% 概率失败
参数说明
reruns:失败后最多重试次数(默认为 1)。reruns_delay:每次重试前等待的秒数(默认为 0)。
上例中,如果测试失败,最多会再运行 3 次,每次间隔 1 秒。
全局配置
在pytest.ini设置默认重试策略
# pytest.ini
[tool:pytest]
addopts = --reruns 2 --reruns-delay 1
注意:命令行参数
--reruns是pytest-rerunfailures插件的功能,和pytest-flaky不同。两者功能类似,但@pytest.mark.flaky更灵活(可针对单个测试标记)。
Fixture
夹具,在pytest中可以通过Fixture来实现对前置和后置的内容完成
- 准备环境,准备数据
- 基于已有的数据进行实际的测试执行
- 检验测试结果
- 资源释放
Fixture基本应用:
- Fixture是基于函数的形态来实现的。调用的时候,通过函数名称来进行调用。
- Fixture定义时,需要通过Fixture的装饰器来进行声明
- 因为fixture本身属于函数,所以可以通过return来实现对需要获取的内容进行返回。在用例中可以直接调用return的数据
- 在用例中,可以同时调用多个Fixture来实现测试用例内容的完善
- 调用Fixture是通过在用例中定义形参,形参名称必须与Fixture保持一致
- fixture运行顺序是基于用例中形参的顺序依次执行。
- Fixture一定是先于测试用例来执行的。
- 缓存机制。在用例执行的时候,Fixture可以有多次的被调用。在第一次请求的时候,如果有返回值,则会通过这个返回值来实现后续的所有操作。而非重新运行
- fixture因为是函数,本质也是代码的运行,所以在定义Fixture的时候,你的代码可能会有概率报错。如果在执行用例,并且关联Fixture的时候,Fixture产生了报错,则代码终止运行。测试用例不会执行。返回error状态。
- Fixture只有被调用了才会执行。否则不会运行。Fixture默认不执行,但是有一个参数叫做autouse参数。默认为False,如果设置为True,则每一个用例执行前,都会调用该Fixture来执行。如果Fixture有return值,则完全没有定义为autouse的意义存在。
from time import sleep
import pytest
@pytest.fixture
def demo():
print("这是demo的fixture函数")
return 123
@pytest.fixture
def demo1():
print("这是demo1的fixture函数")
return "abc"
# 测试用例先运行fixture函数
def test_func(demo,demo1):
print("这是测试用例")
print(demo)# 通过形参输出返回值
print(demo1)
# fixture与fixture之间调用
@pytest.fixture(scope="session")
def driver():
service=Service('../myselenium/chromedriver.exe')
driver=webdriver.Chrome(service=service)
driver.implicitly_wait(5)
return driver
# 调用driver
@pytest.fixture()
def open(driver):
driver.get("http://www.baidu.com")
# 测试用例调用driver和open
def test_01(driver,open):
driver.find_element('id','chat-textarea').send_keys('aaa')
sleep(5)
def test_02(driver):
driver.get("http:///www.jd.com")
@pytest.fixture
def a():
return 'a'
@pytest.fixture(scope='session')
def b():
print("b Fixture")
return []
@pytest.fixture
def c(a,b):
b.append(a)
#autouse=True,每一个测试用例都会默认执行这个fixture
@pytest.fixture(autouse=True)
def error():
1/0
# 先运行a,b,c的fixture函数
def test_test03(a,b,c):
print("测试用例")
# a返回'a',b返回添加'a'的列表,c返回None
print(a)
print(b)
print(c)
def test_04(b):
print(b) # 因为b的scope='session'有缓存机制,['a']
if __name__ == '__main__':
pytest.main(['-v', '-s', __file__])
Fixture实行前置
基于Fixture实现pytest的前置操作:
- pytest中所有的前置都是基于Fixture来完成的。Fixture默认的作用域是当前文件。
- 除非是定义Fixture在conftest之中。则可以实现对当前路径及子路径全部有效。要先确认你的pytest版本及文件命名是否规范。
- pytest启动的时候,是会产生一个session对象来管理本次所要执行的所有测试用例。所以本次执行的代码,都是基于当下的session来统一管理的。所以Fixture定义作用域可以分为以下不同的等级:
- 所有的Fixture等级都基于参数scope来实现管理和定义
- session级别:整个测试session中都有效。只会在第一次调用的时候执行一次,后续调用都是基于已有的数据来继续执行
- module级别:整个py文件执行一次
- class级别:整个class执行一次
- function级别:每一个测试用例执行一次。scope的默认等级
import pytest
# session级别
@pytest.fixture(scope='session')
def session_func():
print('这个是session级别')
# module级别
@pytest.fixture(scope='module')
def module_func():
print('这个是module级别')
# class级别,类级别的前置
@pytest.fixture(scope='class')
def class_func():
print('这个是class级别')
# function级别,默认,用例级别的前置
@pytest.fixture
def func_func():
print('这个是func级别')
class TestDemo:
def test_01(self, session_func, module_func, class_func, func_func):
print('这是测试用例01')
# session_func, module_func, class_func在一个class里都执行一次
def test_02(self, session_func, module_func, class_func, func_func):
print('这是测试用例02')
fixture参数传入:
如果要解决Fixture传参的操作:
- 基于Fixture需要的数据,通过其他的Fixture传入进来。
- 通过request来接收外部数据的传入
# 基于request实现对外部数据的传入操作
@pytest.fixture()
def demo(request):
name, value = request.param # request是固定名称,不允许修改。
print('这是Fixture数据的传入')
print(name)
print(value)
# 用例传入数据
# indirect=True表示为调用的是Fixture,而非普通参数
@pytest.mark.parametrize('demo', [['name参数', 'value参数']], indirect=True)
def test_01(demo):
print('这是测试用例')
print(demo)
Fixture实行后置
基于Fixture来实现teardown相关操作:
- 一定是基于Fixture来实现teardown
- 总计两种不同的teardown实现效果:
-
基于关键字yield来实现
yield是生成器,既可以return数据,也可以让函数挂起,暂停运行。从而满足到teardown的特殊要求。核心就是通过yield的挂起机制。来满足需求。当setup完成执行后,通过yield来实现挂起。在用例执行结束后,再调用yield后续的内容。从而满足到teardown的需要。yield本身的实现是依托于python程序的运行机制。所以当Fixture报错的时候,yield后的代码不会继续执行,从而可能会产生不可预见的风险。如果说前置内容非常简单的情况下,则yield非常好用。
-
基于finalizer()来实现(推荐)
能够解决yield如果出现报错则无法继续执行的风险。
teardown的代码块一定要在setup之前。因为代码运行是自上而下的。所以先注册teardown,才能够确保即便setup出错,也能够正常执行teardown。
finalizer示例:
def demo(request): # request不可修改 # 定义teardown内容 def demo_finalizer(): # 建议名称在原有函数名后方添加_finalizer(),表明是teardown teardown代码块 request.addfinalizer(demo_finalizer) # 函数名称不要加括号,让pytest将其识别为teardown # setup内容 setup代码块 return 需要的数据
-
import pytest
# 通过yield挂起实现用例级别的前置与后置,如果fixture报错,则后置有可能不会执行
@pytest.fixture
def demo():
print('这是前置的执行内容') # 这是前置
#1 / 0
yield 12345566
print('这是后置操作') # 这是后置
def test_func(demo):
print("这是测试用例")
def test_func2(demo):
print("这是测试用例2")
# 基于finalizer()来实现
@pytest.fixture()
def demo_fina(request):
# 定义teardown的内容
def demo_fina_finalizer():
print('这是teardown')
request.addfinalizer(demo_fina_finalizer) # 注册teardown
# 定义前置内容
print('这是前置')
12 / 0
return 123 # 不需要yield来实现数据返回了。
def test_func02(demo_fina):
print('这是测试用例2')
print(demo_fina)
conftest.py文件的使用
conftest.py文件的主要作用:
- 管理所有的hook函数。实现pytest的功能增强及二次修改。
- 用于管理整个测试过程中的所有Fixture
- 可以让不同的文件实现对所有Fixture的正常调用。不用担心Fixture无法跨文件访问的作用域问题。
- conftest的作用域默认是当前路径下的所有测试文件以及子路径下的所有文件及文件夹。一般推荐将其放在测试用例根路径下
- session级别的Fixture都必须要放在conftest之中。
- autouse的Fixture也需要放在conftest之中。
- 当测试用例运行不同作用域(scope)的fixture时,作用域范围更大的fixture会优先执行。
import pytest
@pytest.fixture
def lzh(request):
def lzh_finazlier():
print("这是后置函数")
# 注册到后置函数
request.addfinalizer(lzh_finazlier)
print("this is setup fixture")
# session级别的前后置操作行为
@pytest.fixture(scope='session')
def session_lzh():
print('这是session级别的Fixture')
yield 'lzh'
print('session的teardown')
# autouse的Fixture定义
@pytest.fixture(autouse=True)
def lzh_auto():
print('这是auto use hcc')
pytest的ini文件
pytest.ini文件名称固定,不允许修改。专门用于配置pytest的各项设置的一个配置文件。如果不定义,则默认遵循pytest规则
整个工程只需要一个pytest.ini文件。放在工程的根路径下
[pytest]
# mark标签的定义。解决控制台的警告问题
markers =
login: 这是login标签,用于当前系统的常规登录操作
hcc: 这是hcc标签,用于实现当前系统业务的hcc主流程执行
# 设置pytest的默认执行指令。将pytest中每一个测试用例都需要运行的指令进行默认定义,
# 确保每次启动时,都会执行这些指令,如果有特殊指令,则继续往需要执行的文件或者命令行添加即可
addopts = -s -v
# 修改用例和文件的读取命名规则。以及修改用例的读取路径
# 定义用例的读取路径
testpaths = ./
# 测试用例的文件读取规则
python_files = test_*.py
# 测试用例的类读取规则
python_classes = Test*
# 测试用例的读取规则
python_functions = test_*
# 日志配置:控制台实时日志显示和文件日志写入
# 控制台实时日志
log_cli = true
log_cli_level = info
log_cli_format = %(levelname)s %(asctime)s %(filename)s %(module)s %(funcName)s %(lineno)s : %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
# 日志文件的写入与保存:避免日志等级为debug,导致不必要的多余日志信息产生。
log_file = ./log/test.log
log_file_level = info
log_file_format = %(levelname)s %(asctime)s %(filename)s %(module)s %(funcName)s %(lineno)s : %(message)s
log_file_date_format = %Y-%m-%d %H:%M:%S
在测试用例py文件中引用logger在能使ini文件中的日志配置生效
import logging
import pytest
logger=logging.getLogger(__name__)
@pytest.mark.login
def test_lzh(lzh,session_lzh):
print("这是测试用例")
logger.debug("这是debug信息")
logger.info("这是info信息")
logger.warning("这是warning信息")
if __name__ == '__main__':
test_lzh('lzh')
skip装饰器
skip装饰器的应用:将不需要执行的测试用例通过跳过的设置来定义@pytest.mark.skip
@pytest.mark.skip(str='无条件跳过原因'): 无条件跳过@pytest.mark.skipif(表达式,reason=str): 当表达式为True时,跳过用例,为False时,执行用例- skip会先通过表达式判断哪些用例执行,即使后续表达式有修改也不影响
# 比对条件之前,pytest会先获取所有用例是否执行,a默认为0,即便后续有修改,但依旧会进行运行。
@pytest.mark.skipif(a == 0, reason='这是skipif,条件为False')
def test_func04():
print('这是三条基本的测试用例')
- 自定义路过skip设置
# 自定义跳过设置,版本小于3.10跳过
hccskip = pytest.mark.skipif(sys.version_info < (3, 10), reason='python版本过低,跳过')
# 对于特定环境的测试用例需要考虑自定义跳过装饰器。
@hccskip
def test_func05():
print('只有3.10以上python才可以运行')
测试报告
- 使用命令生成测试报告
pytest -sv test.py --html=/路径/测试报告.html - 此报告默认不会展示任何的报错信息。如果想要将报错内容完整展示,则需要额外添加指令
--capture = sys - 正常报告会有css文件,如果不需要css文件或者如果想要此报告作为邮件的附件进行发送。可以直接通过指令:
--self-contained-html
Allure测试报告
使用指令--alluredir=测试数据保存路径。将pytest运行时产生的数据整理为allure可识别的数据。例如--alluredir=./allure_data。
基于数据生成最终的allure测试报告
-
生成临时的测试报告:
allure serve json数据的保存路径,例如allure server .\allure_data\生成一个后台服务,只有访问这个端口才能访问测试报告
-
保存测试报告到指定目录:
allure generate json数据保存路径 -o allure测试报告路径,例如allure generate .\allure_data\ -o ./allure_report。如果新生成的测试报告工程要覆盖旧的工程,则记得在指令末尾添加--clean。 -
调用os下的命令行指令。来执行allure测试报告生成
os.system('allure generate ./allure_data/ -o ./allure_report_new/ v--clean'),system是启动本地的cmd
import logging
import os
from time import sleep
import pytest
logger = logging.getLogger(__name__)
def test_01():
print('这是01')
logger.info('这是logger')
def test_02():
assert 1 == 2
def test_03():
1 / 0
if __name__ == '__main__': # 如果指令无法正常生成测试报告,可以考虑添加sleep等待两秒
# 基于pytest生产测试结果数据。也就是allure_data
pytest.main(['-sv', './myallure.py' '--alluredir=./allure_data/'])
sleep(3)
# 调用os下的命令行指令。来执行allure测试报告生成
# system是启动本地的cmd
os.system('allure generate ./allure_data/ -o ./allure_report_new/ v--clean')
Allure装饰器
Allure常用装饰器:
- 所有的装饰器其实和ChromeOptions配置项类似。没有任何所谓的逻辑可言。都属于对测试报告进行的一些配置和设置项。
- 常用的装饰器:都是基于@allure来实现的。
epic装饰器: 主要用于定义到当前测试执行的描述。一般用于做测试业务流程的说明feature装饰器:主要用于定义到测试用例的执行的功能模块的说明。story装饰器:相当于定义测试流程分支的描述title装饰器: 用于定义测试用例的标题description装饰器: 用于描述测试用例- 基于装饰器形态实现的描述,个人建议此方式进行描述
- 基于三对单引号实现的描述,就是多行注释
- 当两种方式一起使用时,装饰器用于做用例的allure描述,而三引号则是对代码进行多行注释
step装饰器: 实现对测试用例步骤的描述- 基于装饰器形态来实现步骤的定义。一般是整体的步骤描述
- 基于
with allure.step()来实现。个人更为推荐
link装饰器:给用例添加外部的链接资源
- 装饰器在实际调用时,如果装饰器中的值相同,则会识别为同一个类别,在报告中作为同级输出。一般我们每一个测试用例,根据不同的业务,他们的epic会保持一致。而其他内容则会有不同
allure.attach():- 主要用于在allure测试报告中添加附件。搭配到driver的截图功能,可以实现当出现错误的时候,allure在测试报告对应的位置实现截图的上传和展示。注意,此方法不是装饰器
- 所有的装饰器在实际应用的过程中只是为了给allure测试报告进行服务的,不会影响你的代码逻辑运行。所以,原有的用例该怎么写还是怎么写,要调用什么就正常调用即可。
- 通过
allure.severity()方法进行用例的优先级定义。也是装饰器的形态allure对测试用例的优先级定义和管理。默认等级是normal级别blocker(阻塞,功能未实现,无法继续后续的流程操作)critical(严重)normal(一般)minor(次要)trivial(轻微)
dynamic动态实现对测试报告数据的修改操作,可以二次修改原定的测试报告相关内容。帮助我们快速查看到相关问题。allure.dynamic.*(修改后的内容)
登录erp的allure测试报告
@allure.severity('critical') # 优先级的第一种写法
# @allure.severity(allure.severity_level.TRIVIAL)
@allure.epic('执行erp系统的新增供应商业务流程自动化测试')
@allure.feature('登录功能模块')
@allure.story('执行erp的登录操作')
@allure.title('登录用例')
@allure.description('基于jsh账号,实现对erp系统的登录操作,'
'\n结合ddddocr实现对验证码的自动识别校验。从而解决简单验证码的登录操作'
'\n业务流程')
@allure.link('http://www.baidu.com')
@allure.step('基于{data}账号,关联ddddocr实现登录的操作行为') # {data}可以将测试用例中的data数据传入装饰器之中。实现动态展示
@pytest.mark.parametrize('data', yaml.safe_load(open('./user.yaml', 'r', encoding='utf-8')))
def test_01(data): # 测试用例
'''
如果用例在执行时出现报错,则进行截图,并抛出对应的错误
'''
try:
with allure.step('1. 浏览器初始化'):
service = Service('../chromedriver.exe')
driver = webdriver.Chrome(service=service)
driver.implicitly_wait(10)
with allure.step('2. 访问erp系统登录页'):
driver.get('http://39.101.122.147:3000/user/login')
with allure.step('3. 实现账号密码的输入'):
driver.find_element('id', 'loginName').send_keys(data['user'])
driver.find_element('id', 'password').send_keys(data['pwd'])
with allure.step('4. 实现ddddocr的验证码识别操作'):
file = driver.find_element('xpath', '//img[@data-v-4f5798c5]').screenshot_as_png # 获取验证码图片
driver.find_element('id', 'inputCode').send_keys(
ddddocr.DdddOcr(show_ad=False).classification(file)) # 将验证码图片内容解析成文字信息
with allure.step('5. 点击登录按钮,实现登录操作'):
1 / 0 # 人为制造异常
driver.find_element('xpath', '//button[@data-v-4f5798c5]').click()
except:
with allure.step('用例执行失败,错误信息如下'):
allure.attach(driver.get_screenshot_as_png(), '错误截图', allure.attachment_type.PNG)
sleep(5)
# 动态修改制定装饰器原有值的操作行为
allure.dynamic.title(f'登录失败,本次登录用户数据为:{data}')
raise # 针对已生成的异常进行二次抛出,确保测试用例依旧保持失败的状态
Request模块
requests库的接口测试:
- 准备测试数据
- 模拟请求的下发
- 接收响应结果并解析校验
res = requests.post(url=url, json=data)res.text输出响应的文本信息,返回的类型是strres.content响应文本源信息。res.status_code响应状态码res.json()将str转为dict类型的数据,想要获取响应结果中的特定key对应的value。需要做数据的二次处理res.request.*基于request来获取到请求的所有信息,包括方法,头,体都可以正常获取res.request.headers获取响应头
request中的参数
param:将数据附加到 URL 的末尾,形成?key=value&...的形式,相当于 GET 请求的参数data:发送 表单格式数据,内容类型通常是:Content-Type: application/x-www-form-urlencodedjson:将一个 Python 对象(字典、列表等)序列化为 JSON 字符串,并作为请求体发送,同时自动设置:Content-Type: application/json- 自动调用
json.dumps()。 - 自动设置正确的
Content-Type。 - 是现代 Web API(RESTful API)最常用的方式。
- 自动调用
# 准备测试数据
url = 'http://154.12.20.250:5000/user_services/login' # 接口url
# 账号密码: 必须与接口文档保持一致
data = {
"password": "password123",
"username": "user1"
} # 在Python中,字典是可以非常方便切换为json格式的。所以在实际测试中,一般都用dict来做数据的准备
# 模拟请求的下发:基于requests库来实现,json参数会自动将请求的数据转为json格式进行传输。
res = requests.post(url=url, json=data) # 默认情况下返回的响应对象会包含状态码
# res = requests.post(url=url, data=data) # 默认情况下返回的响应对象会包含状态码
# 定义请求头
headers = {
'Authorization': res.json()['token']
'Authorization': "ajsdhajkdshaskjdhsla"
}
# 接口的断言:基于关键字assert来实现
# assert 200 == res2.status_code # 响应状态码断言,只能作为辅助,不能作为核心
# 主要推荐的断言方式
assert res.json()['token'] # 断言是否包含有特定字段
assert '用户' in res.json()['nickname'] # 断言是否在指定key中包含有特定内容
assert '用户PzimMjJyZg' == res.json()['nickname'] # 断言值是否与指定key的值完全一致
Json和JsonPath
基于json库实现对json格式的数据进行各类操作
json本身就是str类型。所以json库本质上就是操作str。只是数据格式比较特殊而已。
json库只有四个不同的方法:
- dumps:将数据内容转为json格式。相当于将Python中的普通数据类型,转变为str的json格式内容.默认不支持中文处理。需要将
ensure_ascii参数修改为False即可。 - dump:与dumps类似。但是数据内容是写入到json文件之中。并进行保存。
- loads:将json格式的数据,转为Python可以识别的数据类型。与dumps相反。满足程序对数据进行处理的需要
- load:与loads类似,但是是将json文件的内容读取出来,作为Python可以识别的数据类型
如果单纯处理数据,则使用dumps和loads。如果要操作文件,则使用dump和load即可在实际测试过程中,基于获取的数据内容,根据实际情况自行选择dumps或者loads来对数据进行二次操作。因为核心诉求就是满足程序对数据的处理需要。这也是接口通信之中非常常见的二次数据处理行为。
res.json()其实本质上也就是json.loads()的实现。
jsonpath:
表达式:
$..name:$表示根路径,..表示不考虑查找的位置name表示指定的key。从根路径下开始查找所有的key,直到所有key为name的value全部获取为止。- 返回数据:
- 如果获取到数据,因为考虑到会有不同层级的重复key存在,如果获取成功,则默认返回为list数据类型。
- 如果没有对应的key存在,返回False
- 因为jsonpath主要用于解析响应结果。所以一般在自动化中,会应用jsonpath进行二次封装。实现对响应结果指定内容的提取操作
def get_text(res, key):
# jsonpath会获取所有的value,返回为list
values = jsonpath.jsonpath(res, f'$..{key}') # 拼接字符串,实现表达式的完整性
# jsonpath获取之后的二次处理
if values:
if len(values) == 1: # 如果长度为1,则直接返回对应内容。
return values[0]
# else:
# # return values[num] # 如果有多个值,则默认返回全部,或者指定的下标元素
# return values # 默认返回全部内容
# else:
# return values
return values # 因为除了单个元素之外,都是返回整个values,所以直接外部定义。
接口自动化
使用unittest
- api_keys.py
'''
关键字驱动类的实现:
1. 实现对常用请求的方法封装
2. 解决在模拟请求的时候的所有内容
3. 解决断言校验的数据处理,包括数据关联的提取能力。
封装的设计逻辑:
1. 对于可有可无的参数,我们优先通过设定默认值为None来确保逻辑的顺利执行
2. 所有的请求其实都是通过request()方法来实现执行的。
'''
import jsonpath
import requests
from requests import request
from myinterface.conf.set_conf import read_conf
class ApiKeys:
# 构造方法:初始化测试环境数据,实现一键切换测试环境。
def __init__(self, env):
self.env = env
# get请求
# def do_get(self, url, params=None, headers=None, **kwargs):
# return requests.get(url=url, params=params, headers=headers, **kwargs)
#
# # post请求
# def do_post(self, url, data=None, json=None, headers=None, **kwargs):
# return requests.post(url=url, data=data, json=json, headers=headers, **kwargs)
# 基于request统一封装的模拟请求。
def request(self, method, path=None, headers=None, **kwargs):
'''
:param method: 请求方法的定义
:param url: 在不同环境下,url的IP和端口是会有改变,但是接口路径是不会发生变化的。
基于此特性,我们可以拆解url,进行相关的配置读取操作。从而满足不同环境下的接口测试
:param headers: 需要新增的请求头参数,基于set_headers方法实现对请求时,请求头信息
的补全操作
:param kwargs: 其他请求参数的定义
:return: 返回response 对象。
'''
url = self.set_url(path)
headers = self.set_headers(headers)
return request(method=method, url=url, headers=headers, **kwargs)
# url的拼接处理
def set_url(self, path):
url = read_conf(self.env, 'host')
if path:
url = url + path
return url
# 拼接headers,实现对实时新增的数据进行添加
def set_headers(self, headers):
# 定义默认请求头
base_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/135.0.0.0 Safari/537.36' # 定义请求的浏览器信息
}
if headers:
base_headers.update(headers) # 基于headers字典数据添加至base_headers之中。
# 如果运行过程中,token值已被成功写入,则每次请求都默认拼接token值
if read_conf('data', 'token'):
token = read_conf('data', 'token')
base_headers['Authorization'] = token
return base_headers
# jsonpath的封装,获取指定的数据
def get_text(self, res, key):
# jsonpath会获取所有的value,返回为list
values = jsonpath.jsonpath(res, f'$..{key}') # 拼接字符串,实现表达式的完整性
# jsonpath获取之后的二次处理
if values:
if len(values) == 1: # 如果长度为1,则直接返回对应内容。
return values[0]
return values # 因为除了单个元素之外,都是返回整个values,所以直接外部定义。
# 断言校验
def assert_text(self, expected, res, key):
reality = self.get_text(res, key)
assert expected == reality, f'''
预期结果为:{expected}
实际结果为:{reality}
断言结果:{expected} != {reality}
'''
- set_conf.py
'''
实现对ini文件内容的相关操作行为
'''
import configparser
import pathlib
# 获取配置文件的绝对路径
file = pathlib.Path(__file__).parents[0].resolve() / 'server.ini'
conf = configparser.ConfigParser()
# 读取ini文件
def read_conf(section, option):
conf.read(file)
values = conf.get(section=section, option=option)
return values
# 写入ini文件
def write_conf(section, option, value):
conf.read(file)
# 将指定的内容写入到conf文件之中。
conf.set(section, option, value)
with open(file, 'w') as f:
conf.write(f)
- server.ini
[Test_Env]
host = http://127.0.0.1:5000/
[Dev_Env]
host = http://www.baidu.com
[data]
token =
user_id =
- test.py
'''
接口自动化测试用例执行(UnitTest版本)
1. 在接口自动化中,最关键的一定是用例的设计。虽然没有多少技术含量。
2. 接口自动化,最核心技术点一定是接口关联:
1. 全局变量方式来解决数据关联问题
非常简单的一种解决手段,但是如果关联数据过多,则不推荐使用。
一般在三五个数据以内,推荐使用此方法。
2. 写入文件再读取文件来解决数据关联问题
实现过程相对比较复杂,因为涉及到文件的读写。但是对于复杂数据相对处理会更方便
'''
import unittest
from ddt import file_data, ddt
from myinterface.api_keys.api_keys import ApiKeys
@ddt
class TestBlackCore(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.api = ApiKeys('Test_Env')
cls.token = None # 定义关联数据全局变量
@file_data('../test_data/login.yaml')
def test_01_login(self, **kwargs):
res = self.api.request(**kwargs['login'])
# self.api.request(method='post',path='',json={})
print(res.json())
# UnitTest下,全局变量的赋值,必须要以类名.变量名才可以赋值。无法使用self.变量名赋值
TestBlackCore.token = self.api.get_text(res.json(), 'token')
# self赋值,只对当前用例有效,无法对全局生效。
# self.token = self.api.get_text(res.json(), 'token')
# print(self.token)
@file_data('../test_data/login.yaml')
def test_02_userInfo(self, **kwargs):
kwargs['userInfo']['headers']['Authorization'] = self.token
# print(kwargs['userInfo'])
res = self.api.request(**kwargs['userInfo'])
print(res.json())
# def test_03(self):
# print(self.token)
if __name__ == '__main__':
unittest.main()
Flask框架
'''
flask基本应用:
1. 基于flask所实现的后端接口研发
2. flask的基本语法应用
'''
import pymysql
from flask import Flask, request, jsonify
from myinterfaceplus import read_conf
# flask示例
# 部署flask的后端服务。相当于部署一个web server。提供用户可以直接访问和请求的能力
app = Flask(__name__) # 固定写法。相当于部署当前的py文件为flask服务。
# 默认flask不支持中文的显示,想要支持,需要修改flask的设置项
#app.json.ensure_ascii = False
# 基于flask实现的接口逻辑
# get请求定义
@app.route('/demo', methods=['GET']) # 定义接口路由。确定接口的请求方法以及接口的路径
def demo(): # 定义接口的内容
name = request.args.get('name') # 用于接收请求中传入的参数内容,只接受参数名为name的参数
# 响应结果
data = {
'name': name,
'age': 18,
'message': '获取用户信息成功',
'code': 200
}
return jsonify(data) # 响应结果以json格式传递
# Post请求实现:Login登录操作
@app.route('/login', methods=['POST'])
def login(): # 定义接口的业务逻辑
username = request.form.get('username')
password = request.form.get('password')
# 正常的接口逻辑实现,是基于底层代码的调用来完成,而不是在接口中写入逻辑代码。
# xxx.login(username,password) # 实现对数据库内容的访问,确认账号密码是否成功
# 演示过程中,会在接口中直接写入底层逻辑运行模式。但实际接口不会写。是基于底层代码的封装实现的。
# 连接数据库
conn = pymysql.connect(**read_conf.read('TEST_ENV'))
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
# sql处理逻辑
try:
sql = 'select * from user_info where username=%s and password=%s'
# 执行sql语句
cursor.execute(sql, (username, password))
result = cursor.fetchall()
# 数据结果唯一,则登录成功,否则登录失败
if len(result) != 1:
raise # 数据不唯一,抛出异常
# 数据唯一,定义响应结果
data = {
'code': 200,
'message': '登录成功!',
'token': result[0]['token']
}
return jsonify(data)
except:
return jsonify(message='登录失败,请检查你的账号或密码')
finally: # 释放资源
cursor.close()
conn.close()
# mock示例:其实就是自定义接口。或者专门用于测试服务的接口
@app.route('/mock_info', methods=['GET']) # mock接口建议在名称前添加mock字样,用于区分mock与正常接口
def mock_info():
'''
mock获取用户信息接口。在mock中必须与实际的接口保持一致。不管是参数的数量、名称、数据类型还是响应结果
内容,所有一切都需要与正常的接口保持完全相同。
例如:
常规info接口,需要用户token放在请求头中,作为token字段的参数值
那么mock_info接口,则需要与info接口保持一致。
对于mock接口的返回数据,一定要与常规接口格式和内容保持完全一致。但是常规接口的中间处理过程,
在mock中可以直接省略。只需要定义请求与响应即可。其余内容能省略的都可以直接省略。
因为mock本身就是临时存在用于调试业务流程,确保测试工作顺利执行的一种手段而已。
mock只能作为测试用接口。无法替代实际的接口。
当mock测试使用完以后,还是要回归到系统的正常环境进行接口的调用。确保接口的质量
'''
token = request.headers.get('token')
if token == True:
return jsonify('请求成功,这是用户数据') # 正确情况,依照接口文档返回正确结果
else:
return jsonify('失败') # 错误情况,依照接口文档返回错误结果
# 启动flask
if __name__ == '__main__':
'''
host:指定IP地址,如果不指定,默认为127.0.0.1
port:指定端口,如果不指定,则默认为5000
debug:调试模式,默认不启动。如果启动,则代码发生修改,服务会自行重启。
'''
app.run(host='127.0.0.1', port=5000) # 代码稳定时,需要关闭debug
数据加密与解密
很多系统为了考虑到数据的传输安全性,所以在数据传递之前会进行数据的加密手段,用于确保即便数据在被非法获取时,也不会明文显示在获取者之处。一般加密都是前端的手段。基于数据传输之前,对需要加密的数据进行加密,然后再将加密数据作为请求数据进行发送。
目前市场加密手段主要分为两种:
-
单向加密:只可加密,不可解密
- 最常见的就是md5加密手段。加密规则固定。此类加密手段的破解只能基于撞库的操作行为来进行破译。就是准备一个密码字典。包含有所有常见的密码明文与加密后的md5字符串,循环遍历进行匹配对应。如果对应到字典中的md5值,则返回对应的明文值。
-
公钥与私钥:基于客户端私钥进行加密,基于服务端公钥进行解密
AES加密手段最常见。
- 前端基于aes的私钥进行加密算法的调用,生成加密数据
- 后端基于aes的公钥进行解密,从而获取原本的数据内容。
md5加密
import hashlib
def md5(data):
md = hashlib.md5()# md5 加密演示
md.update(data.encode('utf-8'))# 对指定的数据内容进行md5加密
return md.hexdigest()# 输出加密后的密码数据
Faker库
Faker库的应用:非常实用的数据生成库。
在实际测试过程中,我们需要做大批量数据筹备时,可以非常方便地生成你想要的数据内容。
生成的数据是固定的,我们可以结合我们自己的实际业务做对应的二次计算。数据生成的过程是会关联到很多方面的信息,所以在实际实现时,一般会关联faker、random等一系列的各种库,来实现你的数据组的完整生成。不要单独只关注到faker一个库。
import random
from faker import Faker
from faker.providers import BaseProvider
# Faker示例
faker = Faker('zh_CN') # 创建中文faker对象
print(faker.profile()) # 生成随机的个人档案
print(faker.name()) # 随机姓名
print(faker.address()) # 地址
print(faker.email()) # 邮箱
print(faker.date()) # 年月日
print(faker.ssn()) # 身份证
print(faker.date_time()) # 时分秒
print(faker.phone_number()) # 手机号码
print(faker.random_int(min=18, max=70))
Faker自定义数据内容生成
class HccProvider(BaseProvider): # 继承BaseProvider实现数据的自定义生成
def sku(self): # 定义数据产生的格式与内容
part1 = ''.join(random.choices('ABCDEFG', k=2))
part2 = ''.join(random.choices('1234567890', k=4))
return f'{part1}---{part2}'
# 获取自定义数据内容
faker = Faker()
faker.add_provider(HccProvider) # 自定义数据生成加入到Faker之中
print(faker.sku()) # 生成自定义数据格式数据
# 基于业务需求,生成数据组格式
def person_profile(): # 封装数据格式组
profile = {
'name': faker.name(),
'age': faker.random_int(min=18, max=50),
'email': faker.email(),
'ssn': faker.ssn()
}
return profile
for i in range(1,10):
print(person_profile())
Git
- 先在gitee或者github添加仓库
- 使用
git clont 仓库地址将远程仓库克隆到本地目录 - 使用
git add filename将要提交的代码放到缓存区- filename是要提交的文件,使用
.表示当前目录下所有文件
- filename是要提交的文件,使用
- 使用
git commit -m "说明"将要提交的文件提交 - 使用
git push将commit的代码推送到仓库
- pull:将仓库代码拉取到本地
- push:将本地代码提推送仓库
- clone:实现对远程仓库中的代码拉取到本地的行为
Jenkins
- 下载 jenkins.war(确保是 LTS 或 Weekly 最新版)
wget https://get.jenkins.io/war-stable/latest/jenkins.war - 直接运行(使用 Java 11+)
java -jar jenkins.war --httpPort=8080 - 访问 http://localhost:8080