介绍背景
情况是这样的,某段时间之前,开发就想找我用ui自动化帮他们实现一个功能:在系统某些时候生成报告的时候会fail, 但是又不再重新生成,需要人工edit再次submit才能生成,原因png是由当前html页面生成。但是作为测试的我有一个疑惑? 开始不是常用定时任务或是失败重试吗?怎么不这样做呢?或者有其他办法使之成功呢?然后开发自己优化了一下,就默默的成功了, 这事儿就算过去了,不曾想几天前又复活了,需要我来协助,然后问,这玩意儿不能使用接口去完成吗?开发解释:不能,why? 原因是html转png:前端拿到接口响应数据,动态绘制html,然后在生成png。问题来了:为什么会失败?什么情况下会失败呢?
AI 代码解读
开发与实现
作者的python开发环境那是有好几套,接口、ui自动化的环境那是现成的,拿来即用,这里用来演示步骤,就不截图了。
部署python本地开发环境
# 安装工具库 pip install selenium
AI 代码解读
开始码代码
又因为作者是有比较系统的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()
AI 代码解读
思路分析
- 第一步,开发针对我的特殊帐号去除验证码登录,<毕竟实现验证码登录成本还是有的>
- 首页增加报告查询条件: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::平级中的下级
AI 代码解读
- 其实实现之后并不需要如此,因为是先按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
AI 代码解读
- 从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()
AI 代码解读
代码实现思路
- 注意看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.
AI 代码解读
在启动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')
AI 代码解读
下载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.
AI 代码解读
重申:注意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()
AI 代码解读
问题:UnicodeEncodeError: ‘ascii’ codec can’t encode characters; 在linux系统中,如果py脚本中有中文注释,执行的时候会包字符解码错误
解决办法:在文件头部添加注释==>>> #-*-coding: utf-8 -*-
总结
以上为本篇的全过程,如有不对或更好的方法,欢迎拍砖!!!