Selenium_POM架构(17)
阅读原文时间:2023年07月09日阅读:3

POM是Page Object Model的简称,它是一种设计思想,意思是,把每一个页面,当做一个对象,页面的元素和元素之间操作方法就是页面对象的属性和行为。

POM一般使用三层架构,分别为:基础封装层、页面对象层、测试用例层。

目录结构大致如下

下面简单介绍下我的POM架构实现方式。

基础封装层

基础封装层主要是封装一些常用的方法,提高代码的复用。

基础封装层当前只包含了3个文件:

  • base_page.py:将所有界面共用的方法进行封装
  • browser.py:继承了selenium常用的webdriver操作,并对部分操作进行了封装
  • log,py:封装日志功能

base_page.py文件代码如下:

class BasePage(object):

def \_\_init\_\_(self, driver):  
    self.\_\_driver = driver

def find\_element(self, by, value, times=10, wait\_time=1) -> object:  
    return self.\_\_driver.until\_find\_element(by, value, times=10, wait\_time=1)

因为在页面对象层,我们会将每个界面定义成一个类对象,而每个类对象都需要传入一个webdriver的实例对象,为了减少这样的重复操作,我们在base_page.py定义一个页面基类BasePage,在页面对象层定义的类继承该基类就可以完成webdriver实例对象的传入。

browser.py文件代码如下:

import time
import logging

from selenium.webdriver import Chrome, Firefox
from selenium.common.exceptions import NoSuchElementException

class Browser(Chrome, Firefox):

def \_\_init\_\_(self, browser\_type="chrome", driver\_path=None, \*args, \*\*kwargs):  
    """  
    根据浏览器类型初始化浏览器  
    :param browser\_type: 浏览器类型,只可传入chrome或firefox  
    :param driver\_path:指定驱动存放的路径  
    """  
    # 检查browser\_type值是否合法  
    if browser\_type not in \["chrome", "firefox"\]:  
        # 不合法报错  
        logging.error("browser\_type 输入值不为chrome,firefox")  
        raise ValueError("browser\_type 输入值不为chrome,firefox")

    self.\_\_browser\_type = browser\_type

    # 根据browser\_type值选择对应的驱动  
    if self.\_\_browser\_type == "chrome":  
        if driver\_path:  
            Chrome.\_\_init\_\_(self, executable\_path=f"{driver\_path}/chromedriver.exe", \*args, \*\*kwargs)  
        else:  
            Chrome.\_\_init\_\_(self, \*args, \*\*kwargs)  
    elif self.\_\_browser\_type == "firefox":  
        if driver\_path:  
            Firefox.\_\_init\_\_(self, executable\_path=f"{driver\_path}/geckodriver.exe", \*args, \*\*kwargs)  
        else:  
            Firefox.\_\_init\_\_(self, \*args, \*\*kwargs)

def open\_browser(self, url):  
    self.get(url)  
    self.maximize\_window()

@property  
def browser\_name(self):  
    return self.capabilities\["browserName"\]

@property  
def browser\_version(self):  
    return self.capabilities\["browserVersion"\]

def until\_find\_element(self, by, value, times=10, wait\_time=1):  
    """  
    用于定位元素  
    :param by: 定位元素的方式  
    :param value: 定位元素的值  
    :param times: 定位元素的重试次数  
    :param wait\_time: 定位元素失败的等待时间  
    :return: 返回定位的元素  
    """  
    # 检查by的合法性  
    if by not in \["id", "xpath", "name", "class", "tag", "text", "partial\_text", "css"\]:  
        # 不合法报错  
        logging.error(f"无效定位方式:{by},请输入:id,xpath, name, class, tag, text, partial\_text, css")  
        raise ValueError(f"无效定位方式:{by},请输入:id,xpath, name, class, tag, text, partial\_text, css")

    # 定位元素,如果定位失败,增加重试机制  
    for i in range(times):  
        # 定位元素  
        el = None  
        try:  
            if by == "id":  
                el = super().find\_element\_by\_id(value)  
            elif by == "xpath":  
                el = super().find\_element\_by\_xpath(value)  
            elif by == "name":  
                el = super().find\_element\_by\_name(value)  
            elif by == "class":  
                el = super().find\_element\_by\_class\_name(value)  
            elif by == "tag":  
                el = super().find\_elements\_by\_tag\_name(value)  
            elif by == "text":  
                el = super().find\_element\_by\_link\_text(value)  
            elif by == "partial\_text":  
                el = super().find\_element\_by\_partial\_link\_text(value)  
            elif by == "css":  
                el = super().find\_element\_by\_css\_selector(value)  
        except NoSuchElementException:  
            # 如果报错为未找到元素,则重试  
            logging.error(f"通过{by}未定位到元素【{value}】,正在进行第{i+1}次重试...")  
            time.sleep(wait\_time)  
        else:  
            # 如果成功定位元素则返回元素  
            logging.info(""f"通过{by}成功定位元素【{value}】!")  
            return el

    # 如果循环完仍为定位到元素,则抛错  
    logging.error(f"通过{by}无法定位元素【{value}】,请检查...")  
    raise NoSuchElementException(f"通过{by}无法定位元素【{value}】,请检查...")

def switch\_to\_new\_page(self):  
    # 获取老窗口的handle  
    old\_handle = self.current\_window\_handle

    handles = self.window\_handles  
    for handle in handles:  
        if handle != old\_handle:  
            self.switch\_to.window(handle)  
            break

