介绍背景
情况是这样的,某段时间之前,开发就想找我用ui自动化帮他们实现一个功能:在系统某些时候生成报告的时候会fail,
但是又不再重新生成,需要人工edit再次submit才能生成,原因png是由当前html页面生成。但是作为测试的我有一个疑惑?
开始不是常用定时任务或是失败重试吗?怎么不这样做呢?或者有其他办法使之成功呢?然后开发自己优化了一下,就默默的成功了,
这事儿就算过去了,不曾想几天前又复活了,需要我来协助,然后问,这玩意儿不能使用接口去完成吗?开发解释:不能,why?
原因是html转png:前端拿到接口响应数据,动态绘制html,然后在生成png。问题来了:为什么会失败?什么情况下会失败呢?
开发与实现
作者的python开发环境那是有好几套,接口、ui自动化的环境那是现成的,拿来即用,这里用来演示步骤,就不截图了。
部署python本地开发环境
# 安装工具库
pip install selenium
开始码代码
又因为作者是有比较系统的ui自动化测试思想,首先是po模式,但是这个需求是一次性的,所以并不想把它复杂化<相对线性脚本>,本着开发效率出发<不曾想也花了一天时间>
#!/bin/python3
# -*-coding: utf-8 -*-
import json
import logging
from time import sleep
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
# 日志系统
log_format = '%(asctime)s - %(levelname)s - %(message)s'
logging.basicConfig(filename="scoreSys.log", filemode='a+', format=log_format, level=logging.DEBUG)
options = webdriver.ChromeOptions()
#options.add_argument('--disable-gpu')
#options.add_argument('--headless')
#options.add_argument('--no-sandbox')
#options.add_argument('--disable-dev-shm-usage')
# 初始化页面对象
driver = webdriver.Chrome(executable_path="../chromedriver.exe", options=options)
"""打开浏览器"""
# https://score.1111.com/
logging.info(">>>>>>>>>>>>>>>>>打开口语报告系统<<<<<<<<<<<<<<<<<<<<<<")
# driver.get("https://xxx.xxx.com/home")
driver.get("https://uat.xxx.xxx.com/xxx/home")
driver.maximize_window()
driver.implicitly_wait(10)
# 登录页,页面元素
account_input = (By.XPATH, "//input[@placeholder='Email']")
passwd_input = (By.XPATH, "//input[@placeholder='Password']")
verify_code = (By.XPATH, "//input[@placeholder='Code']")
signIn_btn = (By.XPATH, "//span[text()=' Sign In ']")
# 登录的操作步骤
driver.find_element(*account_input).send_keys("xxx.x@hcp.tech")
driver.find_element(*passwd_input).send_keys("xxxxx")
driver.find_element(*verify_code).send_keys("1234")
driver.find_element(*signIn_btn).click()
logging.info(">>>>>>>>>>>>>>>>>用户登录口语报告系统<<<<<<<<<<<<<<<<<<<<<<")
sleep(1)
# ui登录后获取请求头中token属性
headers = {"content-type":"application/json"}
# 登录系统后用户名元素
account_text = (By.CSS_SELECTOR, ".user_name")
change_passwd = (By.CSS_SELECTOR, ".pwdBtn")
"""检查登录状态"""
account_info = driver.find_element(*account_text).text
logging.info(">>>>>>>>>>>>>>>>>检查用户是否成功登录系统<<<<<<<<<<<<<<<<<<<<<<")
try:
assert account_info == "xxxx", "断言失败"
except:
logging.info(">>>>>>>>>>>>>>>>>用户:{},登录失败!!!<<<<<<<<<<<<<<<<<<<<<<".format(account_info))
else:
logging.info(">>>>>>>>>>>>>>>>>用户:{},登录成功!!!<<<<<<<<<<<<<<<<<<<<<<".format(account_info))
userInfo = json.loads(driver.execute_script('return localStorage.getItem("userInfo");'))
v = json.loads(userInfo.get("v"))
token = v.get("token")
headers["token"] = token
# 搜索条件
select_box = (By.XPATH, "//input[@placeholder='Report status']")
select_input = (By.XPATH, "//span[text()='Failed']")
select_btn = (By.XPATH, "//span[text()='Search']")
"""输入fail点击查询"""
# 查询操作
driver.find_element(*select_box).click()
sleep(1)
driver.find_element(*select_input).click()
driver.find_element(*select_btn).click()
# 列表是否有失败状态的
fail_status = "//td//div[text()='Fail']"
# 找到失败的edit按钮
edit_btn = (By.XPATH, "{}/../parent::td[1]//following-sibling::td//span[text()='Edit']".format(fail_status))
submit_selector = (By.XPATH, "//div[contains(text(),'Submit')]")
"""提交报告"""
# 首先按fail条件查询
click_search()
# 找到更多需要edit的按钮
status_eles = driver.find_elements(*edit_btn)
ele_nums = len(status_eles) # 页面元素找到元素不唯一
# 接口获取fail总数及id列表
nums, fail_li = get_fail_nums()
count = 0 # 记录真实补偿次数
success_li = [] # 记录成功补偿报告id
while ele_nums > 0:
status_eles = driver.find_elements(*edit_btn)
try:
for ele in status_eles:
clicked = ele.is_displayed() # 因为找到元素不唯一,需要判断元素是否显示
if clicked:
ele.click()
driver.find_element(*submit_selector).click() # 提交
success_li.append(driver.current_url.split("=")[-1])
#等待提交之后跳转的页面元素是否出现
WebDriverWait(driver, 50).until(EC.presence_of_element_located(change_passwd))
count += 1
except:
ele_nums -= 1
else:
ele_nums -= 1
finally:
click_search() # 每次需重新查询
driver.quit()
思路分析
- 第一步,开发针对我的特殊帐号去除验证码登录,<毕竟实现验证码登录成本还是有的>
- 首页增加报告查询条件:success or failed;
- 需要发送钉钉通知<这是后面实现>
- 操作流程:登录-检查登录状态-查询fail条件的数据-进入编辑页面-点击submit-再重新查询fail条件-如有继续edit-submit,如初反复直到没有fail的数据为止-关闭浏览器
难点解析
在selenium做ui自动化的时候,最难的不是实现某个功能代码块,而是定位元素表达式,但是页面不全是id、name等唯一元素,更多是需要写css_selector\xpath
- html的结果页是个table,那么在根据fail失败元素同级找到它的edit,这个如下所示;是不是比较懵圈
# 列表是否有失败状态的
fail_status = "//td//div[text()='Fail']"
# 找到失败的edit按钮
edit_btn = (By.XPATH, "{}/../parent::td[1]//following-sibling::td//span[text()='Edit']".format(fail_status))
# 解释下上面的表达式:/.. 表示查找上级,parent::表示父级,following-sibling::平级中的下级
- 其实实现之后并不需要如此,因为是先按fail条件查询,那么剩下就是fail,直接找edit即可。
- 难点是ui+接口的结合;因为上述代码之初,在找到需要重新submit的报告,数目不正确,因为元素重复不唯一,
clicked = ele.is_displayed() # 因为找到元素不唯一,需要判断元素是否显示
- 所以想通过接口来确定真正fail的条数,所以先ui登录,获取token传递给接口请求,
# 这是个新技能点,每次尝试不同的需求,总会遇到不同的问题,然后找到解决方案
userInfo = json.loads(driver.execute_script('return localStorage.getItem("userInfo");')) # 这个userInfo是key,而不是直接叫token
- 从demo中可以看出作者的po思想,将定位元素的标识单独提取出来保存,没直接放在方法里
代码升级
在测试领域中,分层测试并不是说ui不支持接口自动化,意思是某些场景需要与之结合。
加入接口请求
发送钉钉请求是接口 登录系统是接口 查询还是接口
#!/bin/python3
# -*-coding: utf-8 -*-
import json
import logging
from time import sleep
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
# 日志系统
log_format = '%(asctime)s - %(levelname)s - %(message)s'
logging.basicConfig(filename="scoreSys.log", filemode='a+', format=log_format, level=logging.DEBUG)
options = webdriver.ChromeOptions()
options.add_argument('--disable-gpu')
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
# 初始化页面对象
driver = webdriver.Chrome(executable_path="../chromedriver.exe", options=options)
def send_dingding(before_of_after, nums, fail_li, time):
"""处理数据前后发送钉钉业务通知"""
dingDing_robot = "https://oapi.xxxx.com/robot/send?access_token=xxxx"
# dingDing_robot = "https://oapi.xxxx.com/robot/send?access_token=xxxx"
title = "【Teacher Work Platform \n 业务预警提示:{}】".format(time)
if before_of_after == "before":
warning_msg = {"msgtype": "text", "text":{"content":"{}: \n你有{}条口语外教报告id:{}待补偿!!!".format(title, nums, fail_li)}}
requests.post(dingDing_robot, json=warning_msg, headers={"content-type":"application/json"})
elif before_of_after == "after":
warning_msg = {"msgtype": "text", "text":{"content":"{}: \n已成功补偿id:{},{}条口语外教报告!!!".format(title,fail_li, nums)}}
requests.post(dingDing_robot, json=warning_msg, headers={"content-type":"application/json"})
def open_browser():
"""打开浏览器"""
# https://score.1111.com/
logging.info(">>>>>>>>>>>>>>>>>打开口语报告系统<<<<<<<<<<<<<<<<<<<<<<")
# driver.get("https://xxx.xxx.com/home")
driver.get("https://uat.xxx.xxx.com/xxx/home")
driver.maximize_window()
driver.implicitly_wait(10)
# 登录页,页面元素
account_input = (By.XPATH, "//input[@placeholder='Email']")
passwd_input = (By.XPATH, "//input[@placeholder='Password']")
verify_code = (By.XPATH, "//input[@placeholder='Code']")
signIn_btn = (By.XPATH, "//span[text()=' Sign In ']")
def login_system():
"""登录报告系统"""
# 用户登录信息
driver.find_element(*account_input).send_keys("xxx.x@xxx.tech")
driver.find_element(*passwd_input).send_keys("xxxx")
driver.find_element(*verify_code).send_keys("1234")
driver.find_element(*signIn_btn).click()
logging.info(">>>>>>>>>>>>>>>>>用户登录口语报告系统<<<<<<<<<<<<<<<<<<<<<<")
sleep(1)
# ui登录后获取请求头中token属性
headers = {"content-type":"application/json"}
# 登录系统后用户名元素
account_text = (By.CSS_SELECTOR, ".user_name")
change_passwd = (By.CSS_SELECTOR, ".pwdBtn")
def check_status():
"""检查登录状态"""
account_info = driver.find_element(*account_text).text
logging.info(">>>>>>>>>>>>>>>>>检查用户是否成功登录系统<<<<<<<<<<<<<<<<<<<<<<")
try:
assert account_info == "xxxx", "断言失败"
except:
logging.info(">>>>>>>>>>>>>>>>>用户:{},登录失败!!!<<<<<<<<<<<<<<<<<<<<<<".format(account_info))
else:
logging.info(">>>>>>>>>>>>>>>>>用户:{},登录成功!!!<<<<<<<<<<<<<<<<<<<<<<".format(account_info))
userInfo = json.loads(driver.execute_script('return localStorage.getItem("userInfo");'))
v = json.loads(userInfo.get("v"))
token = v.get("token")
headers["token"] = token
def login_by_accout():
"""帐号登录系统"""
url="https://uat.teacher.1111.com/hcp/1111/oralUsers/login"
# url="https://score.1111.com/hcp/1111/oralUsers/login"
data={"password": "xxx", "email": "xxx.x@xxx.tech","captcha":2953,"uuid":"12344"}
res=requests.post(url,json=data,headers={"content-type":"application/json"}).json()
token=res.get("content").get("oralUserToken")
headers["token"]=token
def get_fail_nums():
"""通过接口获取失败记录条数"""
url = "https://uat.xxx.xxx.com/xxx/xxx/xxx/page"
# url = "https://xxx.xxxx.com/xxx/xxx/xxx/page"
data = {"reportStatus":0, "curPage":1, "limit":8}
res = requests.get(url, params=data, headers=headers).json()
nums = res.get("content").get("total")
fails = res.get("content").get("list")
fail_li=[] # 记录fail报告id
for fail in fails:
fail_li.append(fail.get("id"))
return nums, fail_li
# 搜索条件
select_box = (By.XPATH, "//input[@placeholder='Report status']")
select_input = (By.XPATH, "//span[text()='Failed']")
select_btn = (By.XPATH, "//span[text()='Search']")
def click_search():
"""输入fail点击查询"""
# 查询操作
driver.find_element(*select_box).click()
sleep(1)
driver.find_element(*select_input).click()
driver.find_element(*select_btn).click()
# 列表是否有失败状态的
fail_status = "//td//div[text()='Fail']"
# 找到失败的edit按钮
edit_btn = (By.XPATH, "{}/../parent::td[1]//following-sibling::td//span[text()='Edit']".format(fail_status))
submit_selector = (By.XPATH, "//div[contains(text(),'Submit')]")
def submit_report():
"""提交报告"""
# 首先按fail条件查询
click_search()
# 找到更多需要edit的按钮
status_eles = driver.find_elements(*edit_btn)
ele_nums = len(status_eles) # 页面元素找到元素不唯一
# 接口获取fail总数及id列表
nums, fail_li = get_fail_nums()
# 补偿当前时间
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
if nums >= 1: # 有数据才发送钉钉
send_dingding("before", nums, fail_li, now_time)
else:
logging.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>没有fail状态的数据需要处理<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
return None
count = 0 # 记录真实补偿次数
success_li = [] # 记录成功补偿报告id
while ele_nums > 0:
status_eles = driver.find_elements(*edit_btn)
try:
for ele in status_eles:
clicked = ele.is_displayed() # 因为找到元素不唯一,需要判断元素是否显示
if clicked:
ele.click()
driver.find_element(*submit_selector).click() # 提交
success_li.append(driver.current_url.split("=")[-1])
#等待提交之后跳转的页面元素是否出现
WebDriverWait(driver, 50).until(EC.presence_of_element_located(change_passwd))
count += 1
except:
ele_nums -= 1
else:
ele_nums -= 1
finally:
click_search() # 每次需重新查询
duration_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
send_dingding("after", count, success_li, duration_time)
def close_browser():
"""退出浏览器"""
driver.quit()
if __name__ == '__main__':
while True:
# 一直循环去查fail数据,然后进行补偿
login_by_accout()
nums,fail_li=get_fail_nums()
if nums:
print("有fail需补偿数据")
open_browser()
login_system()
check_status()
submit_report()
close_browser()
print("补偿完了")
else:
print("没有补偿数据")
sleep(60)
if driver:
driver.quit()
代码实现思路
- 注意看main中的代码块:先通过接口去登录、查询fail条件的数据,有则通知执行ui测试代码
- 再说接口:通过获取fail的总数及id,将数据通过钉钉发送到群里通知,最后count统计补偿的数据及id
- 这份升级代码是从一步步真是环境验证改进而来,一开始在uat环境是ok的,但在生产总会有点问题:原因是作者将元素个数和edit次数混用了,应该区分计数器控制循环
linux部署selenium运行环境
安装chrome浏览器
yum install dl.google.com/linux/direc…
注意在安装过程中google-chrome浏览器驱动的版本,执行:google-chrome
# 安装chrome是的版本:
Package: google-chrome-stable-92.0.4515-107.x86_64 (google-chrome)
# 以下信息,告诉咱们启动脚本时,需要带上--no-sandbox参数
[6367:6367:0817/170516.458662:ERROR:zygote_host_impl_linux.cc(90)] Running as root without --no-sandbox is not supported. See https://crbug.com/638180.
在启动py脚本执行时报错:ChromeDriver is assuming that Chrome has crashed
实际情况,linux系统下无头启动浏览器设置如下
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless') # 浏览器不提供可视化页面. linux下如果系统不支持可视化不加这条会启动失败
chrome_options.add_argument('--no-sandbox')# 解决DevToolsActivePort文件不存在的报错
# 在windows下只需以上两项即可
chrome_options.add_argument('--disable-gpu') # 谷歌文档提到需要加上这个属性来规避bug
chrome_options.add_argument('--disable-dev-shm-usage')
下载chromedriver
chromedriver.storage.googleapis.com/index.html # 是选择对应浏览器的linux版本哦
解压得到chromedriver,执行输出如下信息,即正确:
Starting ChromeDriver 92.0.4515.107 (87a818b10553a07434ea9e2b6dccf3cbe7895134-refs/branch-heads/4515@{#1634}) on port 9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
[1629191031.401][SEVERE]: bind() failed: Cannot assign requested address (99)
ChromeDriver was started successfully.
重申:注意Chrome版本和ChromeDriver的版本要对应好,否则实际运行时会报错。当然一般chrome可以安装最新版本了,所以只要选择chromedriver时选择支持最新版本的即可
关于linux环境测试selenium环境代码如下:
#!/bin/python3
# -*-coding: utf-8 -*-
from selenium import webdriver
opt=webdriver.ChromeOptions()
opt.set_headless() # 自动适配对应参数
opt.add_argument('--no-sandbox') # 解决DevToolsActivePort文件不存在的报错,去掉这行会提示错误:unknown error: DevToolsActivePort file doesn't exist
driver=webdriver.Chrome(executable_path="./chromedriver",options=opt)
driver.get("https://www.baidu.com")
print(driver.title)
driver.quit()
问题:UnicodeEncodeError: ‘ascii’ codec can’t encode characters; 在linux系统中,如果py脚本中有中文注释,执行的时候会包字符解码错误
解决办法:在文件头部添加注释==>>> #-*-coding: utf-8 -*-
总结
以上为本篇的全过程,如有不对或更好的方法,欢迎拍砖!!!