Python侧开28期-偕行-学习笔记-web自动化测试-selenium

一、web自动化测试体系与价值

做 UI 自动化之前,要想清楚想让 UI 自动化为你做什么,你想要做成 UI 自动化的的业务是不是稳定的,你要覆盖的场景是不是你每次回归测试必须要测试的,当脚本跑起来的时候,它的不稳定因素是什么?UI自动化也不仅仅是验证 UI 层相关的内容,也可以通过 UI 路径来验证接口的业务逻辑。UI 自动化测试是一把双刃剑,不要一味追求覆盖率,覆盖合适的场景才能形成最高的性价比。

1、 什么时候可以做UI自动化测试

  • 业务流程不频繁改动
  • UI 元素不频繁改动
  • 需要频繁回归的场景
  • 核心业务场景等
  • 项目周期长,大版本功能稳定,有小功能不断迭代,有必要对核心功能进行不断回归

2、 Web自动化测试学习路线

image

二、selenium环境准备

1、 Selenium的简介

  • 用于web浏览器测试的工具
  • 支持的浏览器包括IE,Firefox,Safari,Chrome,Edge等
  • 使用简单,可使用Java,Python等多种语言编写用例脚本
  • 主要由三个工具构成:WebDriver、IDE、Grid

架构图:

2、selenium环境配置

(1)安装selenium

pip install selenium

(2) Driver的下载与配置

Edge浏览器驱动下载
Chrome浏览器驱动下载

A、windows

  • 1、下载和浏览器对应版本的dirver
  • 2、 解压下载的驱动,获取到xxxdriver.exe(chromedriver.exe或者msedgedriver.exe等)
  • 3、两种方式任选一种:
    • a、将xxxdriver.exe放到某个路径,之后添加环境变量
    • b、直接将xxxdriver.exe复制到python.exe所在的目录,也就是Python的安装目录
      image

B、mac/linux

  • 1、下载和浏览器对应版本的dirver
  • 2、 解压下载的驱动,获取到xxxdriver
  • 3、将xxxdriver复制到/user/local/bin目录即可

3、selenium 4.x的一些介绍

4、Selenium Manager

Selenium Manager 为 Selenium 提供自动化的驱动程序和浏览器管理。

(1) 自动浏览器管理

Selenium Manager自动检查浏览器驱动是否安装,如果安装正确,则使用此驱动,如果不是,Selenium Manager将管理CfT,无论哪种情况,浏览器最终也会受到管理。

  • 前提:干掉之前手动安装的driver.exe;
  • 安装:pip install webdriver-manager

总结:无需手动更新driver。

  • Chrome:基于Chrome for Testing(CfT)从Selenium 4.11.0开始。
  • Firefox:基于公开的Firefox版本,从Selenium 4.12.0开始。
  • Edge:计划用于Selenium4.13.0
    image
from selenium import webdriver
# 导包的时候注意导入对应浏览器的service和manager
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service

service = Service(executable_path=ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)

三、selenium api使用

1、浏览器控制

操作 api 说明
打开浏览器 driver.get("url") 通过webdriver驱动浏览器打开一个网址
刷新driver驱动的网址窗口 driver.refresh() 注意:是get打开的网址所在的窗口,不一定是肉眼看到的页面窗口
回退到上一个driver驱动的网址 driver.back() 注意:是get打开的网址所在的窗口回退到上一个地址,不一定是肉眼看到的页面窗口
浏览器窗口最大化 driver.maximize_window()
浏览器窗口最小化 driver.minimize_window()
关闭driver驱动的网址页面 driver.close() 注意:是关闭get打开的网址页面,不一定是肉眼看到的页面窗口
关闭整个浏览器进程 driver.quit() 如果不调用quit方法杀掉浏览器进程,电脑CPU会炸
打开新窗口/页面 driver.execute_script(js) selenium没有打开新窗口/页面的方法,需要执行js脚本打开新窗口/页面

注意: 注意区分页面和窗口,也要注意refresh、back、close操作的具体是哪个窗口/页面

from time import sleep
from selenium import webdriver
# 创建驱动对象
driver = webdriver.Edge()
# 打开网页--百度
driver.get("https://www.baidu.com")
# 脚本停止执行3s
sleep(3)
# 窗口最大化
driver.maximize_window()
# 脚本停止执行3s
sleep(3)
# 打开一个新页面--搜狗
# js = 'window.open("https://www.sogou.com", "_blank", "resizable,scrollbars,status");' # 这个是打开新窗口
js = 'window.open("https://www.sogou.com");'# 这个是打开新页面
driver.execute_script(js)
# 脚本停止执行3s
sleep(3)
# 在百度窗口访问淘宝
driver.get("https://www.taobao.com/")
# 脚本停止执行3s
sleep(3)
# 刷新淘宝网页
driver.refresh()
# 脚本停止执行3s
sleep(3)
# 回退到百度网页
driver.back()
# 脚本停止执行3s
sleep(3)
# 关闭当前窗口--注意:这里关闭的是百度,而不是搜狗
driver.close()
# 脚本停止执行3s
sleep(3)
# 窗口最小化
driver.minimize_window()
# 脚本停止执行3s
sleep(3)
# 关闭整个浏览器
driver.quit()

补充: 使用js打开新窗口或者新页面

  • window.open(strUrl, strWindowName, [strWindowFeatures]);
    • strUrl: 新窗口需要载入的url地址。

    • strWindowName:新窗口的名字,通过这个名字我们可以获得新窗口的引用,容易重复,如果我们希望每次打开新窗口都是一个全新的窗口,可以设置成关键字 “_blank”。

      • 注意:_blank 是打开一个新窗口,不给这个参数才是打开新页面,注意区别,使用上方代码可以肉眼观察效果;
    • strWindowFeatures:新窗口的一些设置,比如是否显示菜单栏,是否可以滚动和缩放窗口大小等。

2、元素定位方法

注意:

  • 在最新的selenium版本中已经舍弃了driver.find_element_by_定位方式(定位元素)系列的定位方法,只支持driver.find_element(By.定位方式, 定位元素) 定位方法;

  • 在使用响应定位方法的时候,最好在网页源代码中搜索一下有没有重复,必须要用唯一的定位属性,不然会遇到让你炸裂的问题;