在browser.py文件中,我们主要定义一个 Browser类,该类继承了selenium的Chrome 和 Firefox,在实例化Browser类后,我们能使用selenium所有的方法,同时,我们在Browser类中还封装一些其它操作,比如将查找元素的8种方法进行封装并增加元素定位失败后重试次数,比如切换新界面的handle等

log.py文件代码如下:

import os
import logging
import time

from logging.handlers import RotatingFileHandler

def log(log_level="DEBUG"):
# 创建logger,如果参数为空则返回root logger
logger = logging.getLogger()

# 设置logger日志等级  
# logger.setLevel(logging.DEBUG)  
logger.setLevel(log\_level)

# 创建handler  
log\_size = 1024 \* 1024 \* 20  
# 将日志写入到文件中  
dir\_name = "./logs/"  
if not os.path.exists(dir\_name):  
    os.mkdir(dir\_name)  
time\_str = time.strftime("%Y%m%d", time.localtime())  
fh = RotatingFileHandler(dir\_name + f"{time\_str}.log", encoding="utf-8", maxBytes=log\_size, backupCount=100)  
# 将日志输出到控制台  
ch = logging.StreamHandler()

# 设置输出日志格式  
formatter = logging.Formatter(  
    fmt="%(asctime)s \[%(levelname)s\] %(filename)s line:%(lineno)s  %(message)s",  
    # datefmt="%Y/%m/%d %X"  
)  
# 注意 logging.Formatter的大小写

# 为handler指定输出格式,注意大小写  
fh.setFormatter(formatter)  
ch.setFormatter(formatter)

# 为logger添加的日志处理器  
logger.addHandler(fh)  
logger.addHandler(ch)

log.py文件主要定义了一个log函数,函数中定义了日志相关的操作,注意,定义的log函数需要在任意被执行文件中被调用,比如,在用例层我们调用了browser.py文件中的方法,那么在架构中必定执行utils文件中__init.py文件,所以我们在__init__.py文件中调用log函数。

__init__.py文件代码如下:

from .log import log

log("INFO")

到此,我们完成了日志的环境配置,当需要记录日志时,只需要在文件中导入logging包,使用logging.info()这种方式记录日志即可。点我查看更多日志操作

页面对象层

什么是页面对象?页面对象就是将每个界面当成一个对象,界面中的元素当成对象的属性。下面以百度首页和新闻页为例,介绍页面对象层。

在页面对象层,新增文件baidu.py,文件代码如下:

from utils.base_page import BasePage

class HomePage(BasePage):

@property  
def input\_box(self):  
    return self.find\_element("id", "kw")

@property  
def search\_button(self):  
    return self.find\_element("id", "su")

@property  
def news\_link(self):  
    return self.find\_element("xpath", '//\*\[@id="s-top-left"\]/a\[1\]')

class NewsPage(BasePage):
@property
def game_link(self):
return self.find_element("xpath", '//*[@id="channel-all"]/div/ul/li[10]/a')

类对象HomePage和NewsPage分别代表百度首页和百度新闻页,在类对象中定义了一些方法,每个方法表示页面中的一个元素,再使用装饰器@property将这些方法属性化。比如,input_box表示输入框,search_button表示搜索框。

测试用例层

在测试用例层,我们使用了uniittest框架来管理和执行用例,下面以两个简单的用例,来演示脚本的编写。

test_baidu.py文件代码如下:

import unittest
import time
import logging

from utils.browser import Browser
from page_object.baidu import HomePage, NewsPage

class Baidu(unittest.TestCase):

def setUp(self) -> None:  
    self.driver = Browser("firefox")  
    self.driver.open\_browser("http://www.baidu.com")  
    logging.info("打开浏览器")  
    logging.info(f"浏览器名称:{self.driver.browser\_name},浏览器版本:{self.driver.browser\_version}")

    self.homepage = HomePage(self.driver)  
    self.newspage = NewsPage(self.driver)

def tearDown(self) -> None:  
    self.driver.quit()  
    logging.info("关闭浏览器")

def test\_search(self):  
    """ 用例1:测试百度搜索框输入selenium能搜索出包含selenium相关的信息 """  
    logging.info("用例1:测试百度搜索框输入selenium能搜索出包含selenium相关的信息")

    # 输入搜索信息  
    self.homepage.input\_box.send\_keys("selenium")  
    logging.info("输入搜索信息")

    # 点击按钮  
    self.homepage.search\_button.click()  
    logging.info("点击搜索按钮")  
    time.sleep(2)

    # 校验搜索结果  
    els = self.driver.find\_element\_by\_partial\_link\_text("selenium")  
    self.assertIsNotNone(els)

def test\_access\_game\_news(self):  
    """ 用例2:测试通过百度首页能进入新闻界面的游戏专题 """  
    logging.info("用例2:测试通过百度首页能进入新闻界面的游戏专题")

    # 点击新闻链接  
    self.homepage.news\_link.click()  
    logging.info("点击新闻链接")

    # 切换窗口  
    self.driver.switch\_to\_new\_page()  
    logging.info("切换窗口")

    # 点击游戏链接  
    self.newspage.game\_link.click()  
    logging.info("点击游戏链接")

    # 校验url  
    current\_url = self.driver.current\_url  
    self.assertEqual(current\_url, "http://news.baidu.com/game")

if __name__ == '__main__':
unittest.main()

执行用例

到此,POM架构基本实现。