06.2 PO模式与数据驱动
06.2 PO模式与数据驱动
- 目标
1. 深入理解方法封装的思想
2. 能够使用方法封装的思想对代码进行优化
3. 深入理解PO模式的思想
4. 熟练掌握PO模式的分层思想
1. PO模式学习思路
采用版本迭代的方式来学习,便于对不同版本的优缺点进行对比和理解。
- V1:不使用任何设计模式和单元测试框架
- V2:使用UnitTest管理用例
- V3:使用方法封装的思想,对代码进行优化
- V4:采用PO模式的分层思想对代码进行拆分
- V5:对PO分层之后的代码继续优化
- V6:PO模式深入封装,把共同操作提取封装到父类中,子类直接调用父类的方法
2. 无模式
对TPshop项目的登录模块进行自动化测试。
提示:登录模块包含了很多测试用例,比如:账号不存在、密码错误、验证码错误、登录成功等等。
为了节省时间我们只选取几个有代表性的用例来演示。
1. 选择的测试用例
账号不存在
- 点击首页的‘登录’链接,进入登录页面
- 输入一个不存在的用户名
- 输入密码
- 输入验证码
- 点击登录按钮
- 获取错误提示信息
密码错误
- 点击首页的‘登录’链接,进入登录页面
- 输入用户名
- 输入一个错误的密码
- 输入验证码
- 点击登录按钮
- 获取错误提示信息
2. V1版本
不使用任何设计模式和单元测试框架。
每个文件里编写一个用例,完全的面向过程的编程方式。
1. 存在的问题
- 一条测试用例对应一个文件,用例较多时不方便管理维护
- 代码高度冗余
2. 示例代码
登录功能**-**账号不存在
from selenium import webdriver # 创建浏览器驱动对象,并完成初始化操作 driver = webdriver.Firefox() driver.maximize_window() driver.implicitly_wait(10) driver.get("http://localhost") """ 登录功能-账号不存在 """ # 点击首页的‘登录’链接,进入登录页面 driver.find_element_by_link_text("登录").click() # 输入用户名 driver.find_element_by_id("username").send_keys("13099999999") # 输入密码 driver.find_element_by_id("password").send_keys("123456") # 输入验证码 driver.find_element_by_id("verify_code").send_keys("8888") # 点击‘登录’按钮 driver.find_element_by_name("sbtbutton").click() # 获取提示信息 msg = driver.find_element_by_class_name("layui-layer-content").text print("msg=", msg) # 关闭驱动对象 driver.quit()
登录功能-密码错误
from selenium import webdriver # 创建浏览器驱动对象,并完成初始化操作 driver = webdriver.Firefox() driver.maximize_window() driver.implicitly_wait(10) driver.get("http://localhost") """ 登录功能-密码错误 """ # 点击首页的‘登录’链接,进入登录页面 driver.find_element_by_link_text("登录").click() # 输入用户名 driver.find_element_by_id("username").send_keys("13012345678") # 输入密码 driver.find_element_by_id("password").send_keys("error") # 输入验证码 driver.find_element_by_id("verify_code").send_keys("8888") # 点击‘登录’按钮 driver.find_element_by_name("sbtbutton").click() # 获取提示信息 msg = driver.find_element_by_class_name("layui-layer-content").text print("msg=", msg) # 关闭驱动对象 driver.quit()
3. V2版本
- 使用UnitTest管理用例,并断言用例的执行结果
1. 引入UnitTest的好处
- 方便组织、管理多个测试用例
- 提供了丰富的断言方法
- 方便生成测试报告
- 减少了代码冗余
2. 存在的问题
- 代码冗余
3. 示例代码
import unittest
from selenium import webdriver
class TestLogin(unittest.TestCase):
"""
对登录模块的功能进行测试
"""
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Firefox()
cls.driver.maximize_window()
cls.driver.implicitly_wait(10)
cls.driver.get("http://localhost")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def setUp(self):
# 打开首页
self.driver.get("http://localhost")
# 点击首页的‘登录’链接,进入登录页面
self.driver.find_element_by_link_text("登录").click()
# 账号不存在
def test_login_username_is_error(self):
# 输入用户名
self.driver.find_element_by_id("username").send_keys("13099999999")
# 输入密码
self.driver.find_element_by_id("password").send_keys("123456")
# 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 点击‘登录’
self.driver.find_element_by_name("sbtbutton").click()
# 断言提示信息
msg = self.driver.find_element_by_class_name("layui-layer-content").text
print("msg=", msg)
self.assertIn("账号不存在", msg)
# 密码错误
def test_login_password_is_error(self):
# 输入用户名
self.driver.find_element_by_id("username").send_keys("13012345678")
# 输入密码
self.driver.find_element_by_id("password").send_keys("error")
# 输入验证码
self.driver.find_element_by_id("verify_code").send_keys("8888")
# 点击‘登录’
self.driver.find_element_by_name("sbtbutton").click()
# 断言提示信息
msg = self.driver.find_element_by_class_name("layui-layer-content").text
print("msg=", msg)
self.assertIn("密码错误", msg)
3. 方法封装
- 目标
- 深入理解方法封装的思想
- 能够使用方法封装的思想对代码进行优化
1. 方法封装
方法封装
- 是将一些有共性的或多次被使用的代码提取到一个方法中,供其他地方调用。
封装的好处
避免代码冗余
容易维护
隐藏代码实现的细节
目的
- 用最少的代码实现最多的功能
2. V3版本
- 使用方法封装的思想,对代码进行优化。
- 定义获取驱动对象的工具类
- 封装“获取弹出框的提示消息”
1. 定义获取驱动对象的工具类
对登录流程的代码进行优化,定义获取驱动对象的工具类
# utils.py class DriverUtil: """ 浏览器驱动工具类 """ _driver = None @classmethod def get_driver(cls): """ 获取浏览器驱动对象,并完成初始化设置 :return: 浏览器驱动对象 """ if cls._driver is None: cls._driver = webdriver.Firefox() cls._driver.maximize_window() cls._driver.implicitly_wait(10) cls._driver.get("http://localhost") return cls._driver @classmethod def quit_driver(cls): """ 关闭浏览器驱动 """ if cls._driver: cls._driver.quit() cls._driver = None
2. 封装“获取弹出框的提示消息”
对登录流程的代码进行优化,封装‘获取弹出框的提示消息’的方法
# utils.py def get_tips_msg(): """ 获取弹出框的提示消息 :return: 消息文本内容 """ msg = DriverUtil.get_driver().find_element_by_class_name("layui-layer-content").text return msg
4. PO模式介绍
- 目标
- 深入理解PO模式的思想
- 掌握PO模式的分层思想
1. 存在的问题
在做UI自动化时定位元素特别依赖页面,一旦页面发生变更就不得不跟着去修改定位元素的代码。
举例:假设要对一个元素进行点击操作,而且会经常对该元素进行操作,那么你就可能会编写多处如下代码
driver.find_element_by_id("login-btn").click()
存在的问题
- 如果开发人员修改了这个元素的id,这时候你就不得不修改所有对应的代码
- 存在大量冗余代码
思考:如何来解决这个问题呢?
2. PO模式
PO是Page Object的缩写,PO模式是自动化测试项目开发实践的最佳设计模式之一。核心思想是通过对界面元素的封装减少冗余代码,同时在后期维护中,若元素定位发生变化, 只需要调整页面元素封装的代码,提高测试用例的可维护性、可读性。
PO模式可以把一个页面分为三层,对象库层、操作层、业务层。
- 对象库层:封装定位元素的方法。
- 操作层:封装对元素的操作。
- 业务层:将一个或多个操作组合起来完成一个业务功能。比如登录:需要输入帐号、密码、点击登录三个操作。
引入PO模式的好处
引入PO模式前
- 存在大量冗余代码
- 业务流程不清晰
- 后期维护成本大
引入PO模式后
- 减少冗余代码
- 业务代码和测试代码被分开,降低耦合性
- 维护成本低
- 减少冗余代码
5. PO模式实践
- 目标
- 能够采用PO模式的分层思想对页面进行封装
1. V4版本
- 采用PO模式的分层思想对代码进行拆分
1. PO分层封装
- 对登录页面进行分层封装
- 对象库层:LoginPage
- 操作层:LoginHandle
- 业务层:LoginProxy
- 调用业务层的方法,编写测试用例
- 测试用例:TestLogin
2. 示例代码
from po.utils import DriverUtil
class LoginPage:
"""
对象库层
"""
def __init__(self):
self.driver = DriverUtil.get_driver()
# 用户名输入框
self.username = None
# 密码
self.password = None
# 验证码输入框
self.verify_code = None
# 登录按钮
self.login_btn = None
# 忘记密码
self.forget_pwd = None
def find_username(self):
return self.driver.find_element_by_id("username")
def find_password(self):
return self.driver.find_element_by_id("password")
def find_verify_code(self):
return self.driver.find_element_by_id("verify_code")
def find_login_btn(self):
return self.driver.find_element_by_name("sbtbutton")
def find_forget_pwd(self):
return self.driver.find_element_by_partial_link_text("忘记密码")
class LoginHandle:
"""
操作层
"""
def __init__(self):
self.login_page = LoginPage()
def input_username(self, username):
self.login_page.find_username().send_keys(username)
def input_password(self, pwd):
self.login_page.find_password().send_keys(pwd)
def input_verify_code(self, code):
self.login_page.find_verify_code().send_keys(code)
def click_login_btn(self):
self.login_page.find_login_btn().click()
def click_forget_pwd(self):
self.login_page.find_forget_pwd().click()
class LoginProxy:
"""
业务层
"""
def __init__(self):
self.login_handle = LoginHandle()
# 登录
def login(self, username, password, verify_code):
# 输入用户名
self.login_handle.input_username(username)
# 输入密码
self.login_handle.input_password(password)
# 输入验证码
self.login_handle.input_verify_code(verify_code)
# 点击登录按钮
self.login_handle.click_login_btn()
# 跳转到忘记密码页面
def to_forget_pwd_page(self):
# 点击忘记密码
self.login_handle.click_forget_pwd()
import unittest
from po import utils
from po.utils import DriverUtil
from po.v4.page.login_page import LoginProxy
class TestLogin(unittest.TestCase):
"""
对登录模块的功能进行测试
"""
@classmethod
def setUpClass(cls):
cls.driver = DriverUtil.get_driver()
cls.login_proxy = LoginProxy()
@classmethod
def tearDownClass(cls):
DriverUtil.quit_driver()
def setUp(self):
# 打开首页
self.driver.get("http://localhost")
# 点击首页的‘登录’链接,进入登录页面
self.driver.find_element_by_link_text("登录").click()
# 账号不存在
def test_login_username_is_error(self):
self.login_proxy.login("13099999999", "123456", "8888")
# 断言提示信息
msg = utils.get_tips_msg()
print("msg=", msg)
self.assertIn("账号不存在", msg)
# 密码错误
def test_login_password_is_error(self):
self.login_proxy.login("13012345678", "123456", "8888")
# 断言提示信息
msg = utils.get_tips_msg()
print("msg=", msg)
self.assertIn("密码错误", msg)
2. V5版本
- 对PO分层之后的代码继续优化
- 优化对象库层的代码,抽取元素的定位方式,把定位信息定义在对象的属性中,便于集中管理
- 优化操作层的代码,针对输入操作应该先清空输入框中的内容再输入新的内容
from selenium.webdriver.common.by import By
from po.utils import DriverUtil
class LoginPage:
"""
对象库层
"""
def __init__(self):
self.driver = DriverUtil.get_driver()
# 用户名
self.username = (By.ID, "username")
# 密码
self.password = (By.ID, "password")
# 验证码输入框
self.verify_code = (By.ID, "verify_code")
# 登录按钮
self.login_btn = (By.NAME, "sbtbutton")
# 忘记密码
self.forget_pwd = (By.PARTIAL_LINK_TEXT, "忘记密码")
def find_username(self):
return self.driver.find_element(self.username[0], self.username[1])
def find_password(self):
return self.driver.find_element(self.password[0], self.password[1])
def find_verify_code(self):
return self.driver.find_element(self.verify_code[0], self.verify_code[1])
def find_login_btn(self):
return self.driver.find_element(self.login_btn[0], self.login_btn[1])
def find_forget_pwd(self):
return self.driver.find_element(self.forget_pwd[0], self.forget_pwd[1])
class LoginHandle:
"""
操作层
"""
def __init__(self):
self.login_page = LoginPage()
def input_username(self, username):
self.login_page.find_username().clear()
self.login_page.find_username().send_keys(username)
def input_password(self, pwd):
self.login_page.find_password().clear()
self.login_page.find_password().send_keys(pwd)
def input_verify_code(self, code):
self.login_page.find_verify_code().clear()
self.login_page.find_verify_code().send_keys(code)
def click_login_btn(self):
self.login_page.find_login_btn().click()
def click_forget_pwd(self):
self.login_page.find_forget_pwd().click()
class LoginProxy:
"""
业务层
"""
def __init__(self):
self.login_handle = LoginHandle()
# 登录
def login(self, username, password, verify_code):
# 输入用户名
self.login_handle.input_username(username)
# 输入密码
self.login_handle.input_password(password)
# 输入验证码
self.login_handle.input_verify_code(verify_code)
# 点击登录按钮
self.login_handle.click_login_btn()
# 跳转到忘记密码页面
def to_forget_pwd_page(self):
# 点击忘记密码
self.login_handle.click_forget_pwd()
6. PO模式深入封装
- 目标
- 能够采用继承的思想对PO模式进行深入的封装
1. V6版本
- 把共同操作提取封装到父类中,子类直接调用父类的方法,避免代码冗余
- 对象库层-基类,把定位元素的方法定义在基类中
- 操作层-基类,把对元素执行输入操作的方法定义在基类中
1. 示例代码
# base_page.py
from po.utils import DriverUtil
class BasePage:
"""
基类-对象库层
"""
def __init__(self):
self.driver = DriverUtil.get_driver()
def find_element(self, location):
return self.driver.find_element(location[0], location[1])
class BaseHandle:
"""
基类-操作层
"""
def input_text(self, element, text):
"""
在输入框里输入文本内容,先清空再输入
:param element: 要操作的元素
:param text: 要输入的文本内容
"""
element.clear()
element.send_keys(text)
from selenium.webdriver.common.by import By
from po.v6.common.base_page import BasePage, BaseHandle
class LoginPage(BasePage):
"""
对象库层
"""
def __init__(self):
super().__init__()
# 用户名输入框
self.username = (By.ID, "username")
# 密码
self.password = (By.ID, "password")
# 验证码
self.verify_code = (By.ID, "verify_code")
# 登录按钮
self.login_btn = (By.NAME, "sbtbutton")
# 忘记密码
self.forget_pwd = (By.PARTIAL_LINK_TEXT, "忘记密码")
def find_username(self):
return self.find_element(self.username)
def find_password(self):
return self.find_element(self.password)
def find_verify_code(self):
return self.find_element(self.verify_code)
def find_login_btn(self):
return self.find_element(self.login_btn)
def find_forget_pwd(self):
return self.find_element(self.forget_pwd)
class LoginHandle(BaseHandle):
"""
操作层
"""
def __init__(self):
self.login_page = LoginPage()
def input_username(self, username):
self.input_text(self.login_page.find_username(), username)
def input_password(self, pwd):
self.input_text(self.login_page.find_password(), pwd)
def input_verify_code(self, code):
self.input_text(self.login_page.find_verify_code(), code)
def click_login_btn(self):
self.login_page.find_login_btn().click()
def click_forget_pwd(self):
self.login_page.find_forget_pwd().click()
class LoginProxy:
"""
业务层
"""
def __init__(self):
self.login_handle = LoginHandle()
# 登录
def login(self, username, password, verify_code):
# 输入用户名
self.login_handle.input_username(username)
# 输入密码
self.login_handle.input_password(password)
# 输入验证码
self.login_handle.input_verify_code(verify_code)
# 点击登录按钮
self.login_handle.click_login_btn()
# 跳转到忘记密码页面
def to_forget_pwd_page(self):
# 点击忘记密码
self.login_handle.click_forget_pwd()
7. 数据驱动
- 目标
- 理解数据驱动的概念
1. 什么是数据驱动?
数据驱动:是以数据来驱动整个测试用例的执行,也就是测试数据决定测试结果。比如我们要测试加法,我们的测试数据是1和1,测试结果就是2,如果测试数据是1和2,测试结果就是3。
数据驱动的特点
数据驱动本身不是一个工业级标准的概念,因此在不同的公司都会有不同的解释。
可以把数据驱动理解为一种模式或者一种思想。
数据驱动技术可以将用户把关注点放在对测试数据的构建和维护上,而不是直接维护脚本,可
以利用同样的过程对不同的数据输入进行测试。
数据驱动的实现要依赖参数化的技术。
2. 传入数据的方式(测试数据的来源)
- 直接定义在测试脚本中(简单直观,但代码和数据未实现真正的分离,不方便后期维护)
- 从文件读取数据,如JSON、excel、xml、txt等格式文件
- 从数据库中读取数据
- 直接调用接口获取数据源
- 本地封装一些生成数据的方法
3. JSON
JSON介绍
JSON的全称是”JavaScript Object Notation”,是JavaScript对象表示法,它是一种基于文本,独立于语言的轻量级数据交换格式。
JSON特点
JSON是纯文本
JSON具有良好的自我描述性,便于阅读和编写
JSON具有清晰的层级结构
有效地提升网络传输效率
JSON语法规则
- 大括号保存对象 - 中括号保存数组 - 对象数组可以相互嵌套 - 数据采用键值对表示 - 多个数据由逗号分隔
JSON值
JSON 值可以是:
数字(整数或浮点数)
字符串(在双引号中)
逻辑值(true 或 false)
数组(在中括号中)
对象(在大括号中)
null
示例
{ "name": "tom", "age": 18, "isMan": true, "school": null, "address": { "country": "中国", "city": "江苏苏州", "street": "科技园路" }, "numbers": [2, 6, 8, 9], "links": [ { "name": "Baidu", "url": "http://www.baidu.com" }, { "name": "TaoBao", "url": "http://www.taobao.com" } ] }
4. JSON操作
- python字典与JSON之间的转换
- JSON文件读写
导入依赖包
import json
python字典与JSON之间的转换
# 把python字典类型转换为 JSON字符串 data = { 'id': 1, 'name': 'Tom', 'address': '北京市海淀区', 'school': None } json_str = json.dumps(data) #把JSON字符串转换为python字典 json_str = '{"id": 1, "name": "Tom", "address": "北京市海淀区", "school": null}' dict_data = json.loads(json_str)
JSON文件读写
#读取JSON文件 with open('data.json', encoding='UTF-8') as f: data = json.load(f) # 返回的数据类型为字典或列表 #写入JSON文件 param = {'name': 'tom', 'age': 20} with open('data2.json', 'w', encoding='UTF-8') as f: json.dump(param, f)
5. 实战
1. 数据驱动实战一
目标
- 掌握数据驱动的开发流程
- 掌握如何读取JSON数据文件
- 巩固PO模式
对网页计算器,进行加法的测试操作。通过读取数据文件中的数据来执行用例。
网址:http://cal.apple886.com/
1.实现步骤
- 采用PO模式的分层思想对页面进行封装
- 编写测试脚本
- 使用参数化传入测试数据
- 把测试数据定义到JSON数据文件中
2.数据文件
第一个数字加第二个数字等于第三个数字,每一行数据代表一个用例
3.示例代码
from selenium import webdriver class DriverUtil: """ 浏览器驱动工具类 """ _driver = None @classmethod def get_driver(cls): """ 获取浏览器驱动对象,并完成初始化设置 :return: 浏览器驱动对象 """ if cls._driver is None: cls._driver = webdriver.Chrome() cls._driver.maximize_window() cls._driver.implicitly_wait(10) cls._driver.get("http://cal.apple886.com/") return cls._driver @classmethod def quit_driver(cls): """ 关闭浏览器驱动 """ if cls._driver: cls._driver.quit() cls._driver = None from selenium.webdriver.common.by import By from ddt.calculator.utils import DriverUtil class CalculatorPage: """ 计算器页面-对象库层 """ def __init__(self): self.driver = DriverUtil.get_driver() # 数字按钮 self.digit_btn = (By.ID, "simple{}") # 加法按钮 self.add_btn = (By.ID, "simpleAdd") # 等号按钮 self.eq_btn = (By.ID, "simpleEqual") # 计算结果 self.result = (By.ID, "resultIpt") def find_digit_btn(self, digit): location = (self.digit_btn[0], self.digit_btn[1].format(digit)) return self.driver.find_element(*location) def find_add_btn(self): return self.driver.find_element(*self.add_btn) def find_eq_btn(self): return self.driver.find_element(*self.eq_btn) def find_result_btn(self): return self.driver.find_element(*self.result) class CalculatorHandle: """ 计算器页面-操作层 """ def __init__(self): self.calculator_page = CalculatorPage() def click_digit_btn(self, digit): self.calculator_page.find_digit_btn(digit).click() def click_add_btn(self): self.calculator_page.find_add_btn().click() def click_eq_btn(self): self.calculator_page.find_eq_btn().click() def get_result(self): return self.calculator_page.find_result_btn().get_attribute("value") def input_numbers(self, numbers): for num in numbers: self.click_digit_btn(num) class CalculatorProxy: """ 计算器页面-业务层 """ def __init__(self): self.calculator_handle = CalculatorHandle() def add(self, num1, num2): self.calculator_handle.input_numbers(str(num1)) self.calculator_handle.click_add_btn() self.calculator_handle.input_numbers(str(num2)) self.calculator_handle.click_eq_btn() def get_result(self): return self.calculator_handle.get_result() import json import time import unittest from parameterized import parameterized from ddt.calculator.page.calculator_page import CalculatorProxy from ddt.calculator.utils import DriverUtil def build_data(): test_data = [] with open("../data/calculator.json", encoding='UTF-8') as f: test_data = json.load(f) print("test_data=", test_data) return test_data class TestCalculator(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = DriverUtil.get_driver() cls.calculatorProxy = CalculatorProxy() @classmethod def tearDownClass(cls): DriverUtil.quit_driver() @parameterized.expand(build_data) def test_add(self, a, b, expect): print('a={} b={} expect={}'.format(a, b, expect)) self.calculatorProxy.add(a, b) # 获取计算结果 result = self.calculatorProxy.get_result() print("result=", result) self.assertEqual(result, str(expect))
2. 数据驱动实战二
目标
- 掌握数据驱动的开发流程
- 掌握如何读取JSON数据文件
- 巩固PO模式
对TPshop网站的登录模块进行单元测试
实现步骤
- 编写测试用例
- 采用PO模式的分层思想对页面进行封装
- 编写测试脚本
- 定义数据文件,实现参数化
用例设计
数据文件
{ "login_username_is_null": { "username": "", "password": "123456", "code": "8888", "is_success": false, "expect": "用户名不能为空" }, "login_password_is_null": { "username": "13012345678", "password": "", "code": "8888", "is_success": false, "expect": "密码不能为空" }, "login_password_is_error": { "username": "13012345678", "password": "error", "code": "8888", "is_success": false, "expect": "密码错误" }, "login_success": { "username": "13012345678", "password": "123456", "code": "8888", "is_success": true, "expect": "我的账户" } }
示例代码
from selenium import webdriver def get_tips_msg(): """ 获取弹出框的提示消息 :return: 消息文本内容 """ msg = DriverUtil.get_driver().find_element_by_class_name("layui-layer-content").text return msg class DriverUtil: """ 浏览器驱动工具类 """ _driver = None @classmethod def get_driver(cls): """ 获取浏览器驱动对象,并完成初始化设置 :return: 浏览器驱动对象 """ if cls._driver is None: cls._driver = webdriver.Firefox() cls._driver.maximize_window() cls._driver.implicitly_wait(10) cls._driver.get("http://localhost") return cls._driver @classmethod def quit_driver(cls): """ 关闭浏览器驱动 """ if cls._driver: cls._driver.quit() cls._driver = None # login_page.py from selenium.webdriver.common.by import By from ddt.tpshop.utils import DriverUtil class LoginPage: """ 登录页面-对象库层 """ def __init__(self): self.driver = DriverUtil.get_driver() # 用户名输入框 self.username = (By.ID, "username") # 密码 self.password = (By.ID, "password") # 验证码 self.verify_code = (By.ID, "verify_code") # 登录按钮 self.login_btn = (By.NAME, "sbtbutton") def find_username(self): return self.driver.find_element(*self.username) def find_password(self): return self.driver.find_element(*self.password) def find_verify_code(self): return self.driver.find_element(*self.verify_code) def find_login_btn(self): return self.driver.find_element(*self.login_btn) class LoginHandle: """ 登录页面-操作层 """ def __init__(self): self.login_page = LoginPage() def input_username(self, username): self.login_page.find_username().send_keys(username) def input_password(self, pwd): self.login_page.find_password().send_keys(pwd) def input_verify_code(self, code): self.login_page.find_verify_code().send_keys(code) def click_login_btn(self): self.login_page.find_login_btn().click() class LoginProxy: """ 登录页面-业务层 """ def __init__(self): self.login_handle = LoginHandle() def login(self, username, password, code): self.login_handle.input_username(username) self.login_handle.input_password(password) self.login_handle.input_verify_code(code) self.login_handle.click_login_btn() # test_login.py import json import time import unittest from parameterized import parameterized from ddt.tpshop import utils from ddt.tpshop.page.login_page import LoginProxy from ddt.tpshop.utils import DriverUtil # 构建测试数据 def build_data(): test_data = [] with open("../data/testData.json", encoding='UTF-8') as f: json_data = json.load(f) for login_data in json_data.values(): test_data.append((login_data.get("username"), login_data.get("password"), login_data.get("code"), login_data.get("is_success"), login_data.get("expect"))) print("test_data=", test_data) return test_data class TestLogin(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = DriverUtil.get_driver() cls.login_proxy = LoginProxy() @classmethod def tearDownClass(cls): DriverUtil.quit_driver() def setUp(self): # 进入首页 self.driver.get("http://localhost") # 点击登录链接 self.driver.find_element_by_link_text("登录").click() @parameterized.expand(build_data) def test_add(self, username, password, code, is_success, expect): print('username={} password={} code={} is_success={} expect={}'. format(username, password, code, is_success, expect)) # 登录 self.login_proxy.login(username, password, code) time.sleep(3) # 登录成功的用例 if is_success: self.assertIn(expect, self.driver.title) else: # 获取提示框消息 msg = utils.get_tips_msg() print("msg=", msg) self.assertIn(expect, msg)
8. 日志收集
- 目标
- 理解日志的相关概念
- 掌握日志的基本用法
- 掌握日志的高级用法
1. 日志相关概念
1. 了解日志的概念
2. 理解日志的作用
3. 掌握常见的日志级别
1. 日志
概念:日志就是用于记录系统运行时的信息,对一个事件的记录;也称为Log。
- 日志的作用
- 调试程序
- 了解系统程序运行的情况,是否正常
- 系统程序运行故障分析与问题定位
- 用来做用户行为分析和数据统计
- 日志级别
思考:是否系统记录的所有日志信息的重要性都一样?
日志级别:是指日志信息的优先级、重要性或者严重程度
常见的日志级别
日志级别 描述 DEBUG 调试级别,打印非常详细的日志信息,通常用于对代码的调试 INFO 信息级别,打印一般的日志信息,突出强调程序的运行过程 WARNING 警告级别,打印警告日志信息,表明会出现潜在错误的情形,一般不影响软件的正常使用 ERROR 错误级别,打印错误异常信息,该级别的错误可能会导致系统的一些功能无法正常使用 CRITICAL 严重错误级别,一个严重的错误,这表明系统可能无法继续运行
说明
- 上面列表中的日志级别是从上到下依次升高的,即:DEBUG < INFO < WARNING < ERROR < CRITICAL; - 当为程序指定一个日志级别后,程序会记录所有日志级别大于或等于指定日志级别的日志信息,而不是仅仅记录指定级别的日志信息; - 一般建议只使用DEBUG、INFO、WARNING、ERROR这四个级别
2. 日志的基本用法
目标
1. 掌握如何设置日志级别 2. 掌握如何设置日志格式 3. 掌握如何将日志信息输出到文件中
logging模块
- Python中有一个标准库模块logging可以直接记录日志
基本用法
import logging logging.debug("这是一条调试信息") logging.info("这是一条普通信息") logging.warning("这是一条警告信息") logging.error("这是一条错误信息") logging.critical("这是一条严重错误信息")
设置日志级别
logging中默认的日志级别为WARNING,程序中大于等于该级别的日志才能输出,小于该级别的日志不会被打印出来。
设置日志级别
- logging.basicConfig(level=logging.DEBUG)
如何选择日志级别
- 在开发环境和测试环境中,为了尽可能详细的查看程序的运行状态来保证上线后的稳定性,可以使用DEBUG或INFO级别的日志获取详细的日志信息,这是非常耗费机器性能的。
- 在生产环境中,通常只记录程序的异常信息、错误信息等(设置成WARNING或ERROR级别),这样既可以减小服务器的I/O压力,也可以提高获取错误日志信息的效率和方便问题的排查。
设置日志格式
默认的日志的格式为:
- 日志级别:Logger名称:日志内容
自定义日志格式:
logging.basicConfig(format="%(levelname)s:%(name)s:%(message)s")
format参数中可能用到的格式化信息
占位符 描述 %(name)s Logger的名字 %(levelno)s 数字形式的日志级别 %(levelname)s 文本形式的日志级别 %(pathname)s 调用日志输出函数的模块的完整路径名,可能没有 %(filename)s 调用日志输出函数的模块的文件名 %(module)s 调用日志输出函数的模块名 %(funcName)s 调用日志输出函数的函数名 %(lineno)d 调用日志输出函数的语句所在的代码行 %(created)f 当前时间,用UNIX标准的表示时间的浮 点数表示 %(relativeCreated)d 输出日志信息时的,自Logger创建以来的毫秒数 %(asctime)s 字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896” %(thread)d 线程ID。可能没有 %(threadName)s 线程名。可能没有 %(process)d 进程ID。可能没有 %(message)s 用户输出的消息
示例代码
import logging fmt = '%(asctime)s %(levelname)s [%(name)s] [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s' logging.basicConfig(level=logging.INFO, format=fmt) logging.debug("调试") logging.info("信息") logging.warning("警告") logging.error("错误")
3.将日志信息输出到文件中
默认情况下Python的logging模块将日志打印到了标准输出中(控制台)
将日志信息输出到文件中:
logging.basicConfig(filename="a.log")
示例代码
import logging fmt = '%(asctime)s %(levelname)s [%(name)s] [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s' logging.basicConfig(filename="a.log", level=logging.INFO, format=fmt) logging.debug("调试") logging.info("信息") logging.warning("警告") logging.error("错误")
3. 日志的高级用法
目标
1. 了解logging日志模块四大组件 2. 掌握如何讲日志输出到多个Handler中
思考:
- 如何将日志信息同时输出到控制台和日志文件中?
- 如何将不同级别的日志输出到不同的日志文件中?
- 如何解决日志文件过大的问题?
- logging日志模块四大组件
组件名称 类名 功能描述
日志器 Logger 提供了程序使用日志的入口
处理器 Handler 将logger创建的日志记录发送到合适的目的输出
格式器 Formatter 决定日志记录的最终输出格式
过滤器 Filter 提供了更细粒度的控制工具来决定输出哪条日志记录,丢弃哪条日志记录
- logging模块就是通过这些组件来完成日志处理的
组件之间的关系
日志器(logger)需要通过处理器(handler)将日志信息输出到目标位置,如:文件、sys.stdout、网络等;
不同的处理器(handler)可以将日志输出到不同的位置;
日志器(logger)可以设置多个处理器(handler)将同一条日志记录输出到不同的位置;
每个处理器(handler)都可以设置自己的格式器(formatter)实现同一条日志以不同的格式输出到不同的地方。
每个处理器(handler)都可以设置自己的过滤器(filter)实现日志过滤,从而只保留感兴趣的日志;
简单点说就是:日志器(logger)是入口,真正干活儿的是处理器(handler),处理器(handler)还可以通过过滤器(filter)和格式器(formatter)对要输出的日志内容做过滤和格式化等处理操作。
Logger类
Logger对象的任务
- 向程序暴露记录日志的方法
- 基于日志级别或Filter对象来决定要对哪些日志进行后续处理
- 将日志消息传送给所有感兴趣的日志handlers
如何创建Logger对象
logger = logging.getLogger() logger = logging.getLogger("myLogger")
- logging.getLogger()方法有一个可选参数name,该参数表示将要返回的日志器的名称标识,如果不提供该参数,则返回root日志器对象。 若以相同的name参数值多次调用getLogger()方法,将会返回指向同一个logger对象的引用。
Logger常用的方法
方法 描述 logger.debug() 打印日志 logger.info() logger.warning() logger.error() logger.critical() logger.setLevel() 设置日志器将会处理的日志消息的最低严重级别 logger.addHandler() 为该logger对象添加一个handler对象 logger.addFilter() 为该logger对象添加一个filter对象
- Handler类
Handler对象的作用是将消息分发到handler指定的位置,比如:控制台、文件、网络、邮件等。 Logger对象可以通过addHandler()方法为自己添加多个handler对象。
如何创建Handler对象
在程序中不应该直接实例化和使用Handler实例,因为Handler是一个基类,它只定义了Handler应该有的接口。 应该使用Handler实现类来创建对象,logging中内置的常用的Handler包括
Handler 描述 logging.StreamHandler 将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。 logging.FileHandler 将日志消息发送到磁盘文件,默认情况下文件大小会无限增长 logging.handlers.RotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按大小切割 logging.hanlders.TimedRotatingFileHandler 将日志消息发送到磁盘文件,并支持日志文件按时间切割 logging.handlers.HTTPHandler 将日志消息以GET或POST的方式发送给一个HTTP服务器 logging.handlers.SMTPHandler 将日志消息发送给一个指定的email地址
Handler常用的方法
方法 描述 handler.setLevel() 设置handler将会处理的日志消息的最低严重级别 handler.setFormatter() 为handler设置一个格式器对象 handler.addFilter() 为handler添加一个过滤器对象
Formatter类
Formatter对象用于配置日志信息的格式。
如何创建Formatter对象
formatter = logging.Formatter(fmt=None, datefmt=None, style='%') fmt:指定消息格式化字符串,如果不指定该参数则默认使用message的原始值 datefmt:指定日期格式字符串,如果不指定该参数则默认使用"%Y-%m-%d %H:%M:%S" style:Python 3.2新增的参数,可取值为 '%', '{'和 '$',如果不指定该参数则默认使用'%'
4. 将日志信息同时输出到控制台和文件中
实现步骤分析
1.创建日志器对象 2.创建控制台处理器对象 3.创建文件处理器对象 4.创建格式化器对象 5.把格式化器添加到处理器中 6.把处理器添加到日志器中
定义日志格式
fmt = '%(asctime)s %(levelname)s [%(name)s] [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s' formatter = logging.Formatter(fmt)
把日志输出到控制台
logger = logging.getLogger() sh = logging.StreamHandler() sh.setFormatter(formatter) logger.addHandler(sh)
把日志输出到文件中
fh = logging.FileHandler("./b.log") fh.setFormatter(formatter) logger.addHandler(fh)
5. 每日生成一个日志文件
定义Handler对象
fh = logging.handlers.TimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0) 将日志信息记录到文件中,以特定的时间间隔切换日志文件。 filename: 日志文件名 when: 时间单位,可选参数 S - Seconds M - Minutes H - Hours D - Days midnight - roll over at midnight W{0-6} - roll over on a certain day; 0 - Monday interval: 时间间隔 backupCount: 日志文件备份数量。如果backupCount大于0,那么当生成新的日志文件时,将只保留backupCount个文件,删除最老的文件。
示例代码
import logging.handlers logger = logging.getLogger() logger.setLevel(logging.DEBUG) # 日志格式 fmt = "%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s" formatter = logging.Formatter(fmt) # 输出到文件,每日一个文件 fh = logging.handlers.TimedRotatingFileHandler("./a.log", when='MIDNIGHT', interval=1, backupCount=3) fh.setFormatter(formatter) fh.setLevel(logging.INFO) logger.addHandler(fh)