定位方式 api 说明
id属性值 driver.find_element(By.ID,“id”) 标签id 属性对应的值
name属性值 driver.find_element(By.NAME,“name”) 标签name属性对应的值
css选择器 driver.find_element(By.CSS_SELECTOR,“css_selector”) 标签的css选择器
xpath选择器 driver.find_element(By.XPATH,“xpath”) 标签的xpath选择器
class属性值 driver.find_element(By.CLASS_NAME,“class_name”) 标签的class属性对应的值
超链接文本 driver.find_element(By.LINK_TEXT,“link_text”) 基本都是用于a标签的文本定位,注意;文本需要完全匹配
超链接文本 driver.find_element(By.PARTIAL_LINK_TEXT,“partial_link_text”) 和上一个方法一样,只是如果多个元素匹配,则只会选择第一个元素
标签名 driver.find_element(By.TAG_NAME,“tag_name”) 标签名称
from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Edge()
driver.get("https://www.baidu.com/")
driver.maximize_window()

# 睡一秒
sleep(1)

# # id定位百度一下按钮
# button = driver.find_element(By.ID,"kw")
# # name定位搜索输入框
# query = driver.find_element(By.NAME,"wd")
#
# # 点击过去
# query.send_keys("刘涛")
# button.click()
# sleep(1)

# # css选择器定位登录按钮
# login = driver.find_element(By.CSS_SELECTOR,"#s-top-loginbtn")
# # 点击登录按钮
# login.click()
# sleep(2)
#
# # xpath选择器定位登录弹框X
# close = driver.find_element(By.XPATH,"//*[@id='TANGRAM__PSP_4__closeBtn']")
# # 关闭登录弹框继续首页定位
# close.click()
# sleep(1)

# # 使用标签class属性值定位--百度搜索输入框
# query = driver.find_element(By.CLASS_NAME,"s_ipt")
# query.send_keys("张雨绮")
# sleep(2)

# # 使用a标签的文本定位--百度新闻
# news = driver.find_element(By.LINK_TEXT,"新闻")
# news.click()
# sleep(2)

# driver.find_element(By.PARTIAL_LINK_TEXT,"partial_link_text")
# driver.find_element(By.TAG_NAME,"tag_name")

driver.quit()

定位策略
image
选择定位器通用原则

  1. 与研发约定的属性优先(比如约定css,则用css 属性选择器: [name='locate'])

  2. 身份属性 id,name(web 定位)

  3. 复杂场景使用组合定位:

    • xpath,css
    • 属性动态变化(id,text)
    • 重复元素属性(id,text,class)
    • 父子定位(子定位父)
  4. js定位

补充:css定位更多css定位请参考文章

  • 绝对定位:从很远的标签开始一级一级的往下下找,从浏览器copy过来的就是绝对定位方式;

  • 相对定位:通过几个css选择器的相对 关系 进行定位,比如 相邻兄弟(+)、兄弟(~)、父子(>)、后代(空格) 相对关系;

  • 相对定位的好处:

    • 可维护性更强
    • 语法更加简洁
    • 解决各种复杂的定位场景
  • 案例:分别使用绝对定位和相对定位来定位同一个元素的区别

