前言

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

POM一般使用三层架构,分别为:基础封装层、页面对象层、测试用例层。
目录结构大致如下

基础封装层

基础封装层主要是封装一些常用的方法,提高代码的复用。
基础封装层当前只包含了3个文件:
base_page.py:将所有界面共用的方法进行封装
browser.py:继承了selenium常用的webdriver操作,并对部分操作进行了封装
log.py:封装日志功能


  1. class BasePage(object):

  2. def __init__(self, driver):

  3. self.__driver = driver

  4. def find_element(self, by, value, times=10, wait_time=1) -> object:

  5. return self.__driver.until_find_element(by, value, times=10, wait_time=1)

因为在页面对象层,我们会将每个界面定义成一个类对象,而每个类对象都需要传入一个webdriver的实例对象。

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

browser.py文件代码如下:

  1. import time

  2. import logging

  3. from selenium.webdriver import Chrome, Firefox

  4. from selenium.common.exceptions import NoSuchElementException

  5. class Browser(Chrome, Firefox):

  6. def __init__(self, browser_type="chrome", driver_path=None, *args, **kwargs):

  7. """

  8. 根据浏览器类型初始化浏览器

  9. :param browser_type: 浏览器类型,只可传入chrome或firefox

  10. :param driver_path:指定驱动存放的路径

  11. """

  12. # 检查browser_type值是否合法

  13. if browser_type not in ["chrome", "firefox"]:

  14. # 不合法报错

  15. logging.error("browser_type 输入值不为chrome,firefox")

  16. raise ValueError("browser_type 输入值不为chrome,firefox")

  17. self.__browser_type = browser_type

  18. # 根据browser_type值选择对应的驱动

  19. if self.__browser_type == "chrome":

  20. if driver_path:

  21. Chrome.__init__(self, executable_path=f"{driver_path}/chromedriver.exe", *args, **kwargs)

  22. else:

  23. Chrome.__init__(self, *args, **kwargs)

  24. elif self.__browser_type == "firefox":

  25. if driver_path:

  26. Firefox.__init__(self, executable_path=f"{driver_path}/geckodriver.exe", *args, **kwargs)

  27. else:

  28. Firefox.__init__(self, *args, **kwargs)

  29. def open_browser(self, url):

  30. self.get(url)

  31. self.maximize_window()

  32. @property

  33. def browser_name(self):

  34. return self.capabilities["browserName"]

  35. @property

  36. def browser_version(self):

  37. return self.capabilities["browserVersion"]

  38. def until_find_element(self, by, value, times=10, wait_time=1):

  39. """

  40. 用于定位元素

  41. :param by: 定位元素的方式

  42. :param value: 定位元素的值

  43. :param times: 定位元素的重试次数

  44. :param wait_time: 定位元素失败的等待时间

  45. :return: 返回定位的元素

  46. """

  47. # 检查by的合法性

  48. if by not in ["id", "xpath", "name", "class", "tag", "text", "partial_text", "css"]:

  49. # 不合法报错

  50. logging.error(f"无效定位方式:{by},请输入:id,xpath, name, class, tag, text, partial_text, css")

  51. raise ValueError(f"无效定位方式:{by},请输入:id,xpath, name, class, tag, text, partial_text, css")

  52. # 定位元素,如果定位失败,增加重试机制

  53. for i in range(times):

  54. # 定位元素

  55. el = None

  56. try:

  57. if by == "id":

  58. el = super().find_element_by_id(value)

  59. elif by == "xpath":

  60. el = super().find_element_by_xpath(value)

  61. elif by == "name":

  62. el = super().find_element_by_name(value)

  63. elif by == "class":

  64. el = super().find_element_by_class_name(value)

  65. elif by == "tag":

  66. el = super().find_elements_by_tag_name(value)

  67. elif by == "text":

  68. el = super().find_element_by_link_text(value)

  69. elif by == "partial_text":

  70. el = super().find_element_by_partial_link_text(value)

  71. elif by == "css":

  72. el = super().find_element_by_css_selector(value)

  73. except NoSuchElementException:

  74. # 如果报错为未找到元素,则重试

  75. logging.error(f"通过{by}未定位到元素【{value}】,正在进行第{i+1}次重试...")

  76. time.sleep(wait_time)

  77. else:

  78. # 如果成功定位元素则返回元素

  79. logging.info(""f"通过{by}成功定位元素【{value}】!")

  80. return el

  81. # 如果循环完仍为定位到元素,则抛错

  82. logging.error(f"通过{by}无法定位元素【{value}】,请检查...")

  83. raise NoSuchElementException(f"通过{by}无法定位元素【{value}】,请检查...")

  84. def switch_to_new_page(self):

  85. # 获取老窗口的handle

  86. old_handle = self.current_window_handle

  87. handles = self.window_handles

  88. for handle in handles:

  89. if handle != old_handle:

  90. self.switch_to.window(handle)

  91. break

