一、什么是基础层
基础层承担了整个UI自动化框架的基础任务,后续框架开发的底层代码。
设计思路:本层主要对基本的方法如定位方法,断言基本类,混淆数据处理,驱动选择等方法提到的一个概念层,分一个主目录存放 目录设计:BasePage.py 基本类 封装基本的元素定位,元素操作类和方法 BaseAsert.py 断言类,异常处理类封装,驱动类封装等等。
1、基础层的作用
1、框架中设计基础层实际是为了应对后续繁杂的业务和逻辑代码,试想如果没有基础层的处理,框架后续交互使用时大量测试人员进入到脚本开发,每个人人都在代码里面写重复的代码或者说他们内部之间协定互相引用公共类,这样对于框架的健壮性可扩展性是不强的。如果最开始框架设计时就给与一个基础层和说明,后续交互后测试人员时都引用基础层的代码即可。
2、基于pageobject的代码设计思路,需要基础层的出现,分离公共代码和其他层代码的耦合
2、基础层有什么
当前的目录设计下我是写了这么几个py文件,如下
- BaseAssert.py 断言基类
- BaseLoggers.py 用例日志打印基类
- BasePage.py 页面对象基类
- BrowserPage.py 浏览器操作基类
- comsrc.py 公共方法基类
- decorator.py 公共装饰器类
- Driver.py 驱动以及运行选择基类
- exceptions.py 异常处理类
- AddCookies.py 鉴权基类
当前只是完成了这么几个基类,后续随着框架的完善,会新增部分代码
3、poium借鉴式开发的原因
- 我的代码思路实现在之前的书籍上也有大家可以看看,但是当我看到poium开源代码后,我发现这个框架里有很多我想要的东西,那么为什么不在巨人的肩膀上完善后续框架的开发呢。
- 不是说poium的代码就不需要任何修改直接照搬,实际还是要根据框架的需要来修改源代码,比如日志的输出,新增用例的运行和运行环境的选择等
- 这里还是非常感谢poium的作者,让我有学习提升的机会,特此声明其中BasePage.py、BrowserPage.py文件大体内容都是poium开源代码里面的。
二、基类文件BasePage.py开发
UI自动化实际大家接触的时候最开始学习到的就是元素定位,比如形如find_element_by_id()的几大定位方法,那么本文件中对这些操作都做了一次性的封装,
开发PageObject类
web端的页面对象处理需要使用到PageObject类,本类基本上处理了浏览器初始化时遇到的公共函数,以下对各个类进行代码演示
class PageObject(object):
"""
Page Object pattern.
"""
def __init__(self, driver, url=None):
"""
:param driver: `selenium.webdriver.WebDriver` Selenium webdriver instance
:param url: `str`
Root URI to base any calls to the ``PageObject.get`` method. If not defined
in the constructor it will try and look it from the webdriver object.
"""
self.driver = driver
self.root_uri = url if url else getattr(self.driver, 'url', None)
def get(self, uri):
"""
:param uri: URI to GET, based off of the root_uri attribute.
"""
root_uri = self.root_uri or ''
self.driver.get(root_uri + uri)
self.driver.implicitly_wait(4)
def add_cookies(self, cookies_dict):
self.driver.add_cookie(cookies_dict)
def get_all_cookies(self):
return self.driver.get_cookies()
def delete_all_cookie(self):
self.driver.delete_all_cookies()
def get_one_cookies(self, name):
return self.driver.get_cookie(name)
def delete_one_cookies(self, name):
self.driver.delete_cookie(name)
def get_page_source(self):
return self.driver.page_source
单独拿出get()函数来分析,此函数使用到的host+path格式的url拼接跳转,
def get(self, uri):
"""
:param uri: URI to GET, based off of the root_uri attribute.
"""
root_uri = self.root_uri or ''
self.driver.get(root_uri + uri)
self.driver.implicitly_wait(4)
开发Element类
接下来看一下另外一个主类Element,对元素定位进行了公共的封装,以前我再书籍中写到的定位元素封装使通过对yaml文件中形如这样的数据xpath->//button[contains(text(),‘确认支付’)],进行分割识别定位方式和定位的元素信息,而在这里换了另外一种更为简洁的方法来完成,依赖键值对的特性来完成。这里我截取了部分核心代码供参考
class Element(object):
"""
元素对象
"""
def __init__(self, timeout=5, describe="undefined", index=0, **kwargs):
self.timeout = timeout
self.index = index
self.desc = describe
if not kwargs:
raise ValueError("Please specify a locator")
if len(kwargs) > 1:
raise ValueError("Please specify only one locator")
self.kwargs = kwargs
self.k, self.v = next(iter(kwargs.items()))
if self.k not in LOCATOR_LIST.keys():
raise FindElementTypesError("Element positioning of type '{}' is not supported.".format(self.k))
def __get__(self, instance, owner):
if instance is None:
return None
Browser.driver = instance.driver
return self
def __set__(self, instance, value):
self.__get__(instance, instance.__class__)
self.send_keys(value)
@func_set_timeout(0.5)
def __elements(self, key, vlaue):
elems = Browser.driver.find_elements(by=key, value=vlaue)
return elems
def __find_element(self, elem):
"""
Find if the element exists.
"""
for i in range(self.timeout):
try:
elems = self.__elements(elem[0], elem[1])
except FunctionTimedOut:
elems = []
if len(elems) == 1:
# app.logger.info("✅ Find element: {by}={value} ".format(by=elem[0], value=elem[1]))
logger.info("✅ Find element: {by}={value} ".format(by=elem[0], value=elem[1]))
break
elif len(elems) > 1:
logger.info("❓ Find {n} elements through: {by}={value}".format(
n=len(elems), by=elem[0], value=elem[1]))
break
else:
sleep(1)
else:
error_msg = "❌ Find 0 elements through: {by}={value}".format(by=elem[0], value=elem[1])
logger.error(error_msg)
print(error_msg)
raise NoSuchElementException(error_msg)
def __get_element(self, by, value):
"""
根据传入的定位方式获取到页面对象供后续调用
"""
# selenium
if by == "id_":
self.__find_element((By.ID, value))
elem = Browser.driver.find_elements_by_id(value)[self.index]
elif by == "name":
self.__find_element((By.NAME, value))
elem = Browser.driver.find_elements_by_name(value)[self.index]
elif by == "class_name":
self.__find_element((By.CLASS_NAME, value))
elem = Browser.driver.find_elements_by_class_name(value)[self.index]
elif by == "tag":
self.__find_element((By.TAG_NAME, value))
elem = Browser.driver.find_elements_by_tag_name(value)[self.index]
elif by == "link_text":
self.__find_element((By.LINK_TEXT, value))
elem = Browser.driver.find_elements_by_link_text(value)[self.index]
elif by == "partial_link_text":
self.__find_element((By.PARTIAL_LINK_TEXT, value))
elem = Browser.driver.find_elements_by_partial_link_text(value)[self.index]
elif by == "xpath":
self.__find_element((By.XPATH, value))
elem = Browser.driver.find_elements_by_xpath(value)[self.index]
elif by == "css":
self.__find_element((By.CSS_SELECTOR, value))
elem = Browser.driver.find_elements_by_css_selector(value)[self.index]
大概对文中的实现思路则是这样的,通过获取入参数据来识别定位方式和定位元素入xpath=//button[contains(text(),‘确认支付’)],则定位方式为xpath,元素信息是//button[contains(text(),‘确认支付’)],之所以说对比我之前的思路相对更优,是在于使用到了这两个方法__get__、set。便于了获取对象后直接使用方法如这个用法
<!--元素对象-->
user_input = Element(id_="j_username")
<!--对象输入值-->
page.user_input="yuanbaojun"
开发Elements类
实际就是单个元素和一组元素定位的区别,不做概述了
其他类开发
这里面其他类的开发实际根据后面的实际项目开发需要来开发,比如当前能很容易想到的元素隐藏的查找类,元素等待类等等
三、基类文件BrowserPage.py开发
UI自动化框架的开发最开始的切入方向就是从web端自动化开始实现的,在web端自动化时浏览器相关的操作会被经常使用到的,所以基于web自动化的特性需要对浏览器相关的操作做一次封装,BrowserPage.py文件目前是主要的封装类文件了
开发Page类
在这里我把Page类继承自PageObject类,让page类拥有PageObject类的属性,page类的内容有fram的切换,浏览器标签页的切换,截图等功能。这里我罗列出了部分的代码供参考
class Page(PageObject):
"""
Implement the APIs with javascript,
and selenium/appium extension APIs。
"""
def window_scroll(self, width=None, height=None):
"""
JavaScript API, Only support css positioning
Setting width and height of window scroll bar.
"""
if width is None:
width = "0"
if height is None:
height = "0"
js = "window.scrollTo({w},{h});".format(w=str(width), h=height)
self.driver.execute_script(js)
@property
def title(self):
"""
JavaScript API
Get page title.
"""
js = 'return document.title;'
return self.driver.execute_script(js)
@property
def url(self):
"""
JavaScript API
Get page URL.
"""
js = "return document.URL;"
return self.driver.execute_script(js)
def switch_to_frame(self, frame_reference):
"""
selenium API
Switches focus to the specified frame, by id, name, or webelement.
"""
self.driver.switch_to.frame(frame_reference)
def switch_to_parent_frame(self):
"""
selenium API iframe
Switches focus to the parent context.
Corresponding relationship with switch_to_frame () method.
"""
self.driver.switch_to.parent_frame()
@property
def new_window_handle(self):
"""
selenium API
Getting a handle to a new window.
"""
all_handle = self.window_handles
return all_handle[-1]
@property
def current_window_handle(self):
"""
selenium API
Returns the handle of the current window.
"""
return self.driver.current_window_handle
@property
def window_handles(self):
"""
selenium API
Returns the handles of all windows within the current session.
"""
return self.driver.window_handles
def switch_to_window(self, handle, mode='web'):
"""
selenium API
Switches focus to the specified window.
"""
if mode == 'web':
app.logger.info('web切换窗口')
self.driver.switch_to.window(handle)
elif mode == 'app':
app.logger.info('app切换窗口')
try: # app一般很少出现多窗口,这里的处理是避免app多窗口切换时出现异常
self.driver.switch_to.window(handle)
self.switch_to_app()
self.switch_to_web()
self.driver.switch_to.window(handle)
except Exception as e:
app.logger.error(e)
if 'unable to connect to renderer' in str(e):
self.switch_to_app()
self.switch_to_web()
self.driver.switch_to.window(handle)
else:
raise Exception('mode只支持web或app')
def switch_to_new_window(self, handle='', mode='web'):
"""切换到新窗口(非当前窗口),窗口数量大于2个时必须指定handle"""
if len(self.window_handles) > 2 and not handle:
raise Exception('当前窗口数量大于2个,必须指定handle')
if len(self.window_handles) == 1:
self.switch_to_window(self.window_handles[0])
return
for window in self.window_handles:
if window != self.current_window_handle:
self.switch_to_window(handle=window, mode=mode)
return
def screenshots(self, path=None, filename=None):
"""
selenium API
Saves a screenshots of the current window to a PNG image file
:param path: The path to save the file
:param filename: The file name
"""
if path is None:
path = os.getcwd()
if filename is None:
filename = str(time.time()*100000).split(".")[0] + ".png"
file_path = os.path.join(path, filename)
self.driver.save_screenshot(file_path)
return filename
def switch_to_app(self):
"""
appium API
Switch to native app.
"""
self.driver.switch_to.context('NATIVE_APP')
app.logger.debug('已切换到native')
def switch_to_web(self, context=None):
"""
appium API
Switch to web view.
"""
if context is not None:
self.driver.switch_to.context(context)
else:
all_context = self.driver.contexts
for context in all_context:
if "WEBVIEW" in context and context != 'WEBVIEW_chrome':
self.driver.switch_to.context(context)
break
else:
raise NameError("No WebView found.")
app.logger.debug('已切换到webview')
def accept_alert(self):
"""
selenium API
Accept warning box.
"""
self.driver.switch_to.alert.accept()
def dismiss_alert(self):
"""
selenium API
Dismisses the alert available.
"""
self.driver.switch_to.alert.dismiss()
def alert_is_display(self):
"""
selenium API
Determines if alert is displayed
"""
try:
self.driver.switch_to.alert
except NoAlertPresentException:
return False
else:
return True
本代码中频繁出现的装饰器property(),property() 函数的作用是在新式类中返回属性值。
原创文章首发认准 软件测试微课堂
附件
BasePage.py
import platform
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import StaleElementReferenceException
from selenium.common.exceptions import WebDriverException
from appium.webdriver.common.mobileby import MobileBy
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located
from .exceptions import PageElementError, PageSelectException
from .exceptions import FindElementTypesError
from .BaseLoggers import logger
from func_timeout import func_set_timeout
from func_timeout.exceptions import FunctionTimedOut
from uiplatform.utils.data.config_object import Browser
LOCATOR_LIST = {
# selenium
'css': By.CSS_SELECTOR,
'id_': By.ID,
'name': By.NAME,
'xpath': By.XPATH,
'link_text': By.LINK_TEXT,
'partial_link_text': By.PARTIAL_LINK_TEXT,
'tag': By.TAG_NAME,
'class_name': By.CLASS_NAME,
# appium
'ios_uiautomation': MobileBy.IOS_UIAUTOMATION,
'ios_predicate': MobileBy.IOS_PREDICATE,
'ios_class_chain': MobileBy.IOS_CLASS_CHAIN,
'android_uiautomator': MobileBy.ANDROID_UIAUTOMATOR,
'android_viewtag': MobileBy.ANDROID_VIEWTAG,
'android_data_matcher': MobileBy.ANDROID_DATA_MATCHER,
'android_view_matcher': MobileBy.ANDROID_VIEW_MATCHER,
'windows_uiautomation': MobileBy.WINDOWS_UI_AUTOMATION,
'accessibility_id': MobileBy.ACCESSIBILITY_ID,
'image': MobileBy.IMAGE,
'custom': MobileBy.CUSTOM,
}
class PageObject(object):
"""
Page Object pattern.
"""
def __init__(self, driver, url=None):
"""
:param driver: `selenium.webdriver.WebDriver` Selenium webdriver instance
:param url: `str`
Root URI to base any calls to the ``PageObject.get`` method. If not defined
in the constructor it will try and look it from the webdriver object.
"""
self.driver = driver
self.root_uri = url if url else getattr(self.driver, 'url', None)
def get(self, uri):
"""
:param uri: URI to GET, based off of the root_uri attribute.
"""
root_uri = self.root_uri or ''
self.driver.get(root_uri + uri)
self.driver.implicitly_wait(4)
def add_cookies(self, cookies_dict):
self.driver.add_cookie(cookies_dict)
def get_all_cookies(self):
return self.driver.get_cookies()
def delete_all_cookie(self):
self.driver.delete_all_cookies()
def get_one_cookies(self, name):
return self.driver.get_cookie(name)
def delete_one_cookies(self, name):
self.driver.delete_cookie(name)
def get_page_source(self):
return self.driver.page_source
class Element(object):
"""
元素对象
"""
def __init__(self, timeout=5, describe="undefined", index=0, **kwargs):
self.timeout = timeout
self.index = index
self.desc = describe
if not kwargs:
raise ValueError("Please specify a locator")
if len(kwargs) > 1:
raise ValueError("Please specify only one locator")
self.kwargs = kwargs
self.k, self.v = next(iter(kwargs.items()))
if self.k not in LOCATOR_LIST.keys():
raise FindElementTypesError("Element positioning of type '{}' is not supported.".format(self.k))
def __get__(self, instance, owner):
if instance is None:
return None
Browser.driver = instance.driver
return self
def __set__(self, instance, value):
self.__get__(instance, instance.__class__)
self.send_keys(value)
@func_set_timeout(0.5)
def __elements(self, key, vlaue):
elems = Browser.driver.find_elements(by=key, value=vlaue)
return elems
def __find_element(self, elem):
"""
Find if the element exists.
"""
for i in range(self.timeout):
try:
elems = self.__elements(elem[0], elem[1])
except FunctionTimedOut:
elems = []
if len(elems) == 1:
# app.logger.info("✅ Find element: {by}={value} ".format(by=elem[0], value=elem[1]))
logger.info("✅ Find element: {by}={value} ".format(by=elem[0], value=elem[1]))
break
elif len(elems) > 1:
logger.info("❓ Find {n} elements through: {by}={value}".format(
n=len(elems), by=elem[0], value=elem[1]))
break
else:
sleep(1)
else:
error_msg = "❌ Find 0 elements through: {by}={value}".format(by=elem[0], value=elem[1])
logger.error(error_msg)
print(error_msg)
raise NoSuchElementException(error_msg)
def __get_element(self, by, value):
"""
根据传入的定位方式获取到页面对象供后续调用
"""
# selenium
if by == "id_":
self.__find_element((By.ID, value))
elem = Browser.driver.find_elements_by_id(value)[self.index]
elif by == "name":
self.__find_element((By.NAME, value))
elem = Browser.driver.find_elements_by_name(value)[self.index]
elif by == "class_name":
self.__find_element((By.CLASS_NAME, value))
elem = Browser.driver.find_elements_by_class_name(value)[self.index]
elif by == "tag":
self.__find_element((By.TAG_NAME, value))
elem = Browser.driver.find_elements_by_tag_name(value)[self.index]
elif by == "link_text":
self.__find_element((By.LINK_TEXT, value))
elem = Browser.driver.find_elements_by_link_text(value)[self.index]
elif by == "partial_link_text":
self.__find_element((By.PARTIAL_LINK_TEXT, value))
elem = Browser.driver.find_elements_by_partial_link_text(value)[self.index]
elif by == "xpath":
self.__find_element((By.XPATH, value))
elem = Browser.driver.find_elements_by_xpath(value)[self.index]
elif by == "css":
self.__find_element((By.CSS_SELECTOR, value))
elem = Browser.driver.find_elements_by_css_selector(value)[self.index]
def clear(self):
"""清空输入框中涉及到的文本,基本每次输入都需要调用此方法"""
elem = self.__get_element(self.k, self.v)
logger.info("clear element: {}".format(self.desc))
elem.clear()
def send_keys(self, value):
"""
输入信息value
"""
elem = self.__get_element(self.k, self.v)
logger.info("