# 绝对定位
$("#ember63 > td.main-link.clearfix.topic-list-data > span > span > a")
# 相对定位
$("#ember63 [title='新话题']")
  • 顺序定位,使用相对关系及伪类选择器(nth-child(n)nth-of-type(n)详情这篇文章找

    • 父子关系+顺序:'#form>input:nth-child(n)'找的是input的爹form的第n个孩子,也就是冒号前面元素的爹的第n个孩子,只有顺序和冒号前面的选择器都匹配的才会被找到
    • 父子关系+标签类型+顺序:'#form>input:nth-of-type(n)'匹配input的爹form的儿子中,叫input的第n个;
  • 检验css选择器是否正确以及该css选择器能匹配到多少元素的方法:

    • 1、直接打开浏览器开发者选项,在元素页面ctrl+f把ccss选择器输入,按回车,能搜索到元素就说明选择器编写正确,能在源码中搜索到多少个就说明能匹配到多少个;
    • 2、在开发者选项的控制台,使用jQuery的语法编写选择器直接打印找到的元素,点开查看length属性,length是几就找到几个,格式:$("css选择器")
  • 注意:css类选择器,如果在源码中有多个类名通过空格连接,在写css类选择器的时候,复制类名之后要把中间空格干掉改成.即可;
    image

补充:xpath定位

3、强制等待、隐式等待、显示等待

4、控件交互

(1)普通控件交互

场景 api 说明
点击 WebElement.click()
输入 WebElement.send_keys(str)
清空 WebElement.clear()
获取标签属性 WebElement.get_attribute(“属性名称”)
获取标签文本信息 WebElement.text
获取标签名 WebElement.tag_name
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

driver = webdriver.Edge()
driver.get("https://www.baidu.com")
driver.maximize_window()

news = WebDriverWait(driver,10,0.5).until(expected_conditions.element_to_be_clickable((By.LINK_TEXT,"新闻")))
print(news)
# 标签文本
print(news.text) # 新闻
# 标签名
print(news.tag_name) # a
# 标签属性
print(news.get_attribute("href")) # http://news.baidu.com/

driver.quit()

(2)高级控件交互ActionChains类

5、网页 frame 与多窗口处理

(1)多窗口处理

  • 点击某些链接,会重新打开⼀个窗口,对于这种情况,想在新页⾯上操作,就
    得先切换窗口。
  • 获取窗口的唯⼀标识句柄,只需要切换句柄,就可以在多个页⾯灵活操作。

A、具体流程

  • 1、确认想要操作的窗口句柄:driver.current_window_handle–用于后续切换的时候知道切换到哪个句柄;
  • 2、获取所有的窗口句柄:driver.window_handles–返回一个所有窗口句柄的列表;
  • 3、切换到想要操作的窗口句柄:driver.switch_to.window(window_name)–通过之前获取的窗口句柄名称,然后从窗口句柄列表中找;

B、来个案例

  • 需求:
    • 1、打开百度首页,点击登录,点击登录弹窗中立即注册,进入注册页面;
    • 2、在注册页面输入用户名、手机号之后,通过窗口句柄切换到百度首页;
    • 3、在百度首页输入用户名、密码,勾选登录协议,点击登录;

代码实现:

"""
多窗口切换
* 需求:
  * 1、打开百度首页,点击登录,点击登录弹窗中立即注册,进入注册页面;
  * 2、在注册页面输入用户名、手机号之后,通过窗口句柄切换到百度首页;
  * 3、在百度首页输入用户名、密码,勾选登录协议,点击登录;
"""
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Edge()
driver.maximize_window()

# 1、打开百度首页,点击登录,点击登录弹窗中立即注册,进入注册页面;
driver.get("https://www.baidu.com")
driver.implicitly_wait(5)
print(f"打开百度首页之后的窗口句柄:{driver.current_window_handle}") # 80529454E26756CE0F5DD5EE3363D2BA
print(f"打开百度首页之后的所有窗口句柄:{driver.window_handles}") # ['80529454E26756CE0F5DD5EE3363D2BA']
# 点击登录
driver.find_element(By.LINK_TEXT,"登录").click()
# 点击立即注册
driver.find_element(By.LINK_TEXT,"立即注册").click()
print(f"进入注册页面之后的窗口句柄:{driver.current_window_handle}") # 80529454E26756CE0F5DD5EE3363D2BA
print(f"进入注册页面之后的所有窗口句柄:{driver.window_handles}") # ['80529454E26756CE0F5DD5EE3363D2BA', 'EE695F66B3754567AEDE1F5ECDF83D44']

# 获取所有的窗口句柄
handles = driver.window_handles

# 2、在注册页面输入用户名、手机号之后,通过窗口句柄切换到百度首页;
# 需要先切换到注册页面的窗口句柄,不然无法定位到注册页面的元素
driver.switch_to.window(handles[-1])
print(f"切换到注册页面之后的当前句柄:{driver.current_window_handle}") # EE695F66B3754567AEDE1F5ECDF83D44
driver.find_element(By.ID,"TANGRAM__PSP_4__userName").send_keys("register_username")
driver.find_element(By.ID,"TANGRAM__PSP_4__phone").send_keys("register_phone")
driver.find_element(By.ID,"TANGRAM__PSP_4__password").send_keys("register_password")
time.sleep(3)

# 通过窗口句柄切换到百度首页
driver.switch_to.window(handles[0])
print(f"再次切换到首页之后的当前句柄:{driver.current_window_handle}") # 80529454E26756CE0F5DD5EE3363D2BA
time.sleep(3)

# 3、3、在百度首页输入用户名、密码,勾选登录协议,点击登录;
driver.find_element(By.ID,"TANGRAM__PSP_11__userName").send_keys("login_phone")
driver.find_element(By.ID,"TANGRAM__PSP_11__password").send_keys("login_password")
driver.find_element(By.ID,"TANGRAM__PSP_11__isAgree").click()
driver.find_element(By.ID,"TANGRAM__PSP_11__submit").click()

time.sleep(3)
driver.quit()

(2)网页frame处理

A、frame介绍

  • 在web⾃动化中,如果⼀个元素定位不到,那么很⼤可能是在iframe中。

  • 什么是frame?

    • frame是html中的框架,在html中,所谓的框架就是可以在同⼀个浏览器中显⽰不⽌⼀个页⾯。
    • 基于html的框架,又分为垂直框架和⽔平框架(cols,rows)。
  • Frame 分类

    • frame标签包含frameset、frame、iframe三种,
    • frameset:和普通的标签⼀样,不会影响正常的定位,可以使⽤css、xpath、id、name、class等任意种⽅式定位
    • frame与iframe:对selenium定位⽽⾔是⼀样的,需要切换进去之后才可以对立面的元素进行定位;
  • frame存在两种:⼀种是嵌套的,⼀种是未嵌套的。

B、切换frame的方法:

  • drive.switch_to.frame():切换到某个指定frame;

    • frame接收三种参数:
      • 字符串类型的frame标签的name属性值:driver.switch_to.frame(‘frame_name’)
      • 整数类型的frame的下标:driver.switch_to.frame(1)
      • 或者是定位到的frame元素:driver.switch_to.frame(driver.find_elements(By.TAG_NAME, “iframe”)[0])
  • drive.switch_to.default_content():切换到默认frame

  • drive.switch_to.parent_frame():切换到父级frame,如果当前的frame已经是最顶层frame,那保持不变;

C、frame处理案例

  • 地址:https://mail.163.com/
  • 需求:在163邮箱首页输入账号和密码点击登录;
    • 不切换frame是定位不到元素的;

代码实现:

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

drive = webdriver.Edge()
drive.maximize_window()
drive.get("https://mail.163.com/")
drive.implicitly_wait(5)

# # 不切换frame的时候直接定位输入框---定位不到
# email = drive.find_element(By.XPATH,'//*[@name="email"]').send_keys("email") # NoSuchElementException

# 切换frame--方式1:传入定位到的frame元素
frame = drive.find_element(By.XPATH,'//*[@id="loginDiv"]/iframe')
drive.switch_to.frame(frame)
drive.find_element(By.XPATH,'//*[@name="email"]').send_keys("email")
drive.find_element(By.XPATH,'//*[@name="password"]').send_keys("password")
time.sleep(2)
drive.find_element(By.ID,"dologin").click()

time.sleep(3)
drive.quit()

6、文件上传

  • input标签的文件上传:可以直接使⽤element.send_keys(⽂件地址)上传⽂件;
  • 非input型上传
      1. autoIT,借助外力,我们去调用其生成的au3或exe文件。
      1. Python pywin32库,识别对话框句柄,进而操作。
      1. SendKeys库–不稳定,不推荐。
      1. keybd_event,跟3类似,不过是模拟按键,ctrl+a,ctrl+c, ctrl+v…。

7、弹框处理

(1)弹框类型

  • div模态框:使用xpath、css等定位方法正常定位;

  • 系统弹框:

    • Alert弹窗:只有信息及确认按钮
      image
    • Confirm弹窗:在Alert弹窗基础上增加了取消按钮
      image
    • Prompt类型弹框:在Confirm的基础上增加了可输入文本内容的功能
      image
  • 通过js代码动态生成的div弹框:这种弹框基本都是点击js代码监听到点击时间弹出的提示信息,1-2s之后会自动消失,类似于Android中的toast,这种正常定位即可;

(2)系统弹框处理方法

  • driver.switch_to.alert:切换到弹框并且获取到弹框对象
  • alert.text:获取弹框中的文案;
  • alert.accept():接受提示框,相当于点击确定;
  • alert.dismiss():不接收提示框,相当于点击取消;
  • alert.send_keys():给提示框输入内容;

(3)系统弹框处理案例

"""
系统弹框处理
* Alert弹窗:只有信息及确认案例
* Confirm弹窗:在Alert弹窗基础上增加了取消按钮
* Prompt类型弹框:在Confirm的基础上增加了可输入文本内容的功能
"""
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Edge()
driver.maximize_window()
driver.get("https://vip.ceshiren.com/#/ui_study/date_picker")
driver.implicitly_wait(15)

# d点击按钮出现alert弹框
driver.find_element(By.ID,"warning_btn").click()

# 获取alert弹框
alert = driver.switch_to.alert
print(f"alert对象是啥:{alert}")# <selenium.webdriver.common.alert.Alert object at 0x0000028CFD44E590>
# 获取警告框中的文案
alerttext = alert.text
print(f"弹框中的文案:{alerttext}")
time.sleep(3)
# 接收弹框--相当于点击弹框中的确定
alert.accept()

time.sleep(3)
driver.quit()

(4)js动态div弹框案例

"""
div弹框处理
"""
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By

driver = webdriver.Edge()
driver.maximize_window()
driver.get("https://vip.ceshiren.com/#/ui_study/date_picker")
driver.implicitly_wait(15)

# 点击按钮出现toast--使用action模拟鼠标点击
btn = driver.find_element(By.ID,"success_btn")
action = ActionChains(driver)
time.sleep(1)
# 鼠标移上去
action.move_to_element(btn)
time.sleep(1)
# 点击--无语,点一次居然不出现toast,那就双击
action.double_click().perform()

time.sleep(1)
# 获取div弹框中的文案
toast = driver.find_element(By.XPATH,'//*[@role="alert"]')
print(f"toast中的文案:{toast.text}")

time.sleep(3)
driver.quit()

8、浏览器复用–复用已有浏览器

(1)为什么需要使用浏览器复用

  • 自动化测试过程中,存在人为介入场景;

    • 人为处理登录:无法通过输入用户名密码的方式解决登录问题,比如扫描登录,后面所有的脚本都无法执行;
  • 提高调试 UI 自动化测试脚本效率;

    • 调试时跳过步骤: 比如有多条测试用例,在第30步的时候卡住了,修改之后重新运行,需要从第一步开始运行,通过浏览器复用,可以只执行这一步;—使用debug模式启动浏览器,并在页面人工手动点击进入需要调试的页面,多余的步骤在脚本中注释掉,只运行需要调试的哪一步,脚本会复用这个浏览器直接就找到了需要调试的页面;
  • 说明:复用已有浏览器的具体脚本运行步骤如下:

    • 1、先把脚本写好放着不动,不着急执行;
    • 2、使用如下步骤将将浏览器的debug模式配置好,并以debug模式成功启动浏览器;
    • 3、在启动的浏览器中输入网址,人工登录或者是人工点击进入到脚本需要操作的界面;
    • 4、此时才去运行脚本,脚本不会启动新的浏览器窗口或者进程,而是复用已经启动了的debug模式下的浏览器,之所以叫复用已有浏览器的缘由也在于此;
    • 5、只要debug模式启动的浏览器窗口一直不关,脚本可以一直复用这个浏览器,这对脚本调试也带来了很大的便捷;

(2)浏览器复用步骤

A、google浏览器复用步骤

  • 1、配置环境变量,保证命令行输入chrome命令可以正常启动浏览器;

    • 将Chrome浏览器的安装目录添加到环境变量;
  • 2、 关闭所有的chrome浏览器窗口及进程;

    • windows:查看任务管理器里的浏览器进程;
    • linux/mac:使⽤ps aux|grep chrome|grep -v 'grep’查看是否有chrome进程存在。确保没有chrome进程被启动过。
  • 3、重启命令行窗口,在命令行输入命令启动浏览器:

    • windows:chrome --remote-debugging-port=9222
    • mac:Google\ Chrome --remote-debugging-port=9222
  • 4、验证浏览器是否启动成功

    • 在打开的浏览器中输入localhost:9222只要不显示无法访问此网站就表示配置成功
  • 5、 代码中增加debug参数;

from selenium import webdriver
# 注意:导包的时候不要导错,chrome.options和edge.options要分清楚
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

# Edge浏览器选项
# from selenium.webdriver.edge.options import Options
# 火狐浏览器选项
# from selenium.webdriver.firefox.options import Options

# 获取浏览器配置的实例对象
option = Options()
# 设置浏览器的debug模式和地址属性---值就是通过命令行启动浏览器时的localhost:端口
option.debugger_address = "localhost:9222"
# 创建浏览器驱动的时候需要把option对象传入
driver = webdriver.Chrome(options=option)
driver.maximize_window()

# 访问企业微信
driver.get("https://work.weixin.qq.com/wework_admin/frame")

B、Edge浏览器复用步骤

  • 1、配置环境变量,保证命令行输入Edge命令可以正常启动浏览器;

  • 2、 关闭所有的Edge浏览器窗口及进程–查看任务管理器里的浏览器进程;

  • 3、重启命令行窗口,在命令行输入命令启动浏览器:

# 1、正常情况下使用这个命名就可以
msedge.exe --remote-debugging-port=9225
# 2、有个时候需要加user-data-dir参数,否则会失败
msedge.exe --remote-debugging-port=9225 --user-data-dir="xxx"
  • 4、验证浏览器是否启动成功
    • 在打开的浏览器中输入localhost:9225只要不显示无法访问此网站就表示配置成功
  • 5、代码中增加EdgeOptions:
from selenium import webdriver
# 注意:导包的时候不要导错,chrome.options和edge.options要分清楚
from selenium.webdriver.edge.options import Options
from selenium.webdriver.common.by import By

# 获取浏览器配置的实例对象
option = Options()
# 设置浏览器的debug模式和地址属性---值就是通过命令行启动浏览器时的localhost:端口
option.debugger_address = "localhost:9225"
# 创建浏览器驱动的时候需要把option对象传入
driver = webdriver.Edge(options=option)
driver.maximize_window()

# 访问企业微信
driver.get("https://work.weixin.qq.com/wework_admin/frame")

(3)使用案例

  • 需求:进入企业微信,点击跳转到客户联系;

    • 地址:https://work.weixin.qq.com/wework_admin/frame
    • 说明:企业微信需要扫描登录,所以这里只能使用复用已有浏览器,先人工扫描登录进入到需要操作的页面之后,再去启动脚本才能实现需求;

A、复用google浏览器实现

import time

from selenium import webdriver
# 注意:导包的时候不要导错,chrome.options和edge.options要分清楚
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

# Edge浏览器选项
# from selenium.webdriver.edge.options import Options
# 火狐浏览器选项
# from selenium.webdriver.firefox.options import Options

# 获取浏览器配置的实例对象
option = Options()
# 设置浏览器的debug模式和地址属性---值就是通过命令行启动浏览器时的localhost:端口
option.debugger_address = "localhost:9222"
# 创建浏览器驱动的时候需要把option对象传入
driver = webdriver.Chrome(options=option)
driver.maximize_window()

# 访问企业微信
driver.get("https://work.weixin.qq.com/wework_admin/frame")

# 点击跳转到客户联系
driver.find_element(By.XPATH,'//*[(text()="客户联系")]').click()

time.sleep(5)
driver.quit()

B、复用Edge浏览器实现

import time

from selenium import webdriver
# 注意:导包的时候不要导错,chrome.options和edge.options要分清楚
from selenium.webdriver.edge.options import Options
from selenium.webdriver.common.by import By

# 获取浏览器配置的实例对象
option = Options()
# 设置浏览器的debug模式和地址属性---值就是通过命令行启动浏览器时的localhost:端口
option.debugger_address = "localhost:9225"
# 创建浏览器驱动的时候需要把option对象传入
driver = webdriver.Edge(options=option)
driver.maximize_window()

# 访问企业微信
driver.get("https://work.weixin.qq.com/wework_admin/frame")

# 点击跳转到客户联系
driver.find_element(By.XPATH,'//*[(text()="客户联系")]').click()

time.sleep(5)
driver.quit()

9、cookie复用

(1)什么是cookie

  • Cookie 是一些身份认证数据信息,当登录成功之后,服务端会返回带session标识的cookie信息存储在电脑的浏览器上,后续发送请求浏览器会带上这个cookie,服务器就知道是谁发过来的请求,也就是认证通过,这样就可以正常访问登录后的接口信息;

  • 当session过期或者是用户退出登录之后,之前的cookie就失效了,需要用户重新登录,这就是session-cookie的认证机制;

(2) 为什么要使用Cookie自动化登录

  • 复用浏览器仍然在每次用例开始都需要人为介入启动浏览器;
  • 若用例需要经常执行,复用浏览器则不是一个好的选择;
  • 大部分cookie的时效性都很长,扫一次登录获取cookie后可以使用多次;

(3)cookie复用流程

  • 1、编写脚本访问登录页面,设置强制等待时间,这个时间用于我们手动去登录;

  • 2、打开浏览器,扫码登录;

  • 3、确保登录之后,通过Python代码获取cookies,并确认是否获取成功;

    • driver.get_cookies()
  • 4、将获取到的cookie信息做持久化存储;

  • 5、Python代码中设置cookies,之后访问页面,后续的调试或者脚本运行就可以携带上这个cookie就行操作了;

    • driver.add_cookie(cookie)
  • 常见问题:

      1. 企业微信cookie有互踢机制。在获取cookie成功之后。不要再进行扫码操作!!!!
      1. 获取cookie的时候,即执行代码获取cookie时,一定要确保已经登录。
      1. 植入cookie的时候是给当前driver打开的地址设置cookie,所以在植入cookie之前需要先访问登录登录页面/刷新浏览器

(4)案例

  • 需求:对企业微信通讯录页面进行UI自动化测试,使用cookie处理登录;

    • 地址:https://work.weixin.qq.com/wework_admin/frame#contacts
  • 特别说明:植入cookie的时候是给当前driver打开的地址设置cookie,所以在植入cookie之前需要先访问登录登录页面

import time
import pytest
import yaml
from selenium import webdriver

class TestWeWork:

    def setup_class(self):
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()

    def teardown_class(self):
        self.driver.quit()

    # 给这条用例做个标记,这是专门用于获取cookies的用例,当cookies没有过期的时候就复用之前获取的cookie,那就通过标记跳过这条用例
    @pytest.mark.cookie
    def test_get_cookies(self):
        # 1、访问企业微信首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame")
        # 2、设置等待时间,这个时间用于去页面上手动扫描成功登录
        time.sleep(20)
        # 3、在页面上确认成功登录之后,在这里获取登录后的cookie信息
        cookies = self.driver.get_cookies()
        # 4、打印cookies信息确保成功获取到cookies
        print(cookies)
        # 5、将获取的cookie做持久化存储
        with open("wework_cookies.yaml","w") as f:
            yaml.safe_dump(cookies,f)

    def test_add_cookie(self):
        # 6、植入cookie的时候是给当前driver打开的地址设置cookie,所以在植入cookie之前需要先访问登录登录页面--或者刷新浏览器
        # self.driver.refresh()
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame")
        # 7、从文件中获取保持的cookies信息
        with open("wework_cookies.yaml",'r') as f:
            cookies = yaml.safe_load(f)
            print(cookies)
        # 8、设置cookie信息---从源码中发现add_cookie()就收的是字典,而cookies中是字典列表,所以需要遍历cookies将每个字典都添加进去
        for cookie in cookies:
            self.driver.add_cookie(cookie)
        # 9、访问目标页面,因为此时已经设置了cookie,已经不再需要扫描登录
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame#contacts")
        time.sleep(5)

10、自动化关键数据记录

内容 作用
日志 1. 记录代码的执行记录,方便复现场景;2. 可以作为bug依据
截图 1. 断言失败或成功截图;2.异常截图达到丰富报告的作用;3. 可以作为bug依据
page source 1. 协助排查报错时元素当时是否存在页面上

(1)行为日志记录

  • 日志封装
  • 脚本日志级别
    • debug记录步骤信息
    • info记录关键信息,比如断言等

日志封装.py

# 日志配置
import logging
# 创建logger实例
logger = logging.getLogger('simple_example')
# 设置日志级别
logger.setLevel(logging.DEBUG)
# 流处理器
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# 日志打印格式
formatter = logging.Formatter\
('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 添加格式配置
ch.setFormatter(formatter)
# 添加日志配置
logger.addHandler(ch)

脚本:

# 日志与脚本结合
class TestDataRecord:
    def setup_class(self):
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(3)
    
    def teardown_class(self):
        self.driver.quit()

    def test_log_data_record(self):
        # 实例化self.driver
        search_content = "霍格沃兹测试开发"
        # 打开搜狗首页
        self.driver.get("https://www.sogou.com/")
        logger.debug("打开搜狗首页")
        # 输入霍格沃兹测试学院
        self.driver.find_element(By.CSS_SELECTOR, "#query").\
        send_keys(search_content)
        logger.debug(f"搜索的内容为{search_content}")
        # 点击搜索
        self.driver.find_element(By.CSS_SELECTOR, "#stb").click()
        # 搜索结果
        search_res = self.driver.find_element(By.CSS_SELECTOR, "em")
        logger.info(f"搜索结果为{search_res.text}")
        assert search_res.text == search_content

(2)异常截图记录

  • driver.save_screenshot(截图路径+名称)
    • 这个方法底层调用了get_screenshot_as_file(filename)方法;
  • 记录关键页面
    • 断言页面
    • 重要的业务场景页面
    • 容易出错的页面

案例:

"""
异常截图
"""
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By

driver = webdriver.Edge()
driver.maximize_window()
driver.get("https://vip.ceshiren.com/#/ui_study/date_picker")
driver.implicitly_wait(5)

# 双击按钮
action = ActionChains(driver)
btn = driver.find_element(By.ID,"primary_btn")
action.double_click(btn).perform()
# 截图保留双击之后的效果
driver.save_screenshot("screenshot/double.png")

driver.quit()

(3)page_source页面源码记录

  • 使用driver.page_source属性获取页面源码,返回的是字符串;
  • 在调试过程中,如果有找不到元素的错误时可以保存当时页面的page_source调试代码;

案例:

"""
获取页面源码
"""
import time
from selenium import webdriver

driver = webdriver.Edge()
driver.maximize_window()
driver.get("https://vip.ceshiren.com/#/ui_study/date_picker")
driver.implicitly_wait(5)
time.sleep(5)

# 获取页面源码
source = driver.page_source
print(f"页面源码:{source}")
# 把源码写入html文件
with open("pagesource/double.html","w",encoding="utf-8") as f:
    f.write(source)

driver.quit()

11、异常自动截图

初步方案:使用异常捕获,操作元素的时候产生了异常,捕获异常之后保存截图和页面源码;

import time
import allure
from selenium import webdriver
from selenium.webdriver.common.by import By

class TestBaidu:

    def test_search(self):
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.get("https://www.baidu.com")
        try:
            # 如果操作元素出现了异常,则捕获异常
            self.driver.find_element(By.ID, "kw1").send_keys("selenium")
        except Exception:
            # 捕获异常之后保留截图和页面源码

            # 拼接文件路径
            temp_time = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())
            shotpath = "screenshot/search/" + temp_time + "百度首页截图.png"
            # 保存截图
            self.driver.save_screenshot(shotpath)
            # 将截图文件上传到allure报告
            allure.attach.file(
                # 文件路径
                source=shotpath,
                # allure上文件名称
                name="页面截图",
                # 文件类型
                attachment_type=allure.attachment_type.PNG,
                # 文件扩展名
                extension="png"
            )

            # 拼接文件路径
            sourcepath = "pagesource/search/" + temp_time + "百度首页源码.html"
            # 保存页面源码
            with open(sourcepath, 'w', encoding='utf-8') as f:
                f.write(self.driver.page_source)
            # 将源码文件上传到allure报告
            allure.attach.file(
                source=sourcepath,
                name="页面源码",
                attachment_type=allure.attachment_type.TEXT,
                extension="html"
            )

            # 为了不然异常处理影响用例的真实运行结果,需要再次把异常跑出去
            raise Exception
        time.sleep(3)
        self.driver.quit()

问题:如果每个元素操作都使用异常捕获,那会增加很大代码量;

优化方案:定义一个装饰器实现异常处理,给用例加上装饰器即可;

import time
import allure
from selenium import webdriver
from selenium.webdriver.common.by import By

def ui_exception_recode(func):
    # 通用装饰器形式(*args,**kwargs),任何方法都可以被装饰
    def inner(*args,**kwargs):
        # 执行被装饰函数,如果函数有异常则捕获
        try:
            # 因为被装饰函数需要拿到元素之后进行操作,所以这里需要有返回值
            return func(*args,**kwargs)
        except Exception:
            # 因为在异常处理过程中需要用到driver,所以从传过来的参数中获取driver
            driver = args[0].driver

            # 捕获异常之后保留截图和页面源码
            # print("异常了。。。")
            # 拼接文件路径
            temp_time = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())
            shotpath = "screenshot/search/" + temp_time + "百度首页截图.png"
            # 保存截图
            driver.save_screenshot(shotpath)
            # 将截图文件上传到allure报告
            allure.attach.file(
                # 文件路径
                source=shotpath,
                # allure上文件名称
                name="页面截图",
                # 文件类型
                attachment_type=allure.attachment_type.PNG,
                # 文件扩展名
                extension="png"
            )

            # 拼接文件路径
            sourcepath = "pagesource/search/" + temp_time + "百度首页源码.html"
            # 保存页面源码
            with open(sourcepath, 'w', encoding='utf-8') as f:
                f.write(driver.page_source)
            # 将源码文件上传到allure报告
            allure.attach.file(
                source=sourcepath,
                name="页面源码",
                attachment_type=allure.attachment_type.TEXT,
                extension="html"
            )

            # 为了不然异常处理影响用例的真实运行结果,需要再次把异常跑出去
            raise Exception
    return inner