在browser.py文件中,我们主要定义一个 Browser类,该类继承了selenium的Chrome 和 Firefox,在实例化Browser类后,我们能使用selenium所有的方法,同时,我们在Browser类中还封装一些其它操作。

比如将查找元素的8种方法进行封装并增加元素定位失败后重试次数,比如切换新界面的handle等

log.py文件代码如下:

  1. import os

  2. import logging

  3. import time

  4. from logging.handlers import RotatingFileHandler

  5. def log(log_level="DEBUG"):

  6. # 创建logger,如果参数为空则返回root logger

  7. logger = logging.getLogger()

  8. # 设置logger日志等级

  9. # logger.setLevel(logging.DEBUG)

  10. logger.setLevel(log_level)

  11. # 创建handler

  12. log_size = 1024 * 1024 * 20

  13. # 将日志写入到文件中

  14. dir_name = "./logs/"

  15. if not os.path.exists(dir_name):

  16. os.mkdir(dir_name)

  17. time_str = time.strftime("%Y%m%d", time.localtime())

  18. fh = RotatingFileHandler(dir_name + f"{time_str}.log", encoding="utf-8", maxBytes=log_size, backupCount=100)

  19. # 将日志输出到控制台

  20. ch = logging.StreamHandler()

  21. # 设置输出日志格式

  22. formatter = logging.Formatter(

  23. fmt="%(asctime)s [%(levelname)s] %(filename)s line:%(lineno)s %(message)s",

  24. # datefmt="%Y/%m/%d %X"

  25. )

  26. # 注意 logging.Formatter的大小写

  27. # 为handler指定输出格式,注意大小写

  28. fh.setFormatter(formatter)

  29. ch.setFormatter(formatter)

  30. # 为logger添加的日志处理器

  31. logger.addHandler(fh)

  32. logger.addHandler(ch)

log.py文件主要定义了一个log函数,函数中定义了日志相关的操作,注意,定义的log函数需要在任意被执行文件中被调用。

比如,在用例层我们调用了browser.py文件中的方法,那么在架构中必定执行utils文件中__init.py文件,所以我们在__init__.py文件中调用log函数。

init.py文件代码如下:

  1. from .log import log

  2. log("INFO")

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

页面对象层

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

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

  1. from utils.base_page import BasePage

  2. class HomePage(BasePage):

  3. @property

  4. def input_box(self):

  5. return self.find_element("id", "kw")

  6. @property

  7. def search_button(self):

  8. return self.find_element("id", "su")

  9. @property

  10. def news_link(self):

  11. return self.find_element("xpath", '//*[@id="s-top-left"]/a[1]')

  12. class NewsPage(BasePage):

  13. @property

  14. def game_link(self):

  15. return self.find_element("xpath", '//*[@id="channel-all"]/div/ul/li[10]/a')

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

测试用例层

下面以两个简单的用例,来演示脚本的编写。

test_baidu.py文件代码如下:

  1. import unittest

  2. import time

  3. import logging

  4. from utils.browser import Browser

  5. from page_object.baidu import HomePage, NewsPage

  6. class Baidu(unittest.TestCase):

  7. def setUp(self) -> None:

  8. self.driver = Browser("firefox")

  9. self.driver.open_browser("http://www.baidu.com")

  10. logging.info("打开浏览器")

  11. logging.info(f"浏览器名称:{self.driver.browser_name},浏览器版本:{self.driver.browser_version}")

  12. self.homepage = HomePage(self.driver)

  13. self.newspage = NewsPage(self.driver)

  14. def tearDown(self) -> None:

  15. self.driver.quit()

  16. logging.info("关闭浏览器")

  17. def test_search(self):

  18. """ 用例1:测试百度搜索框输入selenium能搜索出包含selenium相关的信息 """

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

  20. # 输入搜索信息

  21. self.homepage.input_box.send_keys("selenium")

  22. logging.info("输入搜索信息")

  23. # 点击按钮

  24. self.homepage.search_button.click()

  25. logging.info("点击搜索按钮")

  26. time.sleep(2)

  27. # 校验搜索结果

  28. els = self.driver.find_element_by_partial_link_text("selenium")

  29. self.assertIsNotNone(els)

  30. def test_access_game_news(self):

  31. """ 用例2:测试通过百度首页能进入新闻界面的游戏专题 """

  32. logging.info("用例2:测试通过百度首页能进入新闻界面的游戏专题")

  33. # 点击新闻链接

  34. self.homepage.news_link.click()

  35. logging.info("点击新闻链接")

  36. # 切换窗口

  37. self.driver.switch_to_new_page()

  38. logging.info("切换窗口")

  39. # 点击游戏链接

  40. self.newspage.game_link.click()

  41. logging.info("点击游戏链接")

  42. # 校验url

  43. current_url = self.driver.current_url

  44. self.assertEqual(current_url, "http://news.baidu.com/game")

  45. if __name__ == '__main__':

  46. unittest.main()

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

 

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