目录
- 1 Web自动化入门基础
- 1.1 自动化知识以及工具
- 1.2 主流web自动化测试工具
- 1.3 入门案例
- 2 使用工具的API
- 2.1 元素定位
- 2.1.1 id选择器
- 2.1.2 name
- 2.1.3 class_name选择器
- 2.1.4 tag_name选择器
- 2.1.5 link_text选择器
- 2.1.6 partial_link_text选择器
- 2.1.7 xpath选择器
- 2.1.8 CSS选择器
- 2.1.9 Xpath和CSS区别
- 2.1.10 元素定位分类
- 2.1.11 元素定位的另一种写法
- 2.2 元素操作
- 2.3 浏览器操作
- 2.4 获取元素信息
- 2.5 鼠标操作
- 2.5.1 常用方法
- 2.5.2 执行的方法
- 2.5.3 鼠标右击
- 2.5.4 鼠标双击
- 2.5.5 鼠标悬停
- 2.5.6 鼠标拖动
- 2.6 键盘操作
- 2.6.1 常用操作
- 2.6.2 键盘操作
- 2.6.3 元素等待
- 2.6.4 隐式等待
- 2.6.5 显式等待
- 2.6.6 隐式和显式区别
- 2.6.7 下拉框
- 2.6.8 弹出框分类
- 2.7 滚动条实现方法
- 2.8 frame切换
- 2.8.1 多窗口切换
- 2.8.2 窗口截图
- 2.9 验证码处理
- 2.10 cookie
- 3 Pytest框架
- 3.1 总体介绍
- 3.2 断言方法
- 3.3 setup和teardown
- 3.4 配置文件
- 3.5 测试报告插件
- 3. 6 数据参数化
- 3.6.1 单一参数
- 3.6.2 多个参数
- 3.6.3 推荐用法
- 4 PO模式
- 4.1 递进学习路线
- 4.2 无模式
- 4.2.1 案例说明
- 4.2.2 选择测试用例
- 4.3 V1版本
- 4.4 V2版本
- 4.4.1 方法封装
- 4.5 V3版本
- 4.6 PO模式
- 4.6.1 概念
- 4.6.2 PO模式分层
- 4.6.3 PO模式优点
- 4.7 V4版本
- 4.8 v5版本
- 4.9 v6版本
- 4.9.1 示例代码
- 5 数据驱动
- 5.1 JSON基本介绍
- 5.2 字典与JSON转换
- 5.3 JSON文件读写
- 6 项目实战
- 6.1 项目结构
- 6.2 base包
- 6.3 data包
- 6.4 page包
- 6.5 scripts包
- 6.6 utils包
- 7 日志收集
- 7.1 日志收集
- 7.2 日志高级用法
- 7.3 四大组件
- 7.3.1 Logger类
- 7.3.2 Handler类
- 7.3.3 Formatter类
- 8 面试题
1 Web自动化入门基础
1.1 自动化知识以及工具
自动化概念 :由机器设备代替人工自动完成指定目标的过程
优点:
- 减少人工劳动力
- 提高工作效率
- 产品规格统一标准
- 规模化
- 安全
自动化测试概念 :由程序代替人工去执行测试的过程
应用场景 :
- 解决回归测试
- 已实现的功能需要回归
- 已解决的bug需要回归
- 解决压力测试:例如使用Jmeter做接口自动化
- 解决兼容性测试:在不同浏览器上做兼容性测试
- 解决操作重复性问题
1.2 主流web自动化测试工具
- QTP :收费且支持web/桌面自动化测试
- selenium:开源web自动化测试工具(功能测试)【跨平台、支持多种浏览器、支持多种语言、稳定】
- robot framework :基于python的可扩展的关键字驱动的自动化测试框架
1.3 入门案例
# 导包
import time
from selenium import webdriver
# 创建浏览器驱动
driver = webdriver.Chrome()
# 打开百度首页
driver.get("http://www.taobao.com")
# 暂停3秒
time.sleep(10)
# 关闭浏览器
driver.quit()
2 使用工具的API
2.1 元素定位
八种定位方式 :
2.1.1 id选择器
案例:
打开https://parabank.parasoft.com/parabank/admin.htm网站首页,完成以下操作
-
最大化页面
-
使用ID定位,输入用户名:admin
-
使用ID定位,输入密码:12345
-
等待5s,关闭浏览器
import time
from selenium import webdriver
# 获取驱动对象
driver = webdriver.Chrome()
# 获取地址
driver.get("https://parabank.parasoft.com/parabank/admin.htm")
# 页面最大化
driver.maximize_window()
# 根据id查找元素
driver.find_element_by_id('username').send_key("admin")
driver.find_element_by_id('password').send_key("123456")
# 等待五秒
time.sleep(5)
# 关闭浏览器
driver.quit()
2.1.2 name
案例:
打开https://parabank.parasoft.com/parabank/admin.htm 网站首页,完成以下操作
-
最大化页面
-
使用name定位,输入用户名:admin
-
使用name定位,输入密码:12345
-
等待5s,关闭浏览器
import time
from selenium import webdriver
# 获取驱动对象
driver = webdriver.Chrome()
# 获取地址
driver.get("https://parabank.parasoft.com/parabank/admin.htm")
# 页面最大化
driver.maximize_window()
# 根据id查找元素
driver.find_element_by_name('username').send_key("admin")
driver.find_element_by_name('password').send_key("123456")
# 等待五秒
time.sleep(5)
# 关闭浏览器
driver.quit()
2.1.3 class_name选择器
案例:
打开https://parabank.parasoft.com/parabank/admin.htm网站首页,完成以下操作
-
最大化页面
-
使用class_name定位,输入用户名:admin
-
使用class_name定位,输入密码:12345
-
等待5s,关闭浏览器
import time
from selenium import webdriver
# 获取驱动对象
driver = webdriver.Chrome()
# 获取地址
driver.get("https://parabank.parasoft.com/parabank/admin.htm")
# 页面最大化
driver.maximize_window()
# 根据id查找元素
driver.find_element_by_class_name('username').send_key("admin")
driver.find_element_by_class_name('password').send_key("123456")
# 等待五秒
time.sleep(5)
# 关闭浏览器
driver.quit()
2.1.4 tag_name选择器
# 1.导包
import time
from selenium import webdriver
# 2.创建浏览器驱动对象
driver = webdriver.Chrome()
# 3.业务操作
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_tag_name("input").send_keys("xxxxxx")
# 4.暂停5秒
time.sleep(5)
# 5.关闭驱动对象
driver.quit()
2.1.5 link_text选择器
# 1.导包
import time
from selenium import webdriver
# 2.创建浏览器驱动对象
driver = webdriver.Chrome()
# 3.业务操作
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_link_text("访问 新浪 网站").click()
# 4.暂停5秒
time.sleep(5)
# 5.关闭驱动对象
driver.quit()
2.1.6 partial_link_text选择器
# 1.导包
import time
from selenium import webdriver
# 2.创建浏览器驱动对象
driver = webdriver.Chrome()
# 3.业务操作
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# driver.find_element_by_partial_link_text("访问 新浪 网站").click() # 通过全部文本定位超链接
driver.find_element_by_partial_link_text("访问").click() # 通过局部文本定位超链接
# 4.暂停5秒
time.sleep(5)
# 5.关闭驱动对象
driver.quit()
定位一组元素
# 1.导包
import time
from selenium import webdriver
# 2.创建浏览器驱动对象
driver = webdriver.Chrome()
# 3.业务操作
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
elements = driver.find_elements_by_tag_name("input")
elements[1].send_keys("123456")
# 4.暂停5秒
time.sleep(5)
# 5.关闭驱动对象
driver.quit()
2.1.7 xpath选择器
四种定位方式
- 路径
- 元素属性
- 属性与逻辑结合
- 层级与属性结合
方法
element = driver。find_element_by_xpath(xpath)
路径
- 绝对路径:
- 从外层元素到指定元素之间所有经过元素层级的路径
- 绝对路径以/html根节点开始,使用/来分割元素层级,如:/html/body/div/fieldset/p[1]/input
- 绝对路径对页面要求严格,不建议使用
- 相对路径
- 匹配任意层级的元素,不限制元素的位置
- 相对路径//开始
- 格式://input 或者 //*
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 定位用户名输入框, 输入 admin
driver.find_element_by_xpath("/html/body/div/fieldset/form/p[1]/input").send_keys("admin")
# 暂停3s
time.sleep(3)
# 定位密码输入框, 输入 123
driver.find_element_by_xpath("//*[@id='passwordA']").send_keys("123")
time.sleep(5)
driver.close()
使用谷歌浏览器获取 XPath 表达式的过程:
元素上右键 -> 检查
在F12对应的文档中的对应元素上 右键 -> Copy -> Copy XPath 或者 Copy full XPath
使用函数
不使用函数时:
//*[@id='xxx']
使用函数后
//*[text()='xxx'] 文本内容是 xxx 的元素
//*[contains(@attribute, 'xxx')] 属性中含有 xxx 值的元素
//*[starts-with(@attribute, 'xxx')] 属性以xxx开头的元素
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 利用元素属性通过XPath 定位用户名输入框, 并输入 admin
# driver.find_element_by_xpath("//*[@name='userA']").send_keys("admin")
# driver.find_element_by_xpath("//*[@id='userA']").send_keys("admin")
# driver.find_element_by_xpath("//*[@placeholder='请输入用户名']").send_keys("admin")
driver.find_element_by_xpath("//*[@type='text']").send_keys("admin")
time.sleep(5)
driver.close()
2.1.8 CSS选择器
常用的定位方式
- id选择器
- class选择器
- 元素选择器
- 属性选择器
- 层级选择器
方法
element = driver.find_element_by_css_selector(css表达式)
id
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位方式中的id选择器, 定位用户名输入框, 并输入 admin
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_css_selector("#userA").send_keys("admin")
time.sleep(5)
driver.close()
class
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位方式中的class选择器, 定位电话号码输入框, 并输入 13100000000
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_css_selector(".telA").send_keys("13100000000")
time.sleep(5)
driver.close()
元素选择器
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位方式中的元素选择器, 定位注册按钮, 并点击
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_css_selector("button").click()
time.sleep(5)
driver.close()
属性选择器
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位方式中的属性选择器, 定位密码输入框, 并输入 123456
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
driver.find_element_by_css_selector("[type='password']").send_keys("123456")
time.sleep(5)
driver.close()
层级选择器
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位方式中的层级选择器, 定位用户名输入框, 并输入 admin
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# driver.find_element_by_css_selector("p[id='pa']>input").send_keys("admin")
driver.find_element_by_css_selector("div[class='zc'] input").send_keys("admin")
time.sleep(5)
driver.close()
CSS扩展
- input[type^=‘p’] type属性以p字母开头的元素
- input[type$=‘d’] type属性以d字母结束的元素
- input[type*=‘w’] type属性包含w字母的元素
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面, 使用CSS定位扩展的方式, 定位用户名输入框, 并输入 admin
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# driver.find_element_by_css_selector("input[type^='t']").send_keys("admin")
# driver.find_element_by_css_selector("input[name^='u']").send_keys("admin")
# driver.find_element_by_css_selector("input[type$='t']").send_keys("admin")
driver.find_element_by_css_selector("input[type*='ex']").send_keys("admin")
time.sleep(5)
driver.close()
2.1.9 Xpath和CSS区别
XPath和CSS对比
通过标签名定位
XPath
//input
CSS
input
通过id属性定位
XPath
//*[@id='userA']
CSS
#userA
通过class属性定位
XPath
//*[@class='telA']
CSS
.telA
通过其他属性定位
XPath
//*[starts-with(@type,'x')]
以x字母开头的type值的元素
//*[contains(@type, 'x')]
包含x字母的type值的元素
//*[text()='x']
文本内容为 x 的元素
CSS
[type^='x']
以x字母开头的type值的元素
[type*='x']
包含x字母的type值的元素
[type$='x']
以x字母结尾的type值的元素
2.1.10 元素定位分类
- id, name, class_name: 元素属性定位
- tag_name: 元素标签名定位
- link_text, partial_link_text: 通过文本定位超链接
- XPath: 通过路径定位元素
- CSS: 使用CSS选择器定位
2.1.11 元素定位的另一种写法
方法
方法: driver.find_element(方式, 值)
备注:
- 需要2个参数, 第1个参数为定位的类型(由By提供), 第2个参数传入具体的值
- 如果要使用By, 需要导包
示例
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 八中定位方法都适用"另一种方法"
# driver.find_element(By.ID, "userA").send_keys("admin")
driver.find_element(By.XPATH, "//*[@placeholder='请输入电子邮箱']").send_keys("123456@qq.com")
time.sleep(5)
driver.close()
2.2 元素操作
方法
click()
单击元素
send_keys()
模拟输入
clear()
清除文本
案例
需求:打开注册A页面,完成以下操作
-
通过脚本执行输入用户名:admin;密码:123456;电话号码:18611111111;电子邮件:123@qq.com
-
间隔3秒,修改电话号码为:18600000000
-
间隔3秒,点击‘注册’按钮
-
间隔3秒,关闭浏览器
注意:元素定位方法不限
代码
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
# 打开注册A页面,完成以下操作
# 1.通过脚本执行输入用户名:admin;密码:123456;电话号码:18611111111;电子邮件:123@qq.com
# 2.间隔3秒,修改电话号码为:18600000000
# 3.间隔3秒,点击‘注册’按钮
# 4.间隔3秒,关闭浏览器
# ps: 元素定位方法不限
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1
driver.find_element_by_id("userA").send_keys("admin")
driver.find_element_by_id("passwordA").send_keys("123456")
driver.find_element_by_id("telA").send_keys("18611111111")
driver.find_element_by_name("emailA").send_keys("123@qq.com")
# 2
time.sleep(3)
driver.find_element_by_id("telA").clear()
driver.find_element_by_id("telA").send_keys("18600000000")
# 3
time.sleep(3)
driver.find_element_by_css_selector("body > div > fieldset > form > p:nth-child(5) > button").click()
# 4
time.sleep(3)
driver.close()
2.3 浏览器操作
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
## maximize_window() 浏览器窗口最大化
driver.maximize_window()
## set_window_size() 设置窗口大小(单位:像素点) set_window_position() 设置窗口的位置
driver.set_window_size(300, 300)
driver.set_window_position(300, 300)
## back() 后退 forward() 前进 refresh() 刷新
driver.back()
driver.forward()
time.sleep(3)
driver.refresh()
## title 获取页面标题 current_url 获取当前页面url
print("页面标题:", driver.title)
print("当前页面地址:", driver.current_url)
## driver.close() 关闭当前浏览器窗口 ==> 执行结果, 留下了新浪网站, 关闭了注册A页面
time.sleep(3)
driver.find_element_by_link_text("访问 新浪 网站").click()
time.sleep(3)
driver.close()
### 序号 30~48 的脚本应该使用 driver.quit() 关闭浏览器驱动 而不是 driver.close()
## driver.quit() 关闭浏览器驱动对象(关闭浏览器) ==> 执行结果, 关闭所有窗口, 关闭浏览器驱动
time.sleep(3)
driver.find_element_by_link_text("访问 新浪 网站").click()
time.sleep(3)
driver.quit()
2.4 获取元素信息
应用场景
用于校验, 判断定位的元素是否准确
常用方法
size
返回元素大小
text
获取元素文本
get_attribute("xxx")
获取属性值, 参数是元素的属性名
is_displayed()
判断元素是否可见
is_enabled()
判断元素是否可用
is_selected()
判断元素是否选中, 用来检查复选框或单选按钮
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 需求: 打开A页面, 完成以下操作:
# 1.获取用户名输入框的大小
print(driver.find_element_by_id("userA").size)
# 2.获取页面上第一个超链接的文本内容
print(driver.find_element_by_tag_name("a").text)
# 3.获取页面上第一个超链接的地址
print(driver.find_element_by_tag_name("a").get_attribute("href"))
# 4.判断页面中的span标签是否可见
print(driver.find_element_by_tag_name("span").is_displayed())
# 5.判断页面中的取消按钮是否可用
print(driver.find_element_by_id("cancelA").is_enabled())
# 6.判断页面中的'旅游'对应的复选框是否为选中状态
print(driver.find_element_by_id("lyA").is_selected())
time.sleep(3)
driver.quit()
2.5 鼠标操作
什么是鼠标操作
单击, 右击, 双击, 悬停, 拖拽等
为什么要用到鼠标操作
现在web产品中存在丰富的鼠标交互方式, 作为一个web自动化测试框架, 需要应对这些鼠标操作的场景
2.5.1 常用方法
说明: 在Selenium中将鼠标操作的方法封装在 ActionChains 类中
实例化对象: action = ActionChains(driver)
方法:
context_click(element) 右击
double_click(element) 双击
move_to_element(element) 悬停
drag_and_drop(source, target) 拖拽
perform() 执行
2.5.2 执行的方法
说明: 在 ActionChains 类中所有提供的鼠标事件方法, 在调用的时候, 所有行为都存储在 ActionChains 对象中, 而 perform() 方法就是真正去执行所有的鼠标事件
强调: 必须调用 perform() 方法才能执行鼠标事件
2.5.3 鼠标右击
说明: 对于点击鼠标右键, 如果弹出的是浏览器的默认菜单, Selenium并没有提供操作菜单的方法
如果是自定义的右键菜单, 则可以通过元素定位来操作菜单中的选项
需求: 打开A页面, 在用户名文本框上点击鼠标右键
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
driver = webdriver.Chrome()
# 需求: 打开A页面, 在用户名文本框上点击鼠标右键
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 定位用户名输入框
element = driver.find_element_by_id("userA")
# 执行右键点击操作
action = ActionChains(driver)
action.context_click(element).perform()
time.sleep(3)
driver.quit()
2.5.4 鼠标双击
说明: 模拟鼠标双击左键的操作
需求: 打开A页面, 输入用户名 admin, 暂停3s, 双击鼠标左键(选中admin)
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
driver = webdriver.Chrome()
# 需求: 打开A页面, 输入用户名 admin, 暂停3s, 双击鼠标左键(选中admin)
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
element = driver.find_element_by_id("userA")
element.send_keys("admin")
time.sleep(3)
action = ActionChains(driver)
action.double_click(element).perform()
time.sleep(3)
driver.quit()
2.5.5 鼠标悬停
说明: 模拟鼠标悬停在指定元素上
需求: 打开A页面, 模拟鼠标悬停在 注册 按钮上
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
driver = webdriver.Chrome()
# 需求: 打开A页面, 模拟鼠标悬停在 注册 按钮上
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
element = driver.find_element_by_tag_name("button")
action = ActionChains(driver)
action.move_to_element(element).perform()
time.sleep(3)
driver.quit()
2.5.6 鼠标拖动
说明: 模拟鼠标拖动动作, 选定拖动源元素释放到目标元素
- 源元素
source = driver.find_element_by_xxx("xxx")
- 目标元素
target = driver.find_element_by_xxx("xxx")
- 调用方法
action.drag_and_drop(source, target).perform()
需求: 打开 drag.html 页面, 把红色方框拖动到蓝色方框上
import time
from selenium import webdriver
from selenium.webdriver import ActionChains
driver = webdriver.Chrome()
# 需求: 打开 drag.html 页面, 把红色方框拖动到蓝色方框上
driver.get("file:///C:/Users/57769/Desktop/pagetest/drag.html")
red = driver.find_element_by_id("div1")
blue = driver.find_element_by_id("div2")
ActionChains(driver).drag_and_drop(red, blue).perform()
time.sleep(3)
driver.quit()
2.6 键盘操作
说明:
- 模拟键盘上的一些按键或者组合键的输入, 如: 复制/粘贴
- Selenium中把键盘的按键都封装在 Keys 类中
2.6.1 常用操作
导包
send_keys(Keys.BACK_SPACE)
删除键(Backspace)send_keys(Keys.SPACE)
空格键(Space)send_keys(Keys.TAB)
制表键(Tab)send_keys(Keys.ESCAPE)
回退键(ESC)send_keys(Keys.ENTER)
回车键(Enter)send_keys(Keys.CONTROL, 'a')
全选(Ctrl + A)send_keys(Keys.CONTROL, 'c')
复制(Ctrl + C)
提示: 以上方法很多, 不会逐一讲解, 因为调用方法都一样
2.6.2 键盘操作
需求
打开 A 页面, 完成以下操作
- 输入用户名 admin1, 暂停2s, 删除1
- 全选用户名 admin 暂停2s
- 复制用户名 admin 暂停2s
- 粘贴到电话输入框
代码
import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1. 输入用户名 admin1, 暂停2s, 删除1
element = driver.find_element_by_id("userA")
element.send_keys("admin1")
time.sleep(2)
element.send_keys(Keys.BACK_SPACE)
# 2. 全选用户名 admin 暂停2s
element.send_keys(Keys.CONTROL, "a")
time.sleep(2)
# 3. 复制用户名 admin 暂停2s
element.send_keys(Keys.CONTROL, "c")
time.sleep(2)
# 4. 粘贴到电话输入框
driver.find_element_by_id("telA").send_keys(Keys.CONTROL, "v")
time.sleep(5)
driver.quit()
2.6.3 元素等待
概念
定位页面元素, 如果未找到, 在指定时间内一直等待的过程
分类
- 隐式等待
- 显式等待
由于一些原因, 我们想找的元素并没有立刻出来, 此时直接定位会报错, 场景如下:
- 网络速度慢
- 服务器计算慢
- 硬件配置差
思考: 是否定位每个元素时, 都需要元素等待?
2.6.4 隐式等待
方法
隐式等待为全局设置 (只需要设置1次,会作用于所有元素)
参数:
timeout
: 超时的时长, 单位: 秒
driver.implicitly_wait(timeout)
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 需求: 打开A页面, 使用隐式等待定位 "延时加载的输入框", 并输入 admin
driver.implicitly_wait(10)
driver.find_element_by_css_selector("input[placeholder='延时加载的输入框']").send_keys("admin")
time.sleep(3)
driver.quit()
# 不使用元素等待时, 如果找不到元素会报 NoSuchElementException 异常
# 使用隐式等待时, 如果找不到元素会报 NoSuchElementException 异常
注意点
单个元素定位超时会报 NoSuchElementException
2.6.5 显式等待
说明: 在Selenium中把显式等待的相关方法封装在 WebDriverWait 类中
方法 :
显式等待, 为定位不同的元素的超时时间设置不同的值
-
导包
-
WebDriverWait(driver, timeout, poll_frequency=0.5)
- driver: 浏览器驱动对象
- timeout: 超时时长, 单位: 秒
- poll_frequency: 检测的间隔时间, 默认为0.5s
-
调用 until(method)
- method: 函数名称, 该函数用来实现元素定位
- 一般使用匿名来实现:
lambda x: x.find_element_by_xxx("xxx")
如:
element = WebDriverWait(driver,10,1).until(lambda x: x.find_element_by_xxx("xxx"))
案例
import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
driver = webdriver.Chrome()
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 需求: 打开A页面, 使用显式等待定位 "延时加载的输入框", 并输入 admin
wait = WebDriverWait(driver, 10, 1)
element = wait.until(lambda x: x.find_element_by_css_selector("input[placeholder='延时加载的输入框']"))
element.send_keys("admin")
time.sleep(3)
driver.quit()
# 单个元素定位超时会报错 TimeoutException
注意点
单个元素定位超时会报错
TimeoutException
2.6.6 隐式和显式区别
- 作用域: 隐式等待为全局有效, 显式等待为单个元素有效
- 使用方法: 隐式等待直接通过驱动对象调用, 而显式等待方法封装在
WebDriverWait
类中 - 达到最大超时时长后抛出异常不同: 隐式等待为
NoSuchElementException
, 显式等待为TimeoutException
2.6.7 下拉框
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面,完成以下下拉框操作
# 1. 暂停2s, 选择广州
# 2. 暂停2s, 选择上海
# 3. 暂停2s, 选择北京
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1
time.sleep(2)
driver.find_element_by_xpath("//*[@id='selectA']/option[3]").click()
# 2
time.sleep(2)
driver.find_element_by_xpath("//*[@id='selectA']/option[2]").click()
# 3
time.sleep(2)
driver.find_element_by_xpath("//*[@id='selectA']/option[1]").click()
time.sleep(3)
driver.quit()
案例
说明: Select类是Selenium为操作select标签封装的
实例化对象:
select = Select(element)
element: <select>标签对应的元素, 通过元素定位方式获取
例如: driver.find_element_by_id("selectA")
操作方法:
select_by_index(index)
根据option索引来定位, 从0开始select_by_value(value)
根据option属性 value值来定位select_by_visible_text(text)
根据option显示文本内容来定位
步骤分析
- 导包
- 实例化Select类
select = Select(driver.find_element_by_id("selectA"))
- 调用方法
案例
import time
from selenium import webdriver
from selenium.webdriver.support.select import Select
driver = webdriver.Chrome()
# 需求: 打开A页面,完成以下下拉框操作
# 1. 暂停2s, 选择广州
# 2. 暂停2s, 选择上海
# 3. 暂停2s, 选择北京
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
select = Select(driver.find_element_by_id("selectA"))
# 1
time.sleep(2)
select.select_by_index(2)
# 2
time.sleep(2)
select.select_by_value("sh")
# 3
time.sleep(2)
select.select_by_visible_text("北京")
time.sleep(3)
driver.quit()
2.6.8 弹出框分类
- alert 警告框
- confirm 确认框
- prompt 提示框
弹出框的错误示范
错误代码
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面,完成以下弹出框操作
# 1.点击 alert 按钮
# 2.暂停2s, 输入用户名 admin
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1
driver.find_element_by_id("alerta").click()
# 2
time.sleep(2)
driver.find_element_by_id("userA").send_keys("admin")
time.sleep(3)
driver.quit()
# 思考
# 1.什么问题导致的?
# driver的焦点在弹出框页面, 并不在A页面, 无法为你输入admin(找不到用户名输入框)
# 2.如何处理弹出框?
弹出框方法
说明: Selenium中对弹出框的处理, 有专用的方法, 且处理的方法都一样(alert/confirm/prompt)
1.获取弹出框对象
alert = driver.switch_to.alert
2.调用
alert.text
返回alert/confirm/prompt文字信息
alert.accept()
接受对话框选项(确认)
alert.dismiss()
取消对话框选项(取消)
案例
正确代码
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面,完成以下弹出框操作
# 1.点击 alert 按钮
# 2.暂停2s, 输入用户名 admin
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1
driver.find_element_by_id("alerta").click()
time.sleep(2)
alert = driver.switch_to.alert
print(alert.text)
time.sleep(2)
alert.accept()
# 2
time.sleep(2)
driver.find_element_by_id("userA").send_keys("admin")
time.sleep(3)
driver.quit()
2.7 滚动条实现方法
方法
说明: Selenium中没有提供滚动条的操作方法, 但是它提供了执行 JS 的方法, 所有我们可以通过 JS脚本来操作滚动条
- 设置 JS 脚本控制滚动条
js = "window.scrollTO(0,1000)"
(0:左边距, 1000:上边距 单位:像素(px)) - Selenium 调用执行 JS 脚本的方法
driver.execute_script(js)
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开A页面
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# js1 滚动到最底部
js1 = "window.scrollTo(0, 10000)"
# js2 滚动到最顶部
js2 = "window.scrollTo(0, 0)"
# 执行第一个脚本
time.sleep(2)
driver.execute_script(js1)
# 执行第二个脚本
time.sleep(2)
driver.execute_script(js2)
time.sleep(3)
driver.quit()
2.8 frame切换
概念
frame : html页面中的一种框架, 主要作用是在当前页面指定区域显示另一个页面元素
形式一:
<frameset cols="25%,75%">
<frame src="a.html">
<frame src="b.html">
</frameset>
形式二:
<iframe name="iframe_a" src="demo.html" width="200" height="200"></iframe>
方法
说明: 在Selenium中封装了如何切换frame框架的方法
步骤:
1.driver.switch_to.frame(frame_reference)
切换到指定frame
frame_reference
: 可以传frame框架的id,name,定位的frame元素
2.driver.switch_to.default_content()
恢复默认页面
必须回到默认页面才能进一步操作
解决方案
- 在主页面输入用户名 admin
- 切换到A页面, 再输入用户名 adminA
- 恢复默认页面
- 切换到B页面, 再输入用户名 adminB
正确代码
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开"注册实例"页面
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8C%E5%AE%9E%E4%BE%8B.html")
# 1.填写主页面的用户名 admin
time.sleep(2)
driver.find_element_by_id("userA").send_keys("admin")
# 2.填写注册页面A中的用户名 adminA
time.sleep(2)
# driver.switch_to.frame("idframe1") # 从主页面, 切换到了A页面, 通过 id
# driver.switch_to.frame("myframe1") # 从主页面, 切换到了A页面, 通过 name
driver.switch_to.frame(driver.find_element_by_id("idframe1")) # 从主页面, 切换到了A页面, 通过 定位到的元素
driver.find_element_by_id("userA").send_keys("adminA")
# 3.回到主页面
time.sleep(1)
driver.switch_to.default_content()
# 4.填写注册页面B中的用户名 adminB
time.sleep(1)
driver.switch_to.frame("idframe2") # 从主页面, 切换到B页面
driver.find_element_by_id("userA").send_keys("adminB")
time.sleep(3)
driver.quit()
2.8.1 多窗口切换
概念
什么是窗口? 窗口类似于浏览器中的标签页, 每个窗口就对应了一个标签页
为什么要切换窗口? 在html页面中, 当点击按钮或超链接时, 有的会在新窗口打开页面
如果点击按钮或超链接在当前窗口打开新页面, 就不需要切换窗口
需求
打开A页面
- 在新窗口打开新浪页面
- 在新浪的搜索框输入"新浪搜索"
- 在A页面输入用户名 admin
方法
说明: 在Selenium中封装了获取当前窗口句柄,获取所有窗口句柄和切换到指定句柄窗口的方法
句柄: 英文handle, 窗口的唯一识别码
方法:
1. `driver.current_window_handle` 获取当前窗口句柄
2. `driver.window_handles` 获取所有窗口句柄
3. `driver.switch_to.window(handle)` 切换到指定句柄的窗口
对于需求的解决方案:
- 打开A页面, 获取当前窗口句柄(拿到的是A页面的句柄)
- 在A页面点击"访问 新浪 网站" 这个超链接, 获取所有窗口句柄
- 根据句柄, 切换到新浪窗口, 对输入框输入 “新浪搜索”
- 切换回原本窗口(A页面), 输入用户名 admin
注意: 新浪页面需要访问网络, 可能加载慢, 可能需要用到元素等待
代码
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 隐式等待10秒, 以防新浪窗口加载慢, 定位不到输入框
driver.implicitly_wait(10)
# 1. 打开A页面, 获取当前窗口句柄(拿到的是A页面的句柄)
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
print("当前A页面窗口句柄:", driver.current_window_handle)
# 2. 在A页面点击"访问 新浪 网站" 这个超链接, 获取所有窗口句柄
driver.find_element_by_id("fw").click()
handles = driver.window_handles
print("所有窗口句柄:", handles)
# 3. 根据句柄, 切换到新浪窗口, 对输入框输入 "新浪搜索"
driver.switch_to.window(handles[1])
time.sleep(1)
driver.find_element_by_class_name("inp-txt").clear()
time.sleep(1)
driver.find_element_by_class_name("inp-txt").send_keys("新浪搜索")
time.sleep(2)
# 4. 切换回原本窗口(A页面), 输入用户名 admin
driver.switch_to.window(handles[0])
driver.find_element_by_id("userA").send_keys("admin")
time.sleep(3)
driver.quit()
2.8.2 窗口截图
概念
什么是窗口截图?
把当前操作的页面, 截图保存到指定的位置
为什么要窗口截图?
有时候打印的错误信息不十分准确, 需要窗口截图辅助定位错误
方法
说明: 在Selenium中提供了截图方法, 我们只需要调用即可
方法:
driver.get_screenshot_as_file(imgpath)
imgpath: 图片保存路径 + 图片名
案例
import time
from selenium import webdriver
driver = webdriver.Chrome()
# 需求: 打开 A 页面, 完成以下操作
# 1.输入用户名 admin
# 2.截图保存
driver.get("file:///C:/Users/57769/Desktop/pagetest/%E6%B3%A8%E5%86%8CA.html")
# 1
driver.find_element_by_id("userA").send_keys("admin")
# 2
time.sleep(1)
# 每次都是用固定文件名, 会股改上一次生成的图片文件
# driver.get_screenshot_as_file("./png/123.png") # 需要提前创建 png 目录
# 使用时间去格式化文件名, 可以使每次截图保存的文件名都不同, 不会覆盖之前保存的文件, 更有效
imgpath = "./png/test_{}.png".format(time.strftime("%Y%m%d%H%M%S"))
driver.get_screenshot_as_file(imgpath)
time.sleep(3)
driver.quit()
2.9 验证码处理
概念
什么是验证码?
一种随机生成的信息 (数字, 字母, 汉字, 图片, 算术题…) 等为了防止恶意的请求行为, 增加应用的安全性
为什么要学习验证码?
在web应用中, 大部分系统在用户登录注册的时候都需要输入验证码, 而我们自动化脚本也要面临处理验证码的问题
常用方法
说明: Selenium中并没有对验证码处理的方法, 在这里我们介绍几种常用的处理方式
方法:
1.去掉验证码
(测试环境下采用)
2.设置万能验证码
(生产和测试环境下采用)
3.验证码识别技术
(通过 python-tesseract 来识别图片类型的验证码: 识别率很难达到100%)
4.记录 cookie
(通过记录 cookie 进行跳过登录)
注意
1 和 2, 都是开发人员来完成
3 验证码识别技术成功率不高, 不太合适
4 记录cookie 比较实用, 推荐
2.10 cookie
概念
- cookie是由web服务器生成的, 并且保存在用户浏览器上的小文本文件, 它可以包含用户信息
- cookie数据格式: 键值对 (python中的字典)
- cookie产生: 客户端请求服务器, 如果服务器需要记录该用户状态, 就向客户端浏览器颁发一个cookie数据
- cookie使用: 当浏览器再次请求该网站时, 浏览器把请求的数据和cookie数据一同提交给服务器, 服务器检查该cookie, 以此来辨认用户
应用场景
- 实现会话跟踪, 记录用户登录状态
- 实现记住密码和自动登录的功能
- 用户未登录状态下, 记录购物车中的商品
方法
说明: Selenium中对cookie操作提供相应的方法
方法:
1.driver.get_cookies()
获取本网站所有本地cookies
2.driver.get_cookie(name)
获取指定cookie
name: 为cookie中键值对数据的 键名
3.driver.add_cookie(cookie_dict)
添加cookie
cookie_dict: 一个字典对象, 必选的内容包括: “name” 和 “value”
案例需求
使用cookie 实现跳过百度登录
1.手动登录百度, 获取cookie
2.请求百度, 并且带上cookie
步骤分析
BDUSS是登录百度后的唯一身份凭证, 拿到BDUSS就等于拿到了账号的控制权,通行贴吧,知道,文库…主要产品
- 登录百度, 抓取BDUSS
- 添加 BDUSS 的键值对
- 调用刷新的方法
代码
import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.maximize_window()
# 需求: 使用cookie 实现跳过百度登录
# 1.手动登录百度, 获取cookie
# 2.请求百度, 并且带上cookie
# 没有cookie的时候
driver.get("http://www.baidu.com")
# 添加cookie操作
driver.add_cookie({"name": "BDUSS", "value": "VZMUEl0WFJQYkxNSXk0c0VMUk5ZNGYteWVYNG01aVJtZXFCV056alk5M3V3SUZlSVFBQUFBJCQAAAAAAAAAAAEAAAC2KUFmTFhKX0pheQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO4zWl7uM1peQ"})
time.sleep(3)
# 刷新, 再次请求百度首页, 验证是否带上身份信息
driver.refresh()
time.sleep(3)
driver.quit()
3 Pytest框架
3.1 总体介绍
什么是断言
让程序代替人工去判断测试程序的执行结果是否符合预期的过程
为什么学习断言
自动化脚本在执行的时候一般都是无人值守的状态, 我们不知道执行结果是否符合预期, 所以我们需要让程序代替人工去检测程序的执行结果是否符合预期, 这就需要断言
3.2 断言方法
assert xx
判断 xx 为真
assert not xx
判断 xx 不为真
assert a in b
判断 b 包含 a
assert a == b
判断 a 等于 b
assert a != b
判断 a 不等于 b
代码案例
def add(x, y):
return x + y
class TestPlus:
# 判断 1+1 的结果等于 2
def test_a(self):
assert 2 == add(1, 1)
# 调换表达式两个值的位置, 判断 1+1 的结果等于 2
def test_b(self):
assert add(1, 1) == 2
# 判断 1+2 的结果不等于4
def test_c(self):
assert 4 != add(1, 2)
# 误判: 1+2 等于 4 了
def test_d(self):
assert 4 == add(1, 2)
3.3 setup和teardown
应用场景
pytest 在运行自动化脚本的前后会执行两个特殊的方法, 分别是"前置"和"后置"方法
在脚本执行前会执行"前置"方法,在脚本执行后会执行"后置"方法
概念和方法
1.初始化(前置处理方法):
def setup(self)
2.销毁(后置处理方法):
def teardown(self)
3.运行于测试方法的始末, 即:运行一次测试方法就会运行一次 setup 和 teardown
案例
import time
def add(x, y):
return x + y
class TestPlus:
# 获取并打印开始时间, 每个测试函数执行前都打印一次
def setup(self):
print("start-time:", time.time())
# 获取并打印结束时间, 每个测试函数执行后都打印一次
def teardown(self):
print("end-time:", time.time())
def test_a(self):
assert 2 == add(1, 1)
def test_b(self):
assert add(1, 1) == 2
3.4 配置文件
应用场景
使用配置文件, 可以通过配置项来选择执行哪些目录下的哪些测试模块
用法
步骤:
- 新建 scripts 模块, 测试脚本放到模块中
- 新建 pytest.ini 文件, 名称为 pytest.ini, 第一行为 [pytest], 并且补全配置项
- 命令行运行 pytest 即可
示例
pytest.ini
[pytest]
addopts = -s
testpaths = ./scripts
python_files = test_*.py
python_classes = Test*
python_functions = test_*
3.5 测试报告插件
应用场景
需要测试报告来体现自动化脚本测试是否通过
安装
pip install pytest-html==1.21.1
使用
在配置文件中的命令行参数中, 增加 --html=用户路径/report.html
生成报告
步骤:
- 命令行输入 pytest 运行脚本
- 在项目目录下会有一个 report文件夹, 里面有个 report.html 就是测试报告
3. 6 数据参数化
应用场景
需要测试多组值得时候, 使用数据参数化可以使代码更简洁, 可读性更好
方法
数据参数化, 装饰器需要放在要传多组值的函数上
@pytest.mark.parametrize(argnames, argvalues)
参数:
argnames
: 参数名
argvalues
: 参数对应值, 类型必须是可迭代类型, 一般使用 list
3.6.1 单一参数
代码
import pytest
class TestDemo:
# 需求: 不使用数据参数化, 分别打印用户名 "zhangsan" 和 "lisi"
def test_a(self):
print("zhangsan")
def test_b(self):
print("lisi")
# 需求: 使用数据参数化 (单一参数), 修改上面的代码
@pytest.mark.parametrize("name", ["zhangsan", "lisi"])
def test_c(self, name):
print(name)
3.6.2 多个参数
代码
import pytest
class TestDemo:
# 需求: 使用数据参数化 (多个参数), 分别打印2组账号和密码: zhangsan / 111111 和 lisi / 222222
@pytest.mark.parametrize(("username", "password"), [("zhangsan", "111111"), ("lisi", "222222")])
def test_c(self, username, password):
print(username + "-----" + password)
# 使用元组可以传多个值 ("zhangsan", "111111"), 列表行不行?
3.6.3 推荐用法
代码
import pytest
class TestDemo:
# 需求: 使用数据参数化 (推荐用法), 分别打印2组账号和密码: zhangsan / 111111 和 lisi / 222222
# @pytest.mark.parametrize(("username", "password"), [("zhangsan", "111111"), ("lisi", "222222")])
# def test_c(self, username, password):
# print(username + "-----" + password)
@pytest.mark.parametrize("dict", [{"username": "zhangsan", "password": "111111"}, {"username": "lisi", "password": "222222"}])
def test_d(self, dict):
print(dict)
print(dict["username"])
print(dict["password"])
#("zhangsan", "111111", "13000000000", "1", "1", "30", "......")
#("lisi", "222222", "13100000000", ??????)
# 推荐的用法是用字典表示参数值
# {"username": "zhangsan", "password": "111111"}
4 PO模式
4.1 递进学习路线
- v1: 不使用任何设计模式和单元测试框架
- v2: 使用 pytest 管理用例
- v3: 使用方法封装的思想, 对代码进行优化
- v4: 采用PO模式的分层思想对代码进行拆分, 分离page
- v5: 对PO分层后的代码继续优化, 分离page中的元素和操作
- v6: PO模式深入封装, 把共同操作提取封装
4.2 无模式
4.2.1 案例说明
对 TPshop 项目的登录模块进行自动化测试
登录模块包含了很多测试用例, 如: 账号不存在, 密码错误, 验证码错误, 登录成功等等
为了节省时间, 我们只选取几个有代表性的用例来演示: 账号不存在, 密码错误
4.2.2 选择测试用例
-
账号不存在
- 点击首页的"登录"链接, 进入登录页面
- 输入一个不存在的用户名
- 输入密码
- 输入验证码
- 点击登录按钮
- 获取错误提示信息
-
密码错误
-
点击首页的"登录"链接, 进入登录页面
-
输入用户名
-
输入一个错误的密码
-
输入验证码
-
点击登录按钮
-
获取错误提示信息
-
4.3 V1版本
- 不使用任何设计模式和单元测试框架
- 每个文件对应编写一个测试用例, 完全的面向过程的编程方式
示例代码
- 登录功能, 账号不存在
# 账号不存在
import time
from selenium import webdriver
# 实例化浏览器驱动
driver = webdriver.Chrome()
driver.maximize_window()
driver.implicitly_wait(10)
driver.get("http://localhost/")
# 1. 点击首页的"登录"链接, 进入登录页面
driver.find_element_by_class_name("red").click()
# 2. 输入一个不存在的用户名
driver.find_element_by_id("username").send_keys("18800000000")
# 3. 输入密码
driver.find_element_by_id("password").send_keys("123456")
# 4. 输入验证码
driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 关闭浏览器驱动
time.sleep(5)
driver.quit()
- 登录功能, 密码错误
# 密码错误
import time
from selenium import webdriver
# 实例化浏览器驱动
driver = webdriver.Chrome()
driver.maximize_window()
driver.implicitly_wait(10)
driver.get("http://localhost/")
# 1. 点击首页的"登录"链接, 进入登录页面
driver.find_element_by_class_name("red").click()
# 2. 输入用户名
driver.find_element_by_id("username").send_keys("17150312012")
# 3. 输入一个错误密码
driver.find_element_by_id("password").send_keys("error")
# 4. 输入验证码
driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 关闭浏览器驱动
time.sleep(5)
driver.quit()
存在的问题
- 一条测试用例对应一个文件, 用例多时, 不方便维护管理
- 代码高度冗余
4.4 V2版本
引入pytest管理测试用例, 并断言用例的执行结果
好处
- 方便组织和管理多个测试用例
- 提供了丰富的断言方法
- 方便生成测试报告
- 减少了代码冗余
示例代码
# 导包
import time
from selenium import webdriver
# 定义测试类
class TestLogin:
def setup(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
self.driver.implicitly_wait(10)
self.driver.get("http://localhost/")
def teardown(self):
time.sleep(5)
self.driver.quit()
# 定义用户不存在的测试方法
def test_login_account_not_exist(self):
# 1. 点击首页的"登录"链接, 进入登录页面
self.driver.find_element_by_class_name("red").click()
# 2. 输入一个不存在的用户名
self.driver.find_element_by_id("username").send_keys("18800000000")
# 3. 输入密码
self.driver.find_element_by_id("password").send_keys("123456")
# 4. 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
self.driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 断言
assert "账号不存在!" == msg
# 定义密码错误的测试方法
def test_login_password_error(self):
# 1. 点击首页的"登录"链接, 进入登录页面
self.driver.find_element_by_class_name("red").click()
# 2. 输入用户名
self.driver.find_element_by_id("username").send_keys("17150312012")
# 3. 输入一个错误密码
self.driver.find_element_by_id("password").send_keys("error")
# 4. 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
self.driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 断言
assert "密码错误!" == msg
存在问题
依然是代码冗余
4.4.1 方法封装
概念
是将一些有共性的或多次被使用的代码提取到一个方法中, 供其他地方调用
好处
- 避免代码冗余
- 容易维护
- 隐藏代码实现的细节
目的
用最少的代码实现最多的功能
4.5 V3版本
驱动工具类
# 获取/关闭浏览器驱动的类
from selenium import webdriver
class DriverUtils:
__driver = None
# 获取浏览器驱动
@classmethod
def get_driver(cls):
if cls.__driver is None:
cls.__driver = webdriver.Chrome()
cls.__driver.maximize_window()
cls.__driver.implicitly_wait(10)
return cls.__driver
# 关闭浏览器驱动
@classmethod
def quit_driver(cls):
if cls.__driver is not None:
cls.__driver.quit()
cls.__driver = None
测试类
# 导包
import time
from v3.driver_utils_121 import DriverUtils
# 定义测试类
class TestLogin:
def setup(self):
self.driver = DriverUtils.get_driver()
self.driver.get("http://localhost/")
def teardown(self):
time.sleep(5)
DriverUtils.quit_driver()
# 定义用户不存在的测试方法
def test_login_account_not_exist(self):
# 1. 点击首页的"登录"链接, 进入登录页面
self.driver.find_element_by_class_name("red").click()
# 2. 输入一个不存在的用户名
self.driver.find_element_by_id("username").send_keys("18800000000")
# 3. 输入密码
self.driver.find_element_by_id("password").send_keys("123456")
# 4. 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
self.driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 断言
assert "账号不存在!" == msg
# 定义密码错误的测试方法
def test_login_password_error(self):
# 1. 点击首页的"登录"链接, 进入登录页面
self.driver.find_element_by_class_name("red").click()
# 2. 输入用户名
self.driver.find_element_by_id("username").send_keys("17150312012")
# 3. 输入一个错误密码
self.driver.find_element_by_id("password").send_keys("error")
# 4. 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 5. 点击登录按钮
self.driver.find_element_by_name("sbtbutton").click()
# 6. 获取错误提示信息
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
print(msg)
# 断言
assert "密码错误!" == msg
注意: 如果想要引用其他类, 那么被引用的类的文件名要符合要求, 比如不能出现 []
存在的问题
代码冗余
4.6 PO模式
在做UI自动化时, 元素定位特别依赖页面, 如果页面变更, 自动化脚本就需要被修改
存在的问题
- 如果前端工程师改了某个元素, 你就得修改所有对应的代码
- 存在大量的冗余
如果解决?
答案就是 PO模式
4.6.1 概念
PO是Page Object的缩写, PO模式是自动化测试开发的最佳设计模式之一
核心思想:
- 通过对页面元素的封装减少冗余代码, 同时在后期维护中, 若元素发生变化, 只需要调整页面元素封装的代码即可, 提高了测试用例的可维护性, 可读性
- 页面和测试脚本分离
4.6.2 PO模式分层
分层机制, 让不同层去做不同类型的事情, 让代码结构清晰, 增加复用性
分层方式
-
两层: 对象操作层 + 业务数据层
- 对象操作层: 封装页面信息, 包括元素以及元素的操作
- 业务数据层: 封装多种操作组合的业务以及测试数据
-
三层: 对象库 + 操作层 + 业务数据层 / 对象操作层 + 业务层 + 数据层
-
四层: 对象库 + 操作层 + 业务层 + 数据层
4.6.3 PO模式优点
- 引入PO模式前
- 存在大量冗余代码
- 业务流程不清晰
- 后期维护成大
- 引入PO模式后
- 减少冗余代码
- 业务代码和测试数据被分开, 降低耦合性
- 维护成本低
4.7 V4版本
介绍
采用PO模式的分层思想对代码进行拆分
PO封装
对登录页面进行封装: 封装到类 LoginPage
对测试用例进行封装: 封装到类 TestLogin
代码结构
-
utils包
- driver_utils.py
-
page包
- login_page.py
-
scripts包
- test_login.py
-
pytest.ini
PO封装
login_page.py
class LoginPage:
def __init__(self, driver):
self.driver = driver
# 点击首页的"登录"链接, 进入登录页面
def click_login_link(self):
return self.driver.find_element_by_class_name("red").click()
# 输入用户名
def input_username(self, username):
return self.driver.find_element_by_id("username").send_keys(username)
# 输入密码
def input_password(self, password):
return self.driver.find_element_by_id("password").send_keys(password)
# 输入验证码
def input_verify_code(self, code):
return self.driver.find_element_by_id("verify_code").send_keys(code)
# 点击登录按钮
def click_login_btn(self):
return self.driver.find_element_by_name("sbtbutton").click()
# 获取提示信息
def get_msg(self):
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
return msg
test_login.py
class LoginPage:
def __init__(self, driver):
self.driver = driver
# 点击首页的"登录"链接, 进入登录页面
def click_login_link(self):
return self.driver.find_element_by_class_name("red").click()
# 输入用户名
def input_username(self, username):
return self.driver.find_element_by_id("username").send_keys(username)
# 输入密码
def input_password(self, password):
return self.driver.find_element_by_id("password").send_keys(password)
# 输入验证码
def input_verify_code(self, code):
return self.driver.find_element_by_id("verify_code").send_keys(code)
# 点击登录按钮
def click_login_btn(self):
return self.driver.find_element_by_name("sbtbutton").click()
# 获取提示信息
def get_msg(self):
msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
return msg
4.8 v5版本
介绍
对PO分层后的代码继续优化
优化内容
- 分离page页面中的元素和操作
- 优化元素定位方式
示例代码
login_page.py
from selenium.webdriver.common.by import By
class LoginPage:
# 登录链接 按钮
login_link_btn = By.CLASS_NAME, "red"
# 用户名 输入框
username_input = By.ID, "username"
# 密码 输入框
password_input = By.ID, "password"
# 验证码 输入框
verify_code_input = By.ID, "verify_code"
# 登录 按钮
login_btn = By.NAME, "sbtbutton"
# 提示信息
msg_info = By.CSS_SELECTOR, ".layui-layer-content"
def __init__(self, driver):
self.driver = driver
def find_el(self, feature):
return self.driver.find_element(*feature)
# return self.driver.find_elment(feature[0], feature[1])
# 点击首页的"登录"链接, 进入登录页面
def click_login_link(self):
return self.find_el(self.login_link_btn).click()
# return self.driver.find_elment(self.login_link_btn[0], self.login_link_btn[1]).click()
# return self.driver.find_element_by_class_name("red").click()
# 输入用户名
def input_username(self, username):
return self.find_el(self.username_input).send_keys(username)
# return self.driver.find_element_by_id("username").send_keys(username)
# 输入密码
def input_password(self, password):
return self.find_el(self.password_input).send_keys(password)
# return self.driver.find_element_by_id("password").send_keys(password)
# 输入验证码
def input_verify_code(self, code):
return self.find_el(self.verify_code_input).send_keys(code)
# return self.driver.find_element_by_id("verify_code").send_keys(code)
# 点击登录按钮
def click_login_btn(self):
return self.find_el(self.login_btn).click()
# return self.driver.find_element_by_name("sbtbutton").click()
# 获取提示信息
def get_msg(self):
return self.find_el(self.msg_info).text
# msg = self.driver.find_element_by_css_selector(".layui-layer-content").text
# return msg
4.9 v6版本
介绍
把共同的方法进行封装
优化内容
- 封装操作基类
- 封装查找元素的方法
- 封装基本操作方法: 点击/ 清空/ 输入等等
- page继承操作基类
结构
-
utils包
- driver_utils.py
-
page包
- login_page.py
-
scripts包
- test_login.py
-
pytest.ini
-
base包
- base_action.py
4.9.1 示例代码
base_action.py
class BaseAction:
def __init__(self, driver):
self.driver = driver
def find_el(self, feature):
return self.driver.find_element(*feature)
def find_els(self, feature):
return self.driver.find_elements(*feature)
def click(self, feature):
return self.find_el(feature).click()
def input(self, feature, content):
return self.find_el(feature).send_keys(content)
def clear(self, feature):
return self.find_el(feature).clear()
注意: page页面要继承 BaseAction
5 数据驱动
概念
是以数据来驱动整个测试用例的执行, 也就是测试数据决定测试结果
特点
- 可以把数据驱动理解为一种模式或者一种思想
- 数据驱动技术可以让用户把关注点放在测试数据的构建和维护上, 而不是直接维护脚本, 可以利用同样的过程, 对不同的输入数据进行测试
- 数据驱动要依赖参数化技术
数据来源
- 直接定义在测试脚本中 (简单直观, 但测试方法和测试数据未分离, 不方便后期维护)
- 从文件中读取数据, 如 txt, excel, xml, JSON等格式文件
- 从数据库读取数据
5.1 JSON基本介绍
概念
JSON全称是" JavaScript Object Notation", 是JavaScript 对象表示法, 它是一种基于文本, 独立于语言的轻量级数据交换格式
特点
- JSON是纯文本
- JSON具有良好的自我描述性, 便于阅读和编写
- JSON具有清晰的层级结构
- 有效的提升网络传输效率
对比XML
- XML指可扩展标记语言, 被设计用来传输数据
- 如果使用XML, 需要读取XML, 然后通过标签结点来遍历文档, 并读取对应的值, 然后传输
- 使用JSON, 只需要读取JSON字符串
JSON语法规格
- 大括号保存对象
- 中括号保存数组
- 对象和数组可以相互嵌套
- 数据采用键值对来表示
- 多个数据用逗号分隔
JSON值
- 数字 (整数或者浮点数)
- 字符串 (在双引号中)
- 逻辑值 (true 或者 false)
- 数组 (在中括号中)
- 对象 (在大括号中)
- null
- JSON中空值用 null 表示
- python中对应的用 None 表示
JSON基本操作
操作内容
-
python字典与JSON之间的转换
-
JSON文件读写
在python中想要操作 JSON, 需要先导入依赖包
import json
5.2 字典与JSON转换
代码
import json
# 把python字典类型转换为JSON字符串
dict1 = {
"name": "zhangsan",
"age": 18,
"is_man": True,
"school": None
}
# 使用 dumps 方法, 得到的结果是 json 字符串
json_str1 = json.dumps(dict1)
print(json_str1)
# 把JSON字符串转换为python字典
json_str2 = '{"name": "zhangsan", "age": 18, "is_man": true, "school": null}'
# 使用 loads 方法, 得到的结果是 python字典
dict2 = json.loads(json_str2)
print(dict2)
把python字典类型转换为JSON字符串: 使用 dumps 方法
把JSON字符串转换为python字典: 使用 loads 方法
5.3 JSON文件读写
代码
import json
# 读取 data.json 文件
with open("data.json", "r", encoding="utf-8") as f:
data1 = json.load(f)
print(data1)
# 把字典写入json文件 "data2.json"
data2 = data1
with open("data2.json", "w", encoding="utf-8") as f:
json.dump(data2, f)
# 把字典写入json文件 "data3.json" ------解决写入中文的问题
data3 = data1
with open("data3.json", "w", encoding="utf-8") as f:
json.dump(data2, f, ensure_ascii=False)
实现步骤
- 编写测试用例
- 敲代码
- 采用PO模式的分层思想对页面进行封装
- 编写测试脚本
- 定义数据文件, 实现参数化
6 项目实战
在线计算器项目 http://cal.apple886.com/
6.1 项目结构
- base ----> 存储页面对象的父类(便于子类调用)
- data ----> 存储测试用例数据
- page ----> 存储页面对象
- scripts ----> 存储测试脚本
- utils ----> 存储经常使用的工具类
- pytest.ini ----> 运行项目的配置
6.2 base包
新建base_action.py
class BaseAction:
# 初始化驱动
def __init__(self, driver):
self.driver = driver
# 查找单个元素
def find_el(self, feature):
return self.driver.find_element(*feature)
# 查找多个元素
def find_els(self, feature):
return self.driver.find_elements(*feature)
# 查找按钮元素
def click(self, feature):
return self.find_el(feature).click()
# 查找输入元素
def input(self, feature, content):
return self.find_el(feature).send_keys(content)
# 清空
def clear(self, feature):
return self.find_el(feature).clear()
# 定位数字按钮
def find_el_num(self, feature, num):
# 将num格式化为字符串
return self.driver.find_element(feature[0], feature[1].format(str(num)))
6.3 data包
新建cal_data.json
{
"cal_001": {
"data": [1, 3],
"result": 4
},
"cal_002": {
"data": [1, 2],
"result": 3
},
"cal_002": {
"data": [1, 2, 3],
"result": 6
}
}
6.4 page包
新建cal_page.py
from selenium.webdriver.common.by import By
from base.base_action import BaseAction
class CalPage(BaseAction):
# 数字按钮
number_btn = By.ID, "simple{}"
# 加号按钮
add_btn = By.ID, "simpleAdd"
# 等号按钮
equal_btn = By.ID, "simpleEqual"
# 结果
result = By.ID, "resultIpt"
# 点击数字
def click_number_btn(self, num):
return self.find_el_num(self.number_btn, num).click()
# 点击加号
def click_add_btn(self):
return self.click(self.add_btn)
# 点击等于号
def click_equal_btn(self):
return self.click(self.equal_btn)
# 显示结果
def get_result(self):
return self.find_el(self.result).get_attribute("value")
6.5 scripts包
新建tesst_cal.py
# 导包
import time
import pytest
from page.cal_page import CalPage
from utils.driver_utils import DriverUtils
from utils.read_data import read_data
# 定义测试类
class TestCal:
def setup_method(self):
self.driver = DriverUtils.get_driver()
self.cal_page = CalPage(self.driver)
self.driver.get("http://cal.apple886.com/")
def teardown_method(self):
time.sleep(5)
DriverUtils.quit_driver()
# 加法算数
@pytest.mark.parametrize("params", read_data("cal_data.json"))
def test_2_add(self, params):
for i in params["data"]:
self.cal_page.click_number_btn(i)
self.cal_page.click_add_btn()
time.sleep(5)
# 4. 点击等于号按钮
self.cal_page.click_equal_btn()
time.sleep(5)
# 断言
assert str(params["result"]) == self.cal_page.get_result()
6.6 utils包
新建driver_utils.py
# 获取/关闭浏览器驱动的类
from selenium import webdriver
class DriverUtils:
__driver = None
# 获取浏览器驱动
@classmethod
def get_driver(cls):
if cls.__driver is None:
cls.__driver = webdriver.Chrome()
cls.__driver.maximize_window()
cls.__driver.implicitly_wait(10)
return cls.__driver
# 关闭浏览器驱动
@classmethod
def quit_driver(cls):
if cls.__driver is not None:
cls.__driver.quit()
cls.__driver = None
新建read_data.py用于读取data包的数据
# 读取data数据文件
import json
def read_data(filename):
with open("./data/" + filename, "r", encoding="utf-8") as f:
list_data = []
dict_list = json.load(f)
for value in dict_list.values():
list_data.append(value)
return list_data
7 日志收集
7.1 日志收集
概念 : 日志就是用于记录系统运行时的信息, 也称为Log
作用:
- 调试程序
- 旧的方式: print(“xxxx”)
- low
- 新的方式: 通过日志
- 旧的方式: print(“xxxx”)
- 了解程序运行的情况, 是否正常
- 程序运行故障分析与问题定位
- 用来做用户行为分析和数据统计
- 需要学好 sql
级别:
- 思考
- 是否记录的所有日志信息重要性都一样?
- 日志级别, 指日志信息的重要性
常见日志级别:
- DEBUG === 调试
- INFO === 信息
- WARNING === 警告
- ERROR === 错误
- CRITICAL === 严重错误
日志基本用法:
-
logging:python中有一个标准库, logging模块可以直接记录日志
-
使用
- 导入 logging 包
- 输出日志
- 默认的日志级别被设置为 warning
-
设置日志级别
- 方法
logging.basicConfig(level=logging.DEBUG)
- 方法
-
设置日志格式
- 默认格式
- 日志级别 : Logger名称 : 日志内容
- 自定义格式
logging.basicConfig(format="xxxxxx")
- 默认格式
-
将日志信息输出到文件
- 默认
- python的logging模块将日志打印到了标准输出中(控制台)
- 将日志输出到文件的方法
- logging.basicConfig(filename=“xxx.log”)
- 默认
7.2 日志高级用法
思考
- 如何将日志信息同时输出到控制台和日志文件中?
- 如何将不同级别的日志输出到不同的日志文件?
- 如何解决日志文件过大的问题?
7.3 四大组件
- 日志器(Logger):提供了程序使用日志的入口
- 处理器(Handler):将logger创建的日志记录发送到合适的输出
- 格式器(Formatter):决定日志的输出格式
- 过滤器(Filter): 提供了更细粒度的控制工具来决定输出哪条日志记录, 丢弃哪条日志记录
组件之间的关系
- 日志器 (Logger) 是入口,
- 真正干活的是处理器 (Handler),
- 处理器还可以通过格式器 (Formatter)
- 过滤器 (Filter) 对输出的日志内容做格式化和过滤
7.3.1 Logger类
-
如何创建Logger对象
-
logger = logging.getLogger(name)
-
可选参数 name
-
如果不写name, 日志器名称默认为 root
-
如果写了name, 如, logger = logging.getLogger(“myLogger”) 那么日志器的名称为 myLogger
-
-
Logger常用方法
-
打印日志
- logger.debug()
- logger.info()
- logger.warning
- logger.error
- logger.critical()
设置日志级别
logger.setLevel()
- 为logger对象添加一个handler对象
logger.addHandler()
为logger对象添加一个filter对象
logger.addFilter
7.3.2 Handler类
- 如何创建Handler对象
- 在程序中不应该直接实例化和使用Handler实例, 因为Handler是一个基类, 它只定义了Handler应该有的接口, 应该使用Handler实现类来创建对象
- 创建方式
- 输出日志到控制台
- logging.StreamHandler
- 输出到磁盘文件, 默认文件大小会无限增长
- 输出到文件, 按文件大小切割
- 输出到文件, 按时间切割
- logging.hanlders.TimedRotatingFileHandler
- 将日志消息以get或post的方式发送给http服务器
- 将日志消息发送给一个指定的email地址
- 输出日志到控制台
- 常用方法
- 为handler设置格式器对象
- handler.setFormatter()
- 为handler设置格式器对象
7.3.3 Formatter类
**作用:**Formatter对象用于配置日志信息的格式
-
如何创建Formatter对象
- logging.Formatter(fmt=None, datefmt=None) fmt: 消息格式化字符串, 如果不指定该参数则默认使用message的原始值 datefmt: 日期格式化字符串, 如果不指定该参数则默认使用 “%Y-%m-%d %H:%M:%S”
-
案例
-
说明
-
可读性好的日志需要具备一些特征
- 在控制台和文件都能输出
- 文件输出能够按时间切割
-
-
步骤
- 导包
- 创建日志器对象 / 设置日志级别
- 创建处理器对象: 输出到控制台 + 文件(按时间切割)
- 创建格式器对象
- 将格式器添加到处理器
- 将处理器添加到日志器
- 打印日志
代码
# 1. 导包
import logging
import logging.handlers
# 2. 创建日志器对象 / 设置日志级别
logger = logging.getLogger() # 默认日志器名称为 root
# logger = logging.getLogger("An") # 自定义日志器名称为 An
logger.setLevel(level=logging.DEBUG)
# 3. 创建处理器对象: 输出到控制台 + 文件(按时间切割)
ls = logging.StreamHandler()
lf = logging.handlers.TimedRotatingFileHandler(filename="172.log", when="s", backupCount=3)
# 4. 创建格式器对象
fmt = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(funcName)s:%(lineno)d] - %(message)s"
formatter = logging.Formatter(fmt=fmt)
# 5. 将格式器添加到处理器
ls.setFormatter(formatter)
lf.setFormatter(formatter)
# 6. 将处理器添加到日志器
logger.addHandler(ls)
logger.addHandler(lf)
# 7. 打印日志
while 1:
logger.debug("===================================================================")
8 面试题
- 说明样的项目适合做web自动化?
①需求变动不频繁
②项目周期长
③项目需要回归测试
- web自动化一个什么时候开始?
①手工测试结束后