class TestBaidu:

    @ui_exception_recode
    def test_search(self):
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.get("https://www.baidu.com")
        # 百度输入框的id=kw,这里为了让异常产生用了kw1,也就是页面找不到id=kw1这个元素,抛出异常
        self.driver.find_element(By.ID, "kw1").send_keys("selenium")
        time.sleep(3)
        self.driver.quit()

12、多浏览处理

(1) 多浏览器测试概述

  • 是跨不同浏览器组合验证网站或 web 应用程序功能的过程
  • 兼容性测试的一个分支,用于保持功能和质量的一致性
  • 适用于面向客户的网站和组织内部使用的站点

(2)多浏览器的实现方案

(3)使用pytest的hook函数新增选择浏览器的命令行参数

  • pytest_addoption 添加命令行参数组/命令行参数
  • pytest_configure 解析命令行选项,每个插件都会用到这个hook函数

13、执行js脚本(js代码)

(1)常用js脚本-更多js用法查看这里

A、 通过 css 查找操作元素

  • 点击元素(对应click)
  • 修改属性值–input标签对应的值(对应send_keys)
  • 元素的类属性
  • 元素的文本属性
// 百度首页:https://www.baidu.com/
// 修改属性值--input标签对应的值(对应send_keys)
document.querySelector("#kw").value = "霍格沃兹测试学院"
// 点击操作
document.querySelector("#su").click()
// 修改属性值后再点击
document.querySelector("#kw").value="霍格沃兹";document.querySelector("#su").click();

// 淘宝首页: https://www.taobao.com/ 
// 修改元素的类属性
document.querySelector("#J_SiteNavMytaobao").className\
="site-nav-menu site-nav-mytaobao site-nav-multi-menu J_MultiMenu site-nav-menu-hover"
// 测试人首页:https://ceshiren.com/
// 获取元素内的文本信息
document.querySelector("#ember63").innerText

案例1:修改时间控件的值

// 修改时间控件的值--https://www.12306.cn/index/
document.querySelector("#train_date").value="2020-02-02"

案例2:通过修改元素类属性的方式实现鼠标悬停下拉框处理

  • 淘宝首页-我的淘宝控件:
    • 用selenium的api处理:先定位到元素,然后模拟鼠标移动到该元素并悬停,才能出现下拉框,之后才可以对下拉框里面的元素进行操作;

    • 使用js代码处理:打开开发者选项,找到控件的源代码,然后把鼠标放上去在移开,反复操作,然后去观察html代码的变化,发现class类属性的值在变化,那就大胆猜测,这个下拉框是受class类属性的值控制的,那就通过js代码直接修改这个控件的class属性值即可然下拉框一直显示;


// 修改元素的类属性
document.querySelector("#J_SiteNavMytaobao").className="site-nav-menu site-nav-mytaobao site-nav-multi-menu J_MultiMenu site-nav-menu-hover"

