1. 什么是POM
Page Object Model 是ui自动化测试中常见的封装方式。
原理:将页面封装为PO对象,然后通过面向对象的方式实现UI自动化
2. 封装原则
- PO无需包含全部UI元素
- PO应当验证元素
- PO不应该包含断言
- PO不应该暴露元素
3. 怎么进行POM封装
面向对象:属性和方法
封装步骤:
- 创建类,代表页面
- 创建类的属性,代表页面中的元素
- 创建类的方法,代表页面中的交互动作
在项目新建文件user_po.py
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
# 1.创建类
class IndexPage:
"""首页:登录页面"""
def __init__(self, driver: Chrome): # 在进行实例化是被自动调用,可以接收参数
self.driver = driver
# 2.类的属性,即页面元素
btn_login = (By.XPATH, '//*[@id="user_head_tip"]/a[1]') # 立即登录按钮
ipt_username = (By.XPATH, '//*[@id="login-email-address"]') # 账号输入框
ipt_password = (By.XPATH, '//*[@id="login-password"]') # 密码输入框
btn_submit = (By.XPATH, '//*[@id="ajax-login-submit"]') # 登录按钮
msg = (By.XPATH, '//*[starts-with(@id,"fanwe_")]/table/tbody/tr/td[2]/div[2]') # 登录结果
# 3.类的方法,即交互动作
def login(self, username, password):
self.driver.find_element(*self.btn_login).click() # *表示元组解包
self.driver.find_element(*self.ipt_username).send_keys(username)
self.driver.find_element(*self.ipt_password).send_keys(password)
self.driver.find_element(*self.btn_submit).click()
# 显示等待:系统提示里不包含忘记密码并且系统系统不为空
WebDriverWait(self.driver, 10).until(
lambda x: "忘记密码?" not in self.driver.find_element(*self.msg).text and self.driver.find_element(
*self.msg).text != ""
)
msg = self.driver.find_element(*self.msg).text
return msg
# 1.一个页面一个类
class DealPage:
"""交易页面:投资"""
# 2.类的属性,即页面元素
ipt_money = (By.XPATH, '//*[@id="J_BIDMONEY"]') # 投资金额
btn_tz_submit = (By.XPATH, '//*[@id="tz_link"]') # 立即投资
ipt_pay_password = (By.XPATH, '//*[@id="J_bid_password"]') # 支付密码
btn_pay_submit = (By.XPATH, '//*[@id="J_bindpassword_btn"]') # 确定
msg = (By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]')
def __init__(self, driver: Chrome): # 在进行实例化是被自动调用,可以接收参数
self.driver = driver
# 3.类的方法,即交互动作
def pay(self, money, pay_password):
self.driver.find_element(*self.ipt_money).send_keys(money)
self.driver.find_element(*self.btn_tz_submit).click()
self.driver.find_element(*self.ipt_pay_password).send_keys(pay_password)
self.driver.find_element(*self.btn_pay_submit).click()
msg = WebDriverWait(self.driver, 10).until(
lambda x: self.driver.find_element(
By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]'
).text
)
return msg
if __name__ == '__main__':
driver = Chrome()
driver.implicitly_wait(10) # 隐式等待
driver.get('http://47.107.116.139/fangwei/index.php')
page = IndexPage(driver)
msg = page.login('admin', 'msjy123')
print(msg)
driver.get('http://47.107.116.139/fangwei/index.php?ctl=deal&id=25370&preview=1')
page = DealPage(driver)
msg = page.pay(100, 'msjy123')
print(msg)
driver.quit()
4. 引入pytest
- 自动判断用例执行结果
- 统计用例成功数量
- 统一形成测试报告
4.1 安装
pip install pytest
4.2 编写夹具fixture
测试用例所依赖的一些组件应该在夹具中设置就绪:例如启动浏览器,最大化浏览器等。在根目录创建conftest.py文件,在该文件中写夹具
import pytest
from selenium.webdriver import Chrome
# scope用来指定夹具作用域,function指函数,module指模块,如果设置scope=function表示每执行一个函数都会进行浏览器重启和关闭
@pytest.fixture(scope='module') # 这里设置scope=module是因为如果为函数级别的话,我们在test_user中第1个用例如果关闭了浏览器第二个用例就需要重新登录
def driver():
driver = Chrome()
driver.implicitly_wait(5)
driver.maximize_window()
yield driver
driver.quit()
创建test_user.py,代码如下:
from user_po import IndexPage, DealPage
# 在用例中使用夹具:将夹具名称写在参数当中
def test_login(driver):
driver.get('http://47.107.116.139/fangwei/index.php')
page = IndexPage(driver)
msg = page.login('admin', 'msjy123')
assert msg == '成功登录'
def test_deal(driver):
driver.get('http://47.107.116.139/fangwei/index.php?ctl=deal&id=25370&preview=1')
page = DealPage(driver)
msg = page.pay(100, 'msjy123')
assert msg == '投标成功!'
在pycharm终端输入pytest即可自动执行test_user.py中的两个测试用例。
4.3 编写测试用例
以登录功能为例,存在以下几种情况,不同的输入会有不同的输出结果,
- 用户名密码为空:Email格式错误,请重新输入或者昵称格式错误,请重新输入
- 用户名正确,密码为空:密码格式错误,请重新输入
- 错误用户名,密码正确:用户不存在
- 正确用户名,错误密码:密码错误
- 正确用户名,正确密码:成功登录
- 首先新建ddt_login.csv,用于存放我们的用例数据,做参数化使用
用户名,密码,登录结果
,,Email格式错误,请重新输入或者昵称格式错误,请重新输入
admin,,密码格式错误,请重新输入
asasasddd,msjy123,用户不存在
admin,msjy123456,密码错误
admin,msjy123,成功登录
接下来将我们的test_user.py代码修改一下,因为输入不再是固定的,所以使用参数传入:
import csv
import pytest
from user_po import IndexPage, DealPage
# 读取csv文件数据作为参数化数据,使用装饰器进行参数化
@pytest.mark.parametrize("data", csv.DictReader(open("ddt_login.csv", encoding="utf-8-sig")))
# 在用例中使用夹具:将夹具名称写在参数当中
def test_login(driver, data):
driver.get('http://47.107.116.139/fangwei/index.php')
page = IndexPage(driver)
msg = page.login(data["用户名"], data["密码"])
assert msg == data["登录结果"]
def test_deal(driver):
driver.get('http://47.107.116.139/fangwei/index.php?ctl=deal&id=25370&preview=1')
page = DealPage(driver)
msg = page.pay(100, 'msjy123')
assert msg == '投标成功!'
5. po封装管理后台
要封装后台管理的po,如果新建po文件,如果每个po都新建一个文件会导致文件过多,因此我们需要将原来的user_po.py文件名修改为pages.py,然后在里面添加类就可以了。然后添加类的时候我们可以发现每个类都有一个init方法,造成了代码重复,因此我们使用BasePage抽象类,它表示所有页面共用的代码,我们将init方法写在BasePage类中,后续使用时只需要继承即可。修改后代码如下:
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
# 抽象类,所有页面的公共代码,需要使用时继承即可
class BasePage:
def __init__(self, driver: Chrome): # 在进行实例化是被自动调用,可以接收参数
self.driver = driver
# 1.创建类
class IndexPage(BasePage):
"""首页:登录页面"""
# 2.类的属性,即页面元素
btn_login = (By.XPATH, '//*[@id="user_head_tip"]/a[1]') # 立即登录按钮
ipt_username = (By.XPATH, '//*[@id="login-email-address"]') # 账号输入框
ipt_password = (By.XPATH, '//*[@id="login-password"]') # 密码输入框
btn_submit = (By.XPATH, '//*[@id="ajax-login-submit"]') # 登录按钮
msg = (By.XPATH, '//*[starts-with(@id,"fanwe_")]/table/tbody/tr/td[2]/div[2]') # 登录结果
# 3.类的方法,即交互动作
def login(self, username, password):
self.driver.find_element(*self.btn_login).click() # *表示元组解包
self.driver.find_element(*self.ipt_username).send_keys(username)
self.driver.find_element(*self.ipt_password).send_keys(password)
self.driver.find_element(*self.btn_submit).click()
# 显示等待:系统提示里不包含忘记密码并且系统系统不为空
WebDriverWait(self.driver, 10).until(
lambda x: "忘记密码?" not in self.driver.find_element(*self.msg).text and self.driver.find_element(
*self.msg).text != ""
)
msg = self.driver.find_element(*self.msg).text
return msg
# 1.一个页面一个类
class DealPage(BasePage):
"""交易页面:投资"""
# 2.类的属性,即页面元素
ipt_money = (By.XPATH, '//*[@id="J_BIDMONEY"]') # 投资金额
btn_tz_submit = (By.XPATH, '//*[@id="tz_link"]') # 立即投资
ipt_pay_password = (By.XPATH, '//*[@id="J_bid_password"]') # 支付密码
btn_pay_submit = (By.XPATH, '//*[@id="J_bindpassword_btn"]') # 确定
msg = (By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]')
# 3.类的方法,即交互动作
def pay(self, money, pay_password):
self.driver.find_element(*self.ipt_money).send_keys(money)
self.driver.find_element(*self.btn_tz_submit).click()
self.driver.find_element(*self.ipt_pay_password).send_keys(pay_password)
self.driver.find_element(*self.btn_pay_submit).click()
msg = WebDriverWait(self.driver, 10).until(
lambda x: self.driver.find_element(
By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]'
).text
)
return msg
然后我们将之前写的admin.py中管理后台的代码进行po封装,同样是创建类,属性和方法,写入pages.py,如下:
import time
from selenium.webdriver import Chrome
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait
from funcs import img1code, is_login
# 抽象类,所有页面的公共代码,需要使用时继承即可
class BasePage:
def __init__(self, driver: Chrome): # 在进行实例化是被自动调用,可以接收参数
self.driver = driver
# 1.创建类
class IndexPage(BasePage):
"""首页:登录页面"""
# 2.类的属性,即页面元素
btn_login = (By.XPATH, '//*[@id="user_head_tip"]/a[1]') # 立即登录按钮
ipt_username = (By.XPATH, '//*[@id="login-email-address"]') # 账号输入框
ipt_password = (By.XPATH, '//*[@id="login-password"]') # 密码输入框
btn_submit = (By.XPATH, '//*[@id="ajax-login-submit"]') # 登录按钮
msg = (By.XPATH, '//*[starts-with(@id,"fanwe_")]/table/tbody/tr/td[2]/div[2]') # 登录结果
# 3.类的方法,即交互动作
def login(self, username, password):
self.driver.find_element(*self.btn_login).click() # *表示元组解包
self.driver.find_element(*self.ipt_username).send_keys(username)
self.driver.find_element(*self.ipt_password).send_keys(password)
self.driver.find_element(*self.btn_submit).click()
# 显示等待:系统提示里不包含忘记密码并且系统系统不为空
WebDriverWait(self.driver, 10).until(
lambda x: "忘记密码?" not in self.driver.find_element(*self.msg).text and self.driver.find_element(
*self.msg).text != ""
)
msg = self.driver.find_element(*self.msg).text
return msg
# 1.一个页面一个类
class DealPage(BasePage):
"""交易页面:投资"""
# 2.类的属性,即页面元素
ipt_money = (By.XPATH, '//*[@id="J_BIDMONEY"]') # 投资金额
btn_tz_submit = (By.XPATH, '//*[@id="tz_link"]') # 立即投资
ipt_pay_password = (By.XPATH, '//*[@id="J_bid_password"]') # 支付密码
btn_pay_submit = (By.XPATH, '//*[@id="J_bindpassword_btn"]') # 确定
msg = (By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]')
# 3.类的方法,即交互动作
def pay(self, money, pay_password):
self.driver.find_element(*self.ipt_money).send_keys(money)
self.driver.find_element(*self.btn_tz_submit).click()
self.driver.find_element(*self.ipt_pay_password).send_keys(pay_password)
self.driver.find_element(*self.btn_pay_submit).click()
msg = WebDriverWait(self.driver, 10).until(
lambda x: self.driver.find_element(
By.XPATH, '//*[@id="fanwe_success_box"]/table/tbody/tr/td[2]/div[2]'
).text
)
return msg
class AdminLoginPage(BasePage):
"""后台管理登录页面"""
ipt_username = (By.XPATH, '/html/body/form/table/tbody/tr/td[3]/table/tbody/tr[2]/td[2]/input')
ipt_password = (By.XPATH, '/html/body/form/table/tbody/tr/td[3]/table/tbody/tr[3]/td[2]/input')
ipt_verify = (By.XPATH, '/html/body/form/table/tbody/tr/td[3]/table/tbody/tr[5]/td[2]/input')
img_verify = (By.XPATH, '//*[@id="verify"]')
btn_submit = (By.XPATH, '//*[@id="login_btn"]')
def login(self, username, password):
self.driver.find_element(*self.img_verify).screenshot("../temp/code.png")
code = img1code("../temp/code.png")
self.driver.find_element(*self.ipt_username).send_keys(username)
self.driver.find_element(*self.ipt_password).send_keys(password)
self.driver.find_element(*self.ipt_verify).send_keys(code)
self.driver.find_element(*self.btn_submit).click()
time.sleep(1)
return is_login(self.driver) # 使用is_login函数返回值作为交互返回值
class AdminIndexPage(BasePage):
"""后台管理首页"""
ifm_top = (By.XPATH, '/html/frameset/frame[1]')
ifm_left = (By.XPATH, '//*[@id="menu-frame"]')
ifm_main = (By.XPATH, '//*[@id="main-frame"]')
def to_deal(self):
self.driver.refresh()
# 进入框架
iframe = self.driver.find_element(*self.ifm_top)
self.driver.switch_to.frame(iframe)
self.driver.find_element(By.LINK_TEXT, '贷款管理').click() # 点击贷款管理
self.driver.switch_to.default_content() # 退出框架
iframe = self.driver.find_element(*self.ifm_left)
self.driver.switch_to.frame(iframe)
self.driver.find_element(By.LINK_TEXT, '全部贷款').click() # 点击全部贷款
self.driver.switch_to.default_content() # 退出框架
iframe = self.driver.find_element(*self.ifm_main)
self.driver.switch_to.frame(iframe)
return AdminDealPage(self.driver)
class AdminDealPage(BasePage):
"""贷款管理页面"""
btn_new_deal = (By.XPATH, '/html/body/div[2]/div[3]/input[1]') # 新增贷款按钮
tr_deal = (By.XPATH, '//tr[contains(@class,"row")]') # 列表所有
def new_deal(self):
self.driver.find_element(*self.btn_new_deal).click()
return AdminNewDealPage(self.driver) # 返回po表示已经进入新增贷款页面
class AdminNewDealPage(BasePage):
"""新增贷款页面"""
ipt_name = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[4]/td[2]/input') # 贷款名称
ipt_shor_name = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[5]/td[2]/input') # 简短名称
ipt_username = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[6]/td[2]/input[1]') # 会员名称
btn_username = (By.XPATH, '//strong[text()="beifan"]')
btn_city = (By.XPATH, '//*[@id="citys_box"]/div[1]/div[2]/input[1]') # 所在城市
sel_cate = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[8]/td[2]/select') # 分类-房产抵押
btn_show_upload = (By.XPATH,
'/html/body/div[2]/form/table[1]/tbody/tr[14]/td[2]/span/div[1]/div/div/button') # 图片上传按钮
btn_show_local_upload = (By.XPATH, '/html/body/div[6]/div[1]/div[2]/div/div[1]/ul/li[2]') # 本地上传按钮
ipt_upload = (By.XPATH, '//input[@type="file"]') # 发送文件
btn_submit_upload = (By.XPATH, '/html/body/div[6]/div[1]/div[3]/span[1]/input') # 确定上传按钮
sel_type = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[15]/td[2]/select') # 借款用途
sel_contract = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[17]/td[2]/select') # 借款合同范本
sel_tcontract = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[18]/td[2]/select') # 转让合同范本
ipt_amount = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[19]/td[2]/input') # 借款金额
ipt_rate = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[27]/td[2]/input') # 年利率
ipt_enddate = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[28]/td[2]/input') # 筹标期限
btn_status = (By.XPATH, '/html/body/div[2]/form/table[1]/tbody/tr[33]/td[2]/label[1]/input') # 借款状态
ipt_start_time = (By.XPATH, '//*[@id="start_time"]') # 开始时间
btn_submit = (By.XPATH, '/html/body/div[2]/form/table[6]/tbody/tr[2]/td[2]/input[4]') # 新增提交按钮
msg=(By.XPATH, '/html/body/div/table/tbody/tr[3]/td')
def submit(self, data): # data是包含了多个参数的字典
"""提交新的贷款"""
# 贷款名称
self.driver.find_element(*self.ipt_name).send_keys(data['name'])
# 简短名称
self.driver.find_element(*self.ipt_shor_name).send_keys(data['shor_name'])
# 会员名称
self.driver.find_element(*self.ipt_username).send_keys(data['username'])
self.driver.find_element(*self.btn_username).click()
# 城市
self.driver.find_element(*self.btn_city).click()
# 分类-房产抵押
el = self.driver.find_element(*self.sel_cate)
Select(el).select_by_visible_text(data['cate'])
# 图片上传
self.driver.find_element(*self.btn_show_upload).click()
self.driver.find_element(*self.btn_show_local_upload).click()
self.driver.find_element(*self.ipt_upload).send_keys(data['upload'])
self.driver.find_element(*self.btn_submit_upload).click()
# 借款用途
el = self.driver.find_element(*self.sel_type)
Select(el).select_by_visible_text(data['type'])
# 借款合同范本
el = self.driver.find_element(*self.sel_contract)
Select(el).select_by_visible_text(data['contract'])
# 转让合同
el = self.driver.find_element(*self.sel_tcontract)
Select(el).select_by_visible_text(data['tcontract'])
# 借款金额
el = self.driver.find_element(*self.ipt_amount)
el.clear()
el.send_keys(data['amount'])
# 年利率
el = self.driver.find_element(*self.ipt_rate)
el.clear()
el.send_keys(data['rate'])
# 筹标期限
el = self.driver.find_element(*self.ipt_enddate)
el.clear()
el.send_keys(data['enddate'])
# 借款状态
self.driver.find_element(*self.btn_status).click()
# 开始时间
el = self.driver.find_element(*self.ipt_start_time)
self.driver.execute_script("arguments[0].scrollIntoView()", el)
self.driver.execute_script(f"arguments[0].value='{data['start_time']}'", el)
# 新增提交
self.driver.find_element(*self.btn_submit).click()
# 系统提示
el = self.driver.find_element(*self.msg)
return el.text
完成后我们创建test_admin.py文件,去编写测试用例,我们在编写新增贷款流程的测试用例的时候,首先需要登录,我们可以在conftest.py中新建一个fixture夹具,如下:
import pytest
from selenium.webdriver import Chrome
from funcs import save_cookies, load_cookies,is_login
from pages import AdminLoginPage
# scope用来指定夹具作用域,function指函数,module指模块,如果设置scope=function表示每执行一个函数都会进行浏览器重启和关闭
@pytest.fixture(scope='module') # 这里设置scope=module是因为如果为函数级别的话,我们在test_user中第1个用例如果关闭了浏览器第二个用例就需要重新登录
def driver():
driver = Chrome()
driver.implicitly_wait(5)
driver.maximize_window()
yield driver
driver.quit()
@pytest.fixture(scope='session')
def admin_driver():
"""已经登陆的浏览器,给test_admin使用"""
driver = Chrome()
driver.implicitly_wait(5)
driver.maximize_window()
load_cookies(driver)
# 判断:只有未登录才进行登录流程
if is_login(driver) is False:
page = AdminLoginPage(driver) # 实例化
assert page.login('admin', 'msjy123') is True
yield driver
save_cookies(driver)
driver.quit()
这样我们在test_admin.py中就可以使用admin_driver
from pages import *
def test_new_deal(admin_driver):
"""已经登录成功状态"""
page = AdminIndexPage(admin_driver)
page = page.to_deal() # 跳转到贷款管理
page = page.new_deal() # 跳转到新增贷款
# data表示输入的数据
data = {
'name': '借款1亿买别墅',
'shor_name': '买别墅',
'username': 'beifan',
'cate': '|--房产抵押标',
'upload': r'D:\pythonProject2\code.png',
'type': '个人消费',
'contract': '等额本息合同范本【担保】',
'tcontract': '付息还本合同范本【普通】',
'amount': '100000000',
'rate': '5',
'enddate': '30',
'start_time': '2023-12-25 18:02:02'
}
msg = page.submit(data)
assert msg == '添加成功'
也可以在test_admin.py中继续添加测试用例,测试不同的输入数据和结果。这里不再举例。最后在终端运行可以看到测试通过,也可以创建main.py进行运行
import pytest
if __name__ == '__main__':
pytest.main()
写到这里POM封装就基本完成了,整理下项目目录,如下: