基于Python Selenium Unittest PO设计模式
一、什么是PO设计模式(Page Object Model)
1、Page Object是一种设计模式,它主要体现在对界面交互细节的封装上,使测试用例更专注于业务的操作,从而提高测试用例的可维护性。
2、一般PO设计模式有三层
第一层:
- 对Selenium 进行二次封装,定义一个所有页面都继承的 BasePage ,
- 封装 Selenium 基本方法 例如:元素定位,元素等待,导航页面 ,
- 不需要全部封装,用到多少方法就封装多少方法。
第二层:
- 页面元素进行分离,每个元素只定位一次,隔离定位,如果页面改变,只需要改变相应的元素定位;
- 业务逻辑分离 或 操作元素动作分离
第三层:
- 使用单元测试框架对业务逻辑进行测试
二、为什么要使用PO设计模式
- 页面频繁变化,(页面html结构等变化)导致页面UI元素频繁变动,元素定位改变
- 传统线性自动化(面向过程开发),用例中需要反复的定位同一个元素
- 每当页面发生变化的时候,需要在用例中寻找变动的部分,工作量大,容易产生遗漏,不容易维护
三、PO设计模式的六大原则
- 公共方法代表页面提供的服务
- 不要暴露细节
- 不要在封装的框架中做断言
- 方法可以return到新打开的页面
- 不要对所有元素建模,仅对自己关注的元素建模
- 相同的行为会产生不同的结果,可以封装不同的结果
四、PO设计模式实例
以公司的统一登录作为项目例子,用PO设计模式实现登陆:
1、手工用例:
用例编号 | 标题 | 前置条件 | 操作步骤 | 预期结果 | 实际结果 |
T-001 | 登录成功 | 输入正确的用户名和密码 |
1、打开统一登录页 2、输入用户名 3、输入密码 4、点击登录按钮 |
登录成功,正确跳转到应用系统界面 | XXX |
T-002 | 登录失败 | 输入错误的用户名 |
1、打开统一登录页 2、输入用户名 3、输入密码 4、点击登录按钮 |
登录失败,正确提示用户名或密码不正确 | XXX |
2、用PO模式实现自动化用例
项目目录
Base.py
class BasePage: '''基础Page层,封装一些常用方法''' '''第一层:对selenium进行二次封装,定义一个所有页面都继承的BasePage, 封装selenium基本方法,如元素定位、元素等待、导航页面, 不需要全部封装,用到多少方法就封装多少方法''' def __init__(self,driver): self.driver = driver # 打开页面 def open(self, url=None): '''open()方法用于打开网页,它接收一个url参数,默认为None,如果url参数为None, 则默认打开子类中定义的url''' if url is None: self.driver.get(self.url) else: self.driver.get(url) '''以下的定位方法:selenium提供的元素定位方法很长,这里做了简化,只是为了在子类中使用更加简便''' # id定位 def by_id(self,id): return self.driver.find_element_by_id(id) # name定位 def by_name(self,name): return self.driver.find_element_by_name(name) # class定位 def by_class(self,class_name): return self.driver.find_element_by_class_name(class_name) # xpath定位 def by_xpath(self,xpath): return self.driver.find_element_by_xpath(xpath) # CSS定位 def by_css(self,css): return self.driver.find_element_by_css_selector(css) # 获取title def get_title(self): return self.driver.title # 获取页面text,仅使用xpath定位 def get_text(self,xpath): return self.by_xpath(xpath).text # 获取页面的URL def get_current_url(self): return self.driver.current_url # 执行js脚本 def js(self,script): self.driver.execute_script(script)
login_page.py
from Page import Base # 创建LoginPage类继续BasePage类 class LoginPage(Base.BasePage): '''统一平台登录Page层,登录页面封装操作到的元素''' '''第二层:页面元素进行分离,每个元素只定位一次,操作元素动作分离''' # 定义url变量,供父类中的open()方法使用 url ="https://test01....cn/#/login" # 用户名输入框定位 def form_username(self,user_name): return self.by_id("name").send_keys(user_name) # 使用了父类的self.by_id()方法定位元素,简洁了不少 # 密码输入框定位 def form_password(self,pass_word): return self.by_id("password").send_keys(pass_word) # 登录按钮定位 def button_login(self): return self.by_xpath("//*[text()='登录']").click()
test_login.py
from Page import login_page import unittest from selenium import webdriver from time import sleep from selenium.webdriver.common.keys import Keys from CommonMethod import LogUtil logger = LogUtil.logs() # 调用封装的日志方法,将日志输出到控制台以及写入文件 class LoginCase(unittest.TestCase): '''第三层:用单元测试框架对业务逻辑进行测试''' '''使用LoginPage类及它所继承的父类中的方法''' @classmethod def setUpClass(cls): # 实例化webdriver,俗称:打开浏览器 cls.driver = webdriver.Firefox(executable_path='E:\\UI test\\UnittestProject\\Driver\\geckodriver.exe') cls.driver.implicitly_wait(10) @classmethod def tearDownClass(cls): cls.driver.quit() def test_login_success(self): page = login_page.LoginPage(self.driver) # 需要用到哪个Page类时,只需要将它传入浏览器驱动,就可以使用该类中提供的方法了 page.open() page.form_username("XXX") page.form_password("123456") page.button_login() sleep(2) self.assertEqual(page.get_current_url(), "https://test01....cn/#/home") print("登录成功,用例执行结果通过,当前的url为"+ page.get_current_url()) sleep(1) def test_login_fail(self): page = login_page.LoginPage(self.driver) page.open() page.form_username("XXX11") page.form_password("123456") page.button_login() self.assertNotEqual(page.get_current_url(), "https://test01....cn/#/home") print("登录失败,用例执行结果通过,当前的url为"+ page.get_current_url()) page.form_username(Keys.CONTROL+'a') # 输入组合键Ctrl+a,全选输入框内容 page.form_username(Keys.BACK_SPACE) # 删除键,删除选中的内容 page.form_password(Keys.CONTROL + 'a') page.form_password(Keys.BACK_SPACE) sleep(1) if __name__ == '__main__': unittest.main(verbosity=2)
执行结果
在test_login.py中有调用封装的日志方法,这里把封装的日志附上,在CommonMethod目录下的LogUtil.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import logging import logging.handlers import os import time class logs(object): def __init__(self): self.logger=logging.getLogger("") # 设置输出的等级 LEVELS={ 'NOSET':logging.NOTSET, 'DEBUG':logging.DEBUG, 'INFO':logging.INFO, 'WARNING':logging.WARNING, 'ERROR':logging.ERROR, 'CRITICAL':logging.CRITICAL } # 创建文件目录 logs_dir = "E:\\UI test\\UnittestProject\\TestLog" if os.path.exists(logs_dir) and os.path.isdir(logs_dir): pass else: os.mkdir(logs_dir) # 修改log保存位置 timestamp = time.strftime("%Y-%m-%d %H-%M-%S", time.localtime()) logfilename = '%s.txt' % timestamp logfilepath = os.path.join(logs_dir, logfilename) rotatingFileHandler = logging.handlers.RotatingFileHandler(filename=logfilepath, maxBytes=1024 * 1024 * 50, backupCount=5) # 设置输出格式 formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S') rotatingFileHandler.setFormatter(formatter) # 控制台句柄 console = logging.StreamHandler() console.setLevel(logging.NOTSET) console.setFormatter(formatter) # 添加内容到日志句柄中 self.logger.addHandler(rotatingFileHandler) self.logger.addHandler(console) self.logger.setLevel(logging.NOTSET) def info(self, message): self.logger.info(message) def debug(self, message): self.logger.debug(message) def warning(self, message): self.logger.warning(message) def error(self, message): self.logger.error(message)
五、其他补充
1、相同的行为会产生不同的结果,可以封装不同的结果:在login_page针对【登录】按钮封装了2个方法
2、方法可以return到新打开的页面:在login_page针对【登录】按钮封装,封装了之后要return新页面或其他信息。test_login调用时命名变量来接收这个函数就行了,比如indexurl = page.button_login_success(),在后面断言可以用indexurl变量来跟预期的url断言
# 登录失败封装 def button_login_fail(self): self.by_xpath("//span[text()='登录']").click() toast = self.by_xpath("//p[text()='账号或密码错误!']").text return toast # 登录成功封装 def button_login_success(self): self.by_xpath("//span[text()='登录']").click() sleep(2) windows = self.driver.window_handles# 获取打开的多个窗口句柄 self.driver.switch_to.window(windows[-1])# 切换到当前最新打开的窗口 indexurl = self.get_current_url() return indexurl
3、断言:可以通过url、页面标题、text来断言
'''断言跳转的地址,通过try except语句块来进行测试断言,在实际自动化测试脚本开发中,经常要用到处理异常''' try: self.assertEqual(indexurl,"https://qa-xxxt/add") print("点击创建,正确跳转到新页面" + indexurl) except AssertionError as msg: print("没有跳转到正确页面,当前跳转的地址为"+addurl+"\n报错信息如下"+format(msg)) '''当断言失败时会抛出异常测试用例执行失败,输出提示信息后重新将异常抛出,即raise, 若不重新抛出,用例则永远是显示执行成功的,因为它把异常处理掉了''' raise msg
try: self.assertEqual(toast, "账号或密码错误!") print("登录失败用例场景执行通过,正确弹出提示信息为:" + toast) except AssertionError as msg: print("错误提示语与预期结果不一致,请检查"+ format(msg)) raise msg
source = self.driver.page_source # 获取页面源码 try: self.assertIn("2106000013",source) # 断言搜索值是否存在页面源码中 print("正确搜索出该编号数据") excepttotal ="1" self.assertEqual(total,excepttotal) # 断言total值是否为1 print("底部分页统计正确,搜索出"+total+"条数据") except AssertionError as msg: print("搜索的数据不正确"+format(msg)) raise msg