B、 JS脚本滚动操作

  • 页面滚动到底部:document.documentElement.scrollTop=10000;
  • 页面滚动到指定元素位置:document.querySelector('css表达式').scrollIntoView();
    • 先使用css选择器定位到元素,然后再滚动到指定元素;

案例:在测试人社区首页滚动到某个页面不可见元素

// 页面滚动到指定元素位置
document.querySelector('[data-topic-id="25012"] td span a').scrollIntoView()

(2)js脚本使用步骤

  • 先在浏览器开发者工具的consol中调试js代码,调通达到预期结果之后再用selenium的api去执行js代码;
  • 调用selenium的api去执行js代码;
    • 执行普通js代码:driver.execute_script("js 代码")
    • 执行带返回值的js代码:driver.execute_script("return js脚本")
"""
执行js代码
需求:打开淘宝首页
1、定位到我的淘宝-我的足迹,获取控件的文本信息
2、滚动到页面不可见某个控件,获取文案
"""
import time

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.maximize_window()
driver.get("https://www.taobao.com/")
driver.implicitly_wait(5)

# 通过js修改”我的淘宝“控件的class属性值,让hover下拉框一直出现,然后使用selenium的定位去定位“我的足迹”,获取控件的文本信息
driver.execute_script('document.querySelector("#J_SiteNavMytaobao").className="site-nav-menu site-nav-mytaobao site-nav-multi-menu J_MultiMenu site-nav-menu-hover";')
time.sleep(3)
# 使用selenium的定位去定位“我的足迹”
my = driver.find_element(By.XPATH,'//*[@class="site-nav-menu-bd-panel menu-bd-panel"]//*[text()="我的足迹"]')
print(f"控件文本信息为:{my.text}")
time.sleep(5)

