使用Selenium自动化测试解决报告生成失败问题及Linux部署指南

简介: 这篇文章介绍了使用Selenium自动化测试解决报告生成失败问题的方法,包括Linux环境下的部署指南和代码实现。
介绍背景
情况是这样的,某段时间之前,开发就想找我用ui自动化帮他们实现一个功能:在系统某些时候生成报告的时候会fail,
但是又不再重新生成,需要人工edit再次submit才能生成,原因png是由当前html页面生成。但是作为测试的我有一个疑惑?
开始不是常用定时任务或是失败重试吗?怎么不这样做呢?或者有其他办法使之成功呢?然后开发自己优化了一下,就默默的成功了,
这事儿就算过去了,不曾想几天前又复活了,需要我来协助,然后问,这玩意儿不能使用接口去完成吗?开发解释:不能,why?
原因是html转png:前端拿到接口响应数据,动态绘制html,然后在生成png。问题来了:为什么会失败?什么情况下会失败呢?
开发与实现

作者的python开发环境那是有好几套,接口、ui自动化的环境那是现成的,拿来即用,这里用来演示步骤,就不截图了。

部署python本地开发环境
# 安装工具库
pip install selenium

再下载对应chromedriver浏览器驱动

开始码代码

又因为作者是有比较系统的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()
思路分析
  1. 第一步,开发针对我的特殊帐号去除验证码登录,<毕竟实现验证码登录成本还是有的>
  2. 首页增加报告查询条件:success or failed;
  3. 需要发送钉钉通知<这是后面实现>
  4. 操作流程:登录-检查登录状态-查询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传递给接口请求,

clipboard.png

# 这是个新技能点,每次尝试不同的需求,总会遇到不同的问题,然后找到解决方案 
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()
代码实现思路
  1. 注意看main中的代码块:先通过接口去登录、查询fail条件的数据,有则通知执行ui测试代码
  2. 再说接口:通过获取fail的总数及id,将数据通过钉钉发送到群里通知,最后count统计补偿的数据及id
  3. 这份升级代码是从一步步真是环境验证改进而来,一开始在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

clipboard.png 实际情况,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 -*-

总结

以上为本篇的全过程,如有不对或更好的方法,欢迎拍砖!!!

相关文章
|
26天前
|
搜索推荐 Linux 测试技术
Linux系统之部署homer静态主页
【10月更文挑战第11天】Linux系统之部署homer静态主页
75 41
Linux系统之部署homer静态主页
|
17天前
|
运维 监控 Linux
Linux系统之部署Linux管理面板1Panel
【10月更文挑战第20天】Linux系统之部署Linux管理面板1Panel
64 3
Linux系统之部署Linux管理面板1Panel
|
12天前
|
缓存 自然语言处理 并行计算
基于NVIDIA A30 加速卡推理部署通义千问-72B-Chat测试过程
本文介绍了基于阿里云通义千问72B大模型(Qwen-72B-Chat)的性能基准测试,包括测试环境准备、模型部署、API测试等内容。测试环境配置为32核128G内存的ECS云主机,配备8块NVIDIA A30 GPU加速卡。软件环境包括Ubuntu 22.04、CUDA 12.4.0、PyTorch 2.4.0等。详细介绍了模型下载、部署命令及常见问题解决方法,并展示了API测试结果和性能分析。
114 1
|
14天前
|
消息中间件 Linux RocketMQ
在Red Hat Enterprise Linux 9上使用Docker快速安装并部署
通过以上步骤,你可以在Red Hat Enterprise Linux 9上使用Docker快速安装并部署RocketMQ。这种方法不仅简化了安装过程,还提供了一个灵活的环境来管理和扩展消息队列系统。RocketMQ作为一款高性能的分布式消息系统,通过Docker可以实现快速部署和高效管理。
39 2
|
15天前
|
消息中间件 Linux RocketMQ
在Red Hat Enterprise Linux 9上使用Docker快速安装并部署
通过以上步骤,你可以在Red Hat Enterprise Linux 9上使用Docker快速安装并部署RocketMQ。这种方法不仅简化了安装过程,还提供了一个灵活的环境来管理和扩展消息队列系统。RocketMQ作为一款高性能的分布式消息系统,通过Docker可以实现快速部署和高效管理。
26 3
|
16天前
|
Java Linux 网络安全
NIFI在Linux服务区上的部署配置过程是什么?
【10月更文挑战第21天】NIFI在Linux服务区上的部署配置过程是什么?
35 2
|
18天前
|
关系型数据库 MySQL Linux
基于阿里云服务器Linux系统安装Docker完整图文教程(附部署开源项目)
基于阿里云服务器Linux系统安装Docker完整图文教程(附部署开源项目)
156 2
|
27天前
|
机器学习/深度学习 监控 计算机视觉
目标检测实战(八): 使用YOLOv7完成对图像的目标检测任务(从数据准备到训练测试部署的完整流程)
本文介绍了如何使用YOLOv7进行目标检测,包括环境搭建、数据集准备、模型训练、验证、测试以及常见错误的解决方法。YOLOv7以其高效性能和准确率在目标检测领域受到关注,适用于自动驾驶、安防监控等场景。文中提供了源码和论文链接,以及详细的步骤说明,适合深度学习实践者参考。
227 0
目标检测实战(八): 使用YOLOv7完成对图像的目标检测任务(从数据准备到训练测试部署的完整流程)
|
8天前
|
Web App开发 设计模式 JavaScript
自动化测试之美:如何利用Selenium实现Web应用的高效测试
【10月更文挑战第29天】在软件开发的世界中,测试是确保产品质量的关键步骤。本文将带你了解如何使用Selenium这一强大的自动化测试工具,提高Web应用测试的效率和准确性。通过实际案例,我们将探索Selenium的核心功能及其在现代软件开发中的应用,旨在帮助读者掌握自动化测试的精髓,从而提升软件测试工作的整体效能。
|
29天前
|
机器学习/深度学习 并行计算 数据可视化
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
这篇文章介绍了如何使用PaddleClas框架完成多标签分类任务,包括数据准备、环境搭建、模型训练、预测、评估等完整流程。
75 0
目标分类笔记(二): 利用PaddleClas的框架来完成多标签分类任务(从数据准备到训练测试部署的完整流程)
下一篇
无影云桌面