• 基于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

    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)
    LogUtil

     五、其他补充

    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+"
    报错信息如下"+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    
  • 相关阅读:
    知识积累
    来西安对吗
    python循环语句---------while、for
    postman接口功能测试
    Laravel环境搭建
    Vue
    搭建Vue.js环境
    Atom
    PHP
    thinkphp-无限分类下根据任意部门获取顶级部门ID
  • 原文地址:https://www.cnblogs.com/Chilam007/p/14732679.html
Copyright © 2020-2023  润新知