# 滚动到页面不可见某个控件,获取文案
js1 = '''return document.querySelector("[class='mod mod-last '] div h4 span");'''
js2 = '''document.querySelector("[class='mod mod-last '] div h4 span").scrollIntoView();'''
el = driver.execute_script(js1)
print(el)

driver.execute_script(js2)
features = driver.find_element(By.CSS_SELECTOR,'[class="mod mod-last "] div h4 span')
print(f"不可见控件文案:{features.text}")

14、浏览器配置 Options类

  • 是一个配置浏览器启动的选项类,用于自定义和配置Driver会话。

  • 在创建 WebDriver实例时,可以配置它的启动参数以进行一些初始设置,这些设置将会在 WebDriver 的整个生命周期内生效对于不同类型的浏览器,WebDriver 传入的参数并不相同

  • 常见使用场景:

    • 设置无头模式:不会显示调用浏览器,避免人为干扰的问题。
      • options.add_argument('--headless')
    • 设置调试模式:调试自动化测试代码(浏览器复用)。
      • option.debugger_address = "localhost:9222"
  • 特别注意: 当使用了options选项之后,在创建driver的时候需要传入参数options=

from selenium import webdriver
from selenium.webdriver.common.by import By

option = webdriver.ChromeOptions()
# 设置窗口最大化
# option.add_argument("start-maximized")
# 指定浏览器分辨率
option.add_argument('window-size=777×777')
# 无头模式
# option.add_argument('--headless')
driver = webdriver.Chrome(options=option)
# 打开测试人页面
driver.get("https://ceshiren.com/")

