PageObject 设计模式
PageObject 设计模式简要认知
PageObject 设计模式原理
将页面的元素定位和元素行为封装成一个page类。一个页面对应一个类
- 类的属性:元素的定位
- 类的行为:元素的操作
PageObject 设计模式核心思想
- 测试对象(页面)和测试用例(页面操作+测试数据)分离。
- 调用所需页面对象中的行为,组成测试用例。在用例中,是看不到元素定位和元素操作的。
PageObject 设计模式的优点
- 1、当某个页面的元素发生变化,只需要修改该页面对象中的代码即可,测试用例不需要修改。
- 2、提高代码重用率。结构清晰,维护代码更容易。
- 3、测试用例发生变化时,不需要或者只需要修改少数页面对象代码即可。
PageObject 设计模式实操
通过网易163邮箱的登录功能进行举例说明
这个是登录页面
登录成功后,进入首页
测试用例
编码
先建两个包,命名为 PageObjects(用于存放页面类)和 TestCases(用于存放测试用例)
根据表格中编写的测试用例,在 TestCases 文件夹下新建一个 test_login 文件,用于编写用例
(由于刚开始对元素进行定位,可以先不写后置清理操作 tearDown ,等代码运行没什么问题了,再加上也不迟)
test_login.py
import unittest
class TestLogin(unittest.TestCase):
# 前置操作
def setUp(self) -> None:
# 访问163邮箱登录页面
pass
def test_login_success(self):
# 步骤:
# 1、登录页面-登录操作
# 断言:
# 1、首页-获取元素是否存在
pass
def test_login_failed_no_username(self):
# 步骤:
# 1、登录页面-登录操作
# 断言:
# 1、登录页面-获取未输入用户名的错误提示信息
pass
def test_login_failed_no_password(self):
# 步骤:
# 1、登录页面-登录操作
# 断言:
# 1、登录页面-获取未输入密码的错误提示信息
pass
在 PageObjects文件夹下新建一个 login_page 文件,用于编写登录页面的登录行为
login_page.py
class LoginPage:
# 登录
def login(self):
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
先把大概的一个框架整好,然后再往里面添加内容
driver 扩展
对于页面的元素操作,我们要用到 selenium 中的 webdriver,所以在 LoginPage 这个类中,需要对 driver 进行初始化操作,初始化操作的时候,要传一个参数 driver 进来,这样才能进行对元素的一系列 find_element 操作
login_page.py
class LoginPage:
# 初始化操作
def __init__(self,driver):
# 初始化dirver
self.driver = driver
pass
# 登录-元素操作
def login(self):
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
注意,下面这种初始化操作是错误的
login_page.py
from selenium import webdriver
class LoginPage:
# 初始化操作
def __init__(self,driver:WebDriver):
# 初始化dirver
self.driver = webdriver.chrome()
pass
# 登录-元素操作
def login(self):
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
这里的初始化 driver 为什么不采用和之前一样的导入 webdriver 的方式呢?
不采用这种 webdriver 的方式,是因为这种操作每次都打开了一个浏览器,创建了一个新的会话,而我们登录页面的元素操作,都是在同一个登录页面进行的,需要在同一个浏览器的tab页面进行操作
【注意:这里登陆页面嵌套在一个 iframe 框架中,所以需要先定位 iframe 框架】
login_page.py
class LoginPage:
# 初始化操作
def __init__(self,driver):
# 初始化dirver
self.driver = driver
pass
# 登录-元素操作
def login(self,username):
# 定位到 iframe 并进行切换
self.driver.switch_to.frame(0)
# 找到用户名输入框输入用户名
self.driver.find_element_by_xpath("//div[@class='u-input box']/input[@name='email']").send_keys(username)
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
然而,当我们在页面上定位元素之后,写代码的时候发现没有代码提示了
这是因为 driver 的身上没有 webdriver 的属性,初始化的时候,只是传了一个参数 driver,相当于只是给定了一个变量,并没有给定 driver 的来源
所以,我们需要声明这个 driver 的类型
login_page.py
from selenium.webdriver.remote.webdriver import WebDriver
class LoginPage:
# 初始化操作
# driver:WebDriver 这里是声明 driver 是 WebDriver类的实例对象
def __init__(self,driver:WebDriver):
# 初始化dirver
self.driver = driver
pass
# 登录-元素操作
def login(self,username):
# 定位到 iframe 并进行切换
self.driver.switch_to.frame(0)
# 找到用户名输入框输入用户名
self.driver.find_element_by_xpath("//div[@class='u-input box']/input[@name='email']").send_keys(username)
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
这样,driver 身上就有 WebDriver 的属性了,写代码的时候,有代码提示,也可以减少因代码写错而产生的报错
简化页面元素定位
我们在页面上定位的元素,可以将其提出来,优点有以下两点:
- 可能这个元素在下面多个函数中都会用到,这样可以减少我们写的代码,在函数中进行调用也会更加方便
- 为了方便日后代码的维护,比如日后这个元素的属性发生了更改,就不用在代码中每个都用到这个属性的地方都进行更改
login_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.by import By
class LoginPage:
# 简化页面元素定位
# 用户名输入框
user_input = (By.XPATH,"//div[@class='u-input box']/input[@name='email']")
# 密码输入框
psd_input = (By.XPATH, "//div[@class='u-input box']/input[@name='password']")
# 登录按钮
login_btn = (By.XPATH,"//a[@id='dologin']")
# 初始化操作
# driver:WebDriver 这里是声明 driver 是 WebDriver类的实例对象
def __init__(self,driver:WebDriver):
# 初始化dirver
self.driver = driver
# 定位到 iframe 并进行切换
driver.switch_to.frame(0)
pass
# 登录-元素操作
def login(self,username,password):
# 找到用户名输入框输入用户名
self.driver.find_element(*self.user_input).send_keys(username)
# 找到密码输入框输入密码
self.driver.find_element(*self.psd_input).send_keys(password)
# 找到登录按钮进行点击
self.driver.find_element(*self.login_btn).click()
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
设置等待
页面的加载需要时间,且不清楚用例什么时候会调用元素,所以需要设置等待。这个时间一般设置在10~20s,视情况而定,但等待时间设置相对较长的时候,可以避免因网络原因而导致的某些问题产生报错
login_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
# 简化页面元素定位
# 用户名输入框
user_input = (By.XPATH,"//div[@class='u-input box']/input[@name='email']")
# 密码输入框
psd_input = (By.XPATH, "//div[@class='u-input box']/input[@name='password']")
# 登录按钮
login_btn = (By.XPATH,"//a[@id='dologin']")
# 初始化操作
# driver:WebDriver 这里是声明 driver 是 WebDriver类的实例对象
def __init__(self,driver:WebDriver):
# 初始化dirver
self.driver = driver
# 定位到 iframe 并进行切换
driver.switch_to.frame(0)
pass
# 登录-元素操作
def login(self,username,password):
# 设置元素可见等待
# 显性等待时间为20秒,等待元素为登录按钮
WebDriverWait(self.driver,20).until(EC.visibility_of_element_located(self.login_btn))
# 找到用户名输入框输入用户名
self.driver.find_element(*self.user_input).send_keys(username)
# 找到密码输入框输入密码
self.driver.find_element(*self.psd_input).send_keys(password)
# 找到登录按钮进行点击
self.driver.find_element(*self.login_btn).click()
pass
# 获取登录区域的提示信息
def get_error_msg(self):
pass
设置等待可见的元素,视具体情况而定,有些页面会有显示策略,所以设置等待可见的元素一般设置为最后出现的元素即可
断言处理
用户登录成功后,进入163邮箱首页,而登录成功的标志,是首页显示用户名,所以断言的内容可以是用户名
在 PageObjects文件夹下建立的 home_page 文件中编写代码,用于验证首页元素是否存在
home_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class HomePage:
# 用户名元素-是否存在的状态
exists_link = (By.XPATH,"//span[@id='spnUid']")
def __init__(self,driver:WebDriver):
# 初始化 dirver
self.driver = driver
# 是否存在的状态,如果存在,则返回 True ,不存在则返回 False
def get_element_exists(self):
try:
# 可采用 异常捕获处理-设置元素等待可见方式 进行判断
WebDriverWait(self.driver,10).until(EC.invisibility_of_element_located(self.exists_link))
except:
return False
else:
return True
然后,在 PageObjects文件夹下的 login_page 文件下进行断言操作
login_page.py
import unittest
from selenium import webdriver
from PO.PageObjects.login_page import LoginPage
from PO.PageObjects.home_page import HomePage
class TestLogin(unittest.TestCase):
# 前置操作
def setUp(self) -> None:
# 访问163邮箱登录页面
self.driver = webdriver.Chrome()
self.driver.get("https://mail.163.com/")
self.driver.maximize_window()
pass
# 后置清理操作
def tearDown(self) -> None:
self.driver.quit()
def test_login_success(self):
# 步骤:
# 1、登录页面-登录操作
LoginPage(self.driver).login("z14737424527","slhgalsdhgasdg") # 输入账号密码
# 断言:
# 1、首页-获取元素是否存在 (进行断言操作,元素可见返回True)
self.assertTrue(HomePage(self.driver).get_element_exists())
pass
def test_login_failed_no_username(self):
# 步骤:
# 1、登录页面-登录操作
# 断言:
# 1、登录页面-获取未输入用户名的错误提示信息
pass
def test_login_failed_no_password(self):
# 步骤:
# 1、登录页面-登录操作
# 断言:
# 1、登录页面-获取未输入密码的错误提示信息
pass
运行 test_login_success 方法(点击左边绿色小三角),可以发现运行成功
如果运行失败,查看错误提示,通过设置断点和调试模式,定位失败原因是网络原因、元素定位原因或其他原因
逆向用例
对于未输入账户名、未输入密码 或 账户名和密码都未输入 这类逆向用例,163邮箱的提示语都是在密码输入框下,且提示语都是同种类型的元素,只不过是提示的文本不同
在 PageObjects文件夹下建立的 login_page 文件中编写代码,用于获取提示的文本内容
login_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
class LoginPage:
# 简化页面元素定位
# 用户名输入框
user_input = (By.XPATH,"//div[@class='u-input box']/input[@name='email']")
# 密码输入框
psd_input = (By.XPATH, "//div[@class='u-input box']/input[@name='password']")
# 登录按钮
login_btn = (By.XPATH,"//a[@id='dologin']")
# 登录表单区域的提示信息
msg_login_form = (By.XPATH,"//div[@class='ferrorhead']")
# 初始化操作
# driver:WebDriver 这里是声明 driver 是 WebDriver类的实例对象
def __init__(self,driver:WebDriver):
# 初始化 dirver
self.driver = driver
# # 定位到 iframe 并进行切换
driver.switch_to.frame(0)
# 登录-元素操作
def login(self,username,password):
# 设置元素可见等待
# 显性等待时间为20秒,等待元素为登录按钮
WebDriverWait(self.driver,20).until(EC.visibility_of_element_located(self.login_btn))
# 找到用户名输入框输入用户名
self.driver.find_element(*self.user_input).send_keys(username)
# 找到密码输入框输入密码
self.driver.find_element(*self.psd_input).send_keys(password)
time.sleep(10)
# 找到登录按钮进行点击
self.driver.find_element(*self.login_btn).click()
# time.sleep(10)
# 获取登录区域的提示信息
def get_error_msg(self):
WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.msg_login_form))
# 返回文本值
return self.driver.find_element(*self.msg_login_form).text
在 TestCases 文件夹下的 test_login 文件下进行断言操作
test_login.py
import unittest
from selenium import webdriver
from PO.PageObjects.login_page import LoginPage
from PO.PageObjects.home_page import HomePage
class TestLogin(unittest.TestCase):
# 前置操作
def setUp(self) -> None:
# 访问163邮箱登录页面
self.driver = webdriver.Chrome()
self.driver.get("https://mail.163.com/")
self.driver.maximize_window()
pass
# 清理操作
def tearDown(self) -> None:
self.driver.quit()
def test_login_success(self):
# 步骤:
# 1、登录页面-登录操作
LoginPage(self.driver).login("z14737424527","sdgasdgasdg") # 输入账号密码
# 断言:
# 1、首页-获取元素是否存在 (进行断言操作,元素可见返回True)
self.assertTrue(HomePage(self.driver).get_element_exists())
def test_login_failed_no_username(self):
# 步骤:
# 1、登录页面-登录操作
lp = LoginPage(self.driver)
lp.login("", "sdgasdgasdg") # 输入账号为空
# 断言:
# 1、登录页面-获取未输入用户名的错误提示信息 进行对比
self.assertEqual(lp.get_error_msg(),"请输入账号")
def test_login_failed_no_password(self):
# 步骤:
# 1、登录页面-登录操作
lp = LoginPage(self.driver)
lp.login("z14737424527", "") # 输入密码为空
# 断言:
# 1、登录页面-获取未输入密码的错误提示信息
self.assertEqual(lp.get_error_msg(),"请输入密码")
ddt
账户的登录可以引发这样一类数据的问题:
- 同样是验证账号错误,可能有多个数据需要测试;
- 很多测试环境的时候我们要定义不同的数据,用老数据不行
- 涉及到数据复用的问题,很多地方都需要用到同样的数据
所以,如果将数据都放在一个地方,这样会更容易维护。比如:网址接口变了;正确的用户名密码变了
在逆向用例中,都有用到 用户名、密码和文本提示信息(期望数据)这三项,所以此时可以引入 ddt
在 PageObjects文件夹下建立的 login_page 文件中编写代码,用于获取提示的所有文本内容
login_page.py
# from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
# 简化页面元素定位
# 用户名输入框
user_input = (By.XPATH,"//div[@class='u-input box']/input[@name='email']")
# 密码输入框
psd_input = (By.XPATH, "//div[@class='u-input box']/input[@name='password']")
# 登录按钮
login_btn = (By.XPATH,"//a[@id='dologin']")
# 登录表单区域的提示信息
msg_login_form = (By.XPATH,"//div[@class='ferrorhead']")
# 初始化操作
# driver:WebDriver 这里是声明 driver 是 WebDriver类的实例对象
def __init__(self,driver:WebDriver):
# 初始化 dirver
self.driver = driver
# 定位到 iframe 并进行切换
driver.switch_to.frame(0)
pass
# 登录-元素操作
def login(self,username,password):
# 设置元素可见等待
# 显性等待时间为20秒,等待元素为登录按钮
WebDriverWait(self.driver,20).until(EC.visibility_of_element_located(self.login_btn))
# 找到用户名输入框输入用户名
self.driver.find_element(*self.user_input).send_keys(username)
# 找到密码输入框输入密码
self.driver.find_element(*self.psd_input).send_keys(password)
# 找到登录按钮进行点击
self.driver.find_element(*self.login_btn).click()
# 获取登录区域的提示信息
def get_error_msg(self):
WebDriverWait(self.driver,20).until(EC.visibility_of_element_located(self.msg_login_form))
# 返回文本值
eles = self.driver.find_elements(*self.msg_login_form)
if len(eles) == 1:
return eles[0].text
elif len(eles) > 1:
text_list = []
for el in eles:
text_list.append(el.text)
return text_list
对于逆向用例,比如还想再添加 用户名或密码输入错误、用户名和密码输入均为空等场景,这时候引入ddt,就会减少很多工作量,不用每个场景都写一个函数
在 TestCases 文件夹下的 test_login 文件下引入 ddt
test_login.py
import unittest
from selenium import webdriver
from PO.PageObjects.login_page import LoginPage
from PO.PageObjects.home_page import HomePage
import ddt
@ddt.ddt
class TestLogin(unittest.TestCase):
# 前置操作
def setUp(self) -> None:
# 访问163邮箱登录页面
self.driver = webdriver.Chrome()
self.driver.get("https://mail.163.com/")
self.driver.maximize_window()
# 清理操作
def tearDown(self) -> None:
self.driver.quit()
def test_login_success(self):
# 步骤:
# 1、登录页面-登录操作
LoginPage(self.driver).login("z14737424527","dasdgasd") # 输入账号密码
# 断言:
# 1、首页-获取元素是否存在 (进行断言操作,元素可见返回True)
self.assertTrue(HomePage(self.driver).get_element_exists())
cases =[
{"user":"","psd":"dasdgasd","check":"请输入账号"},
{"user": "z14737424527", "psd": "", "check": "请输入密码"},
{"user": "z1473", "psd": "dasdgasd", "check": "账号或密码错误"}
]
@ddt.data(*cases)
def test_login_failed(self,case):
# 步骤:
# 1、登录页面-登录操作
lp = LoginPage(self.driver)
lp.login(case["user"], case["psd"]) # 输入 cases 数据中的 账号和密码
# 断言:
# 1、登录页面-获取错误提示信息 进行对比
self.assertEqual(lp.get_error_msg(),case["check"])
拓展:
当前代码的逻辑是,每执行一次用例,都会打开一次浏览器,创建一个新的会话,这种方式是相对较保险的。
因为在 WebUI自动化中,为了保障自动化测试的质量,通常都是是采用时间换取效率。
但是,如果追求效率,所有用例的执行只打开一次浏览器进行操作,可以通过类方法实现,且可以指定用例执行的顺序。
在 TestCases 文件夹下的新建一个 test_login_2 文件
test_login_2.py
import unittest
from selenium import webdriver
from PO.PageObjects.login_page import LoginPage
from PO.PageObjects.home_page import HomePage
import ddt
@ddt.ddt()
class TestLogin(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
# 访问163邮箱登录页面
cls.driver = webdriver.Chrome()
cls.driver.get("https://mail.163.com/")
cls.driver.maximize_window()
@classmethod
def tearDownClass(cls) -> None:
cls.driver.quit()
# 每次执行用例都刷新页面,这样就可以让输入框中的内容恢复初始值
def setUp(self) -> None:
self.driver.refresh()
def test_login_02_success(self): # 该用例后执行
# 步骤:
# 1、登录页面-登录操作
LoginPage(self.driver).login("z14737424527","zb05270513") # 输入账号密码
# 断言:
# 1、首页-获取元素是否存在 (进行断言操作,元素可见返回True)
self.assertTrue(HomePage(self.driver).get_element_exists())
cases = [
{"user": "", "psd": "zb05270513", "check": "请输入帐号"},
{"user": "z14737424527", "psd": "", "check": "请输入密码"},
{"user": "z1473", "psd": "zb05270", "check": "帐号或密码错误"}
]
@ddt.data(*cases)
def test_login_01_failed(self,case): # 优先执行该用例
# 步骤:
# 1、登录页面-登录操作
lp = LoginPage(self.driver)
lp.login(case["user"], case["psd"]) # 输入 cases 数据中的 账号和密码
# print(case["user"], case["psd"], case["check"])
# 断言:
# 1、登录页面-获取错误提示信息 进行对比
# print(lp.get_error_msg())
self.assertEqual(lp.get_error_msg(),case["check"])
在实际项目中不推荐该种做法因为自动化测试本身就成本较高,且自动化测试的目的是为了保障系统稳定性,所以自动化测试脚本的稳定极其重要。