如何提升WebUI自动化的稳定性
一、导致UI自动化不稳定的因素
1.web页面的多变
在自动化的实际应用场景中,经常性会碰到一类问题,UI界面经常性的变动,导致页面元素对象的维护成本大大增加。
2.页面隐藏元素
受前端影响,页面元素的定位在采用相对定位时可能会定位到隐藏元素,导致脚本的不稳定性
3.页面元素加载不稳定
受机器本身及网络状态影响,UI界面的响应时间存在不稳定
4.系统业务复杂
相比繁杂的业务场景,重复简单的业务参加肯定更适于参与自动化测试。随着业务复杂度的提升,脚本的复杂程度也随之提升,中间的业务逻辑更容易出现问题。
5.AJAX请求问题
ajax请求会导致三种情况的问题:元素没有加载出来,不在DOM结构里面;元素不可见,在DOM结构里面;元素可见,可点击,但是页面上其他位置还没完全加载出来,点击这个元素之后一点反应都没有,下一步操作就会报错。
6.环境测试数据准备问题
自动化测试其实就是将功能操作通过脚本自动执行起来,同样会涉及测试数据清理,测试数据准备的问题
二、解决问题的各种方法
1.从自动化框架解决问题
1.1 设置显示等待
重写selenium的底层接口,加入显示等待,设置等待条件及异常处理
WebDriverWait(driver,timeout,poll_frequency=0.3,ignored_exceptions=None)
driver:浏览器驱动
timeout:最长超过时间,默认以秒为单位
poll_frequency:监测的时间间隔,默认为0.3秒
ignored_exceptions:超时后的异常信息,默认情况下抛NoSuchElementException异常
1.2 等待页面加载稳定
重写selenium的底层接口,定位元素前需要先等待页面加载稳定,页面加载稳定的判断条件为”页面源码在限定时间内不在发生变化”
begin = int(time.time())
source = driver.page_source
while True:
time.sleep(0.3)
nowsource = driver.page_source
if source == nowsource:
break
else:
now = int(time.time())
if now - begin > 3:
break
source = nowsource
try:
WebDriverWait(self.driver, outtime).until(
lambda driver: driver.execute_script('return document.readyState') == 'complete')
except exceptions.TimeoutException:
pass
1.3 过滤隐藏元素
重写selenium的底层接口,在selenium判断的基础上,过滤掉页面特殊的隐藏元素,增强脚本容错性
try:
if not element.is_displayed():
return False
size = element.size
if not int(size['height']) or not int(size['width']):
return False
loc = element.location
if int(loc['x']) < -9000 or int(loc['y']) < -9000:
return False
if not element.is_enabled():
return False
except:
return False
return True
1.4 设置xhr请求等待
重写selenium的底层接口,在脚本开启代理的情况下,判断xhr请求是否都已经响应,此处可作为选配仅在脚本开启代理时作判断
def getxhrcount():
r = requests.get(f'http://{ query_site}/xhr')
if r.status_code == 200:
return float(r.text)
else:
raise ConnectionError(f"访问链接 http://{ query_site}/xhr 出错")
def getxhrtime():
r = requests.get(f'http://{ query_site}/xhrtime')
if r.status_code == 200:
return float(r.text)
else:
raise ConnectionError(f"访问链接 http://{ query_site}/xhrtime 出错")
def gethttpcount():
r = requests.get(f'http://{ query_site}/http')
if r.status_code == 200:
return float(r.text)
else:
raise ConnectionError(f"访问链接 http://{ query_site}/http 出错")
def gethttptime():
r = requests.get(f'http://{ query_site}/httptime')
if r.status_code == 200:
return float(r.text)
else:
raise ConnectionError(f"访问链接 http://{ query_site}/httptime 出错")
start = time.time()
while True:
xhrnum = getxhrcount()
oldtime = getxhrtime()
# xhr 请求有可能是一个接一个发送,所以需要隔一段时间再判断
time.sleep(0.2)
newxhrnum = getxhrcount()
newtime = getxhrtime()
if xhrnum == 0 and newxhrnum == 0 and oldtime == newtime:
if logger:
logger.debug(f'xhr请求最后的响应时间戳为:{ newtime}')
return True
else:
if time.time() - start > outtime:
if logger:
logger.warning(f'等待xhr请求响应超时,超时时间为{ outtime}s')
return False
time.sleep(0.5)
1.5 集成requests模块
传统的WebUI框架结构是测试报告模块+日志模块+项目配置文件模块+unittest模块+selenium模块,可以在此基础上集成requests模块。requests模块提供接口请求、获取响应内容、解析响应内容的基本封装。
class Session(object):
'''Session类'''
def __init__(self):
self.session = None
header={ 'User-Agent': real_confing.get('REQUEST','User-Agent'),
'Accept': real_confing.get('REQUEST','Accept'),
'Accept-Encoding': real_confing.get('REQUEST','Accept-Encoding'),
'Accept-Language': real_confing.get('REQUEST','Accept-Language'),
'Connection': real_confing.get('REQUEST','Connection')
}
self.session = requests.Session()
self.session.headers.update(header)
def get(self,url,params):
return self.session.get(url=url,params=params)
def post(self,url,params,data=None,json=None):
return self.session.post(url=url,data=data,json=json,params=params)
class page():
''' 页面对象的基类,里面封装了一些常用的方法 '''
def __init__(self,session: Session = None):
self.session = session
def request_http(self,params,type='post'):
apiinfo = params['apiinfo']
del params['apiinfo']
del params['self']
try:
url = params['url']
except:
url = None
del params['url']
if type=='post':
if url:
session = self.post(url=url + apiinfo, params=params)
else:
session = self.post(url=real_confing.get('TEST', 'url') + apiinfo, params=params)
elif type=='get':
if url:
session = self.get(url=url + apiinfo, params=params)
else:
session = self.get(url=real_confing.get('TEST', 'url') + apiinfo, params=params)
else:
raise EOFError('请求类型错误')
return session
def extractor_json(self,resource,expressioins,num:int=1,default=None):
''' JSON提取器,对传入的json数据通过json表达式提取出目标数据 :param resource: 源数据 :param jsonpath: jsonpath表达式 :param num: 返回参数,0代表随机,-1代表所有,正整数代表第几个 :param default: 默认值 '''
if type(resource)==str:
resource = json.loads(resource)
res = jsonpath.jsonpath(resource,expressioins)
if res:
if num==-1:
return res
elif num==0:
return res[random.randint(0,len(res)-1)]
elif num>0:
return res[num-1]
else:
if default:
return default
def extractor_regular(self,resource,expressions,num:int=-1,flags=0,default=None):
''' 正则提前器,对传入的数据通过正则表达式提取出目标数据 :param resource: 源数据 :param expressions: 正则表达式 :param num: 返回参数,0代表随机,-1代表所有,正整数代表第几个 :param default: 默认值 '''
res = re.findall(expressions,resource,flags=flags)
if res:
if num==-1:
return res
elif num==0:
return res[random.randint(0,len(res)-1)]
elif num>0:
return res[num-1]
else:
if default:
return default
2.从设计模式解决问题
2.1 采用POM设计模式
POM(Page Object Model)是一种设计思想,将被测系统中的单个页面当作一个对象,页面的元素和元素直接的操作方法就是页面对象的属性和行为,基本采用用类的方法来组织我们的页面。主要优势在于:
a.页面元素在发生变化时,我们仅需从底层修改对应页面对象,大大降低了后期的维护成本;
b.将复杂的业务逻辑划分成多个细小单元,通过组织这些细小单元形成我们的业务代码,增强代码的复用性,提高效率
c.统一代码风格,减少个人不良的代码习惯对脚本稳定性的影响
2.2 通过requests模块完成环境数据准备
根据接口信息,设计接口对象库,再根据接口对象库封装接口操作,通过调用接口操作完成环境测试测试数据的清理与准备。如此设计相比传统通过UI界面准备数据优势:
a.提高脚本执行效率
b.减少脚本在非用例验证点的地方提前报错,提升脚本容错率
c.增强脚本可操性,便捷获取业务系统的一些关键数据,减少UI操作
class api_resources(page):
def new_edata(self,apiinfo='/edatasubject/project.do',action=None,data=None,url=None):
''' :param apiinfo: 接口地址* :param action: save* :param data: {"caption":"标题","ds":"连接池名称","desc":"描述"} '''
params = dict(locals())
self.request_http(params)