(1)创建Options实例的方法

A、直接通过父类Optoins进行实例化

  • 注意:通过这种方式的时候,导包的时候需要导入对应浏览器的options包
# 注意:导包的时候不要导错,chrome.options和edge.options要分清楚
from selenium.webdriver.chrome.options import Options

# Edge浏览器选项
# from selenium.webdriver.edge.options import Options
# 火狐浏览器选项
# from selenium.webdriver.firefox.options import Options

# 获取浏览器配置的实例对象
option = Options()

B、通过webdirver的方法去创建options对象–在新版本中这种方式已经不推荐

from selenium import webdriver

option = webdriver.ChromeOptions()
option = webdriver.EdgeOptions()
option = webdriver.FirefoxOptions()

(2)常用配置

  • 无头模式: --headless
  • 窗体最大化 start-maximized
  • 指定浏览器分辨率 window-size=1920x3000
from selenium import webdriver
from selenium.webdriver.common.by import By

option = webdriver.ChromeOptions()
# 设置窗口最大化
# option.add_argument("start-maximized")
# 指定浏览器分辨率
# option.add_argument('window-size=777×777')
# 无头模式
option.add_argument('--headless')
driver = webdriver.Chrome(options=option)
# 打开测试人页面
driver.get("https://ceshiren.com/")

# 随便定位一个元素获取文本
t = driver.find_element(By.XPATH,'//*[text()="所有类别"]').text
print(t)

driver.quit()

(3)注意:在selenium 4.x以上之后,options和capability 有千丝万缕的关系,需要花时间去看看源码好好梳理一下两者的区别与联系和具体用法–TODO

15、 capability 配置参数解析与分布式运行

(1)capability

  • Capabilities是WebDriver支持的标准命令之外的扩展命令(配置信息)
  • 配置web驱动的属性,如浏览器名称、浏览器平台等。
  • 结合Selenium Grid完成分布式、兼容性等测试
  • 注意:在selenium 4.x以上之后,options和capability 有千丝万缕的关系,需要花时间去看看源码好好梳理一下两者的区别与联系和具体用法–TODO

A、capability使用

(2)Selenium Grid

  • Selenium Grid 允许我们在多台机器上并行运行测试,并集中管理不同的浏览器版本和浏览器配置(而不是在每个单独的测试中)。
  • Selenium Grid 4.x
    image

(3)老版本使用案例

  1. 保证本地可以正常调通
  2. 实例化Remote()类并添加相应的配置
  • 远程地址
  • 设备配置
from selenium import webdriver
from selenium.webdriver.common.by import By

def test_ceshiren_ca():
    # mac
    # capability = {"platformName": "mac"}
    capability = {"platformName": "windows"}
    driver = webdriver.Chrome(desired_capabilities=capability)
    driver.get("https://ceshiren.com/")

def test_ceshiren_grid():
    # 1. 保证本地执行代码调通
    # 2. 切换为webdriver.remote
    # driver = webdriver.Chrome()
    executor_url = "https://selenium-node.hogwarts.ceshiren.com/wd/hub" # 这是远端的selenium grid地址--这个地址目前已经不能用了,这里仅展示代码
    capabilities = {"browserName":"chrome","browserVersion":"99.0"}
    # capabilities = {"browserName":"firefox"}
    driver = webdriver.Remote(
        # Either a string representing URL of the remote server or a custom
        # remote_connection.RemoteConnection object. Defaults to 'http://127.0.0.1:4444/wd/hub'.
        command_executor=executor_url,
        # A dictionary of capabilities to request when starting the browser session. Required parameter.
        desired_capabilities=capabilities
    )
    driver.implicitly_wait(3)
    driver.get("https://ceshiren.com/")
    login_text = driver.find_element(By.CSS_SELECTOR, ".login-button").text
    print(login_text)

16、验证码问题

  • 1、自动化时关闭验证码
  • 2、测试时使用万能验证码
  • 3、使用成功登录后的cookies绕过登录
  • 4、使用图像识别技术处理简单图片验证码-- ddddocr模块
  • 5、使用selenium模拟鼠标事件处理拖拽等验证码

四、PageObject设计模式

image

1、POM建模原则

  • 字段意义

    • 不要暴露页面内部的元素给外部
    • 不需要建模 UI 内的所有元素
  • 方法意义

    • 用公共方法代表 UI 所提供的功能
    • 方法应该返回其他的 PageObject 或者返回用于断言的数据
    • 同样的行为不同的结果可以建模为不同的方法
    • 不要在方法内加断言

案例:搜索场景-POM 脚本

from selenium import webdriver
from selenium.webdriver.common.by import By


class SearchPage:

    __INPUT_SEARCH = (By.NAME, "q")
    __BUTTON_SEARCH = (By.CSS_SELECTOR, "i.search")
    __SPAN_STOCK = (By.XPATH, "//table//strong")

    def __init__(self):
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(3)
        self.driver.get("https://xueqiu.com/")

    def search_stock(self, stock_name: str):
        self.driver.find_element(*self.__INPUT_SEARCH).send_keys(stock_name)
        self.driver.find_element(*self.__BUTTON_SEARCH).click()
        name = self.driver.find_element(By.XPATH, "//table//strong").text
        return name

2、 恢复用例初始状态

  • 问题:单条用例执行完成之后如果不恢复下一条用例的开始状态(回复用例初始页面),则会影响下一条用例的执行。

  • 解决方案:

    • 每条用例执行完成都quit()(影响执行效率)
    • 封装一个方法,用例执行完成之后回到首页

3、 数据清理

  • 清理策略

    • 在前置处理中执行
    • 在后置处理中执行
  • 清理方式

    • 调用业务接口
    • 通过UI自动化方式操作
    • 连接数据库执行SQL(不推荐)