使用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("[email protected]")
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("[email protected]")
    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": "[email protected]","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 -*-

总结

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

相关文章
|
3月前
|
人工智能 Ubuntu 前端开发
Dify部署全栈指南:AI从Ubuntu配置到HTTPS自动化的10倍秘籍
本文档介绍如何部署Dify后端服务及前端界面,涵盖系统环境要求、依赖安装、代码拉取、环境变量配置、服务启动、数据库管理及常见问题解决方案,适用于开发与生产环境部署。
856 1
|
3月前
|
弹性计算 Devops Shell
用阿里云 DevOps Flow 实现 ECS 部署自动化:从准备到落地的完整指南
阿里云 DevOps Flow 是一款助力开发者实现自动化部署的高效工具,支持代码流水线构建、测试与部署至ECS实例,显著提升交付效率与稳定性。本文详解如何通过 Flow 自动部署 Bash 脚本至 ECS,涵盖环境准备、流水线搭建、源码接入、部署流程设计及结果验证,助你快速上手云上自动化运维。
343 0
|
4月前
|
运维 Prometheus 监控
3 年部署经验总结:用自动化工具轻松管理 300+ 服务器开源软件
三年前接手公司IT部门时,我满怀信心,却发现部署效率低下。尽管使用了GitLab、Jenkins、Zabbix等100+开源工具,部署仍耗时费力。文档厚重如百科,却难解实际困境。一次凌晨三点的加班让我下定决心改变现状。偶然看到一篇国外博客,介绍了自动化部署的高效方式,我深受启发。
231 0
|
6月前
|
存储 文字识别 自然语言处理
通义大模型在文档自动化处理中的高效部署指南(OCR集成与批量处理优化)
本文深入探讨了通义大模型在文档自动化处理中的应用,重点解决传统OCR识别精度低、效率瓶颈等问题。通过多模态编码与跨模态融合技术,通义大模型实现了高精度的文本检测与版面分析。文章详细介绍了OCR集成流程、批量处理优化策略及实战案例,展示了动态批处理和分布式架构带来的性能提升。实验结果表明,优化后系统处理速度可达210页/分钟,准确率达96.8%,单文档延迟降至0.3秒,为文档处理领域提供了高效解决方案。
781 1
|
5月前
|
Web App开发 存储 前端开发
Python+Selenium自动化爬取携程动态加载游记
Python+Selenium自动化爬取携程动态加载游记
|
2月前
|
弹性计算 人工智能 前端开发
在阿里云ECS上部署n8n自动化工作流:U2实例实战
本文介绍如何在阿里云ECS的u2i/u2a实例上部署开源工作流自动化平台n8n,利用Docker快速搭建并配置定时任务,实现如每日抓取MuleRun新AI Agent并推送通知等自动化流程。内容涵盖环境准备、安全组设置、实战案例与优化建议,助力高效构建低维护成本的自动化系统。
694 5
|
3月前
|
jenkins Java 持续交付
使用 Jenkins 和 Spring Cloud 自动化微服务部署
随着单体应用逐渐被微服务架构取代,企业对快速发布、可扩展性和高可用性的需求日益增长。Jenkins 作为领先的持续集成与部署工具,结合 Spring Cloud 提供的云原生解决方案,能够有效简化微服务的开发、测试与部署流程。本文介绍了如何通过 Jenkins 实现微服务的自动化构建与部署,并结合 Spring Cloud 的配置管理、服务发现等功能,打造高效、稳定的微服务交付流程。
539 0
使用 Jenkins 和 Spring Cloud 自动化微服务部署
|
5月前
|
NoSQL 关系型数据库 Linux
ERPNext 搭建教程:Linux 一键部署与维护
ERPNext 是一款开源免费的企业资源计划系统,适用于中小企业信息化管理。基于 Python 和 Frappe 框架开发,支持财务、销售、人力、库存等模块,具备高度可定制性。本文介绍如何通过 Websoft9 在 Linux 下快速部署 ERPNext,并提供环境配置、系统维护等实用建议,适合开发者和企业用户快速上手。
900 7
ERPNext 搭建教程:Linux 一键部署与维护
|
5月前
|
运维 监控 持续交付
还在为部署开源工具烦恼?自动化部署工具 Websoft9一键部署 300+ 开源应用
在数字化时代,开源工具因免费、灵活、可定制等特性广受欢迎,但其部署过程却常因环境配置复杂、依赖繁琐、耗时长等问题令人头疼。本文介绍了传统部署的三大难点,并提出两种解决方案:传统手动部署与集成化控制台部署。
还在为部署开源工具烦恼?自动化部署工具 Websoft9一键部署 300+ 开源应用