无论是手工测试还是自动化测试,最核心的任务就是编写测试用例、执行测试用例、输出测试报告以及维护测试用例。因此,如何提高自动化测试效率就等同于如何提高自动化编写、执行、维护测试用例的效率。当下最流行的PO模型和关键字驱动模型都是为了解决此问题,本文主要介绍一下PO模型
PO模型介绍
全称PageObject,也叫做POM模型,它是一种设计思想,不是一种规范,是为了解决自动化测试过程中随代码量的增加导致代码冗余,难以维护、难以扩展等事件的方案。在PO模型下,每个页面都对应到一个类中,每一个类都维护着该页面中的元素集和操作这些元素的方法,使用此模型的目的是使架构解耦合,让程序松耦合
案例演示
以登录为例,文件no_PO_Object.py
,使用PO模型对脚本代码进行优化
# no_PO_Object.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import pytest
login = [["","654321","账号不能为空"],["test","123456","用户名不正确。"],
["admin","","密码不能为空"],["admin","654321","用户名或密码不正确"],["admin","123456","用户中心"]]
class TestUser(object):
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
def teardown_class(self):
self.driver.quit()
@pytest.mark.parametrize("username,pwd,expected",login)
def test_user_login(self,username,pwd,expected):
self.driver.get("http://192.166.66.22:8080/user/login")
# 清空输入框后输入用户名
self.driver.find_element(By.NAME, "user").clear()
self.driver.find_element(By.NAME, "user").send_keys(username)
# 清空输入框后输入密码
self.driver.find_element(By.NAME, "pwd").clear()
self.driver.find_element(By.NAME, "pwd").send_keys(pwd)
# 点击【登录】
self.driver.find_element(By.CLASS_NAME, "btn").click()
if username == "admin" and pwd == "123456":
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.title_is(expected))
# 验证是否登录成功,根据当前页面标题进行判断
assert self.driver.title == expected
else:
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.alert_is_present())
alert = self.driver.switch_to.alert
# 验证报错信息是否正确
assert alert.text == expected
alert.accept()
将代码中容易变动的信息都拿出来,单独写个类中loginPage.py
,若前端属性值、元素或定位方式发生改变,只需要在此模块进行更改
# loginPage.py
from selenium.webdriver.common.by import By
class loginPage():
# 定义一个构造方法,初始化变量,在实例化时传入一个driver参数
def __init__(self,driver):
self.driver = driver
# 抽出定位和元素的属性值
user = (By.NAME, "user")
pwd = (By.NAME, "pwd")
btn = (By.CLASS_NAME, "btn")
url = "http://192.166.66.22:8080/user/login"
# 加载项目地址
def user_loginURL(self):
self.getURL(self.url)
# 元素操作
# 清空输入框后输入用户名
def user_input(self,username):
self.driver.find_element(*loginPage.user).clear()
self.driver.find_element(*loginPage.user).send_keys(username)
# 清空输入框后输入密码
def pwd_input(self,pwd):
self.driver.find_element(*loginPage.pwd).clear()
self.driver.find_element(*loginPage.pwd).send_keys(pwd)
# 点击登录按钮
def btn_click(self):
self.driver.find_element(*loginPage.btn).click()
# 绑定业务函数
# 对于有关联关系的操作可以进行业务绑定,比如登录
def login(self,username,pwd):
self.user_loginURL()
self.user_input(username)
self.pwd_input(pwd)
self.btn_click()
测试用例loginCase.py
的类中直接调用loginPage.py
中的方法
# loginCase.py
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import pytest
from PO.BasePage.loginPage import loginPage
login = [["","654321","账号不能为空"],["test","123456","用户名不正确。"],
["admin","","密码不能为空"],["admin","654321","用户名或密码不正确"],["admin","123456","用户中心"]]
class TestUser(object):
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
def teardown_class(self):
self.driver.quit()
@pytest.mark.parametrize("username,pwd,expected",login)
def test_user_login(self,username,pwd,expected):
# 把用例层面实例的driver传到loginPage中
self.login = loginPage(self.driver)
self.login.loginURL() # 访问项目地址
self.login.user_input(username) # 输入用户名
self.login.pwd_input(pwd) # 输入密码
self.login.btn_click() # 点击登录
# 上面loginPage.py中的业务绑定函数,此处可直接调用,省去调用上面4步单独的元素调用
# self.login.login(username,pwd)
if username == "admin" and pwd == "123456":
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.title_is(expected))
# 验证是否登录成功,根据当前页面标题进行判断
assert self.driver.title == expected
else:
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.alert_is_present())
alert = self.driver.switch_to.alert
# 验证报错信息是否正确
assert alert.text == expected
alert.accept()
至此代码就实现了下图中的转变方式,no_PO_Object.py
转为loginPage.py
+loginCase.py
通过上面的修改可以看到,在loginPage.py
中出现重复的定位元素操作,若页面元素较多,或进行多个页面操作时,会出现更多的重复定位操作,所以创建basePage.py
,对页面操作进一步优化,抽取页面中的公共方法
# basePage.py
class Browser_init():
def __init__(self,driver):
self.driver = driver
# 加载项目地址
def getURL(self,url):
self.driver.get(url)
# 元素定位
def locate_ele(self,locator):
ele = self.driver.find_element(*locator)
return ele
# 元素文本清空
def locate_clear(self,locator):
self.locate_ele(locator).clear()
# 元素文本输入
def locate_input(self,locator,text):
self.locate_ele(locator).send_keys(text)
# 元素点击
def locate_click(self,locator):
self.locate_ele(locator).click()
loginPage.py
中的代码修改如下
# loginPage.py
from selenium.webdriver.common.by import By
from PO.BasePage.basePage import Browser_init
class loginPage(Browser_init):
# 抽出定位和元素的属性值
user = (By.NAME, "user")
pwd = (By.NAME, "pwd")
btn = (By.CLASS_NAME, "btn")
url = "http://192.166.66.22:8080/user/login"
# 加载项目地址
def user_loginURL(self):
self.getURL(self.url)
# 元素操作
# 清空输入框后输入用户名
def user_input(self,username):
self.locate_clear(loginPage.user)
self.locate_input(loginPage.user,username)
# 清空输入框后输入密码
def pwd_input(self,pwd):
self.locate_clear(loginPage.pwd)
self.locate_input(loginPage.pwd,pwd)
# 点击登录按钮
def btn_click(self):
self.locate_click(loginPage.btn)
# 绑定业务函数
def login(self,username,pwd):
self.user_loginURL()
self.user_input(username)
self.pwd_input(pwd)
self.btn_click()
loginCase.py
的代码如下
# loginCase.py
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import pytest
from PO.BasePage.loginPage import loginPage
login = [["","654321","账号不能为空"],["test","123456","用户名不正确。"],
["admin","","密码不能为空"],["admin","654321","用户名或密码不正确"],["admin","123456","用户中心"]]
class TestUser(object):
def setup_class(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
def teardown_class(self):
self.driver.quit()
@pytest.mark.parametrize("username,pwd,expected",login)
def test_user_login(self,username,pwd,expected):
# 把用例层面实例的driver传到loginPage中
self.login = loginPage(self.driver)
self.login.login(username,pwd)
if username == "admin" and pwd == "123456":
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.title_is(expected))
# 验证是否登录成功,根据当前页面标题进行判断
assert self.driver.title == expected
else:
# 等待页面加载
WebDriverWait(self.driver, 3).until(EC.alert_is_present())
alert = self.driver.switch_to.alert
# 验证报错信息是否正确
assert alert.text == expected
alert.accept()
最终,源代码一分为三,可理解为基础层、页面层和业务层
- 基础层:BasePage,封装了基础的Selenium的原生方法,如定位元素、输入输出、元素点击等一些控件操作
- 页面层:loginPage,存放一些封装好的功能用例模块,继承基础层中的操作
- 业务层:loginCase,真正的测试用例的操作,用例的业务逻辑以及数据驱动,调用页面层中的方法
三者关系如图所示
通过以上对代码的拆分,可以看出PO模型的优缺点:
优点:
- 提升代码可维护性,PO模型是一种业务流程与页面元素操作分离的模式,使得测试代码更加清晰整洁,利于代码的可维护性
- 提高代码可读性,因为PO模型对代码进行了分层,而且也会集中管理一个页面内的公共方法,利于用例编写及代码可读性
- 提升代码复用性,页面对象和用例之间的相互分离,使得测试人员可集中管理元素对象
缺点:
- 由于根据项目流程进行了模块化处理, 造成项目结构复杂化
当然,对于以上代码仍可以进行细分,所以再次提醒,PO模型是一种思想,不是一种规范,每个人写出来的方式可能都不一样,没有对错之分,对于PO模型仁者见仁,智者见智