zoukankan      html  css  js  c++  java
  • 基于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    
  • 相关阅读:
    第一节:SpringMVC概述
    SpringMVC【目录】
    Windows 系统快速查看文件MD5
    (error) ERR wrong number of arguments for 'hmset' command
    hive使用遇到的问题 cannot recognize input
    Overleaf支持的部分中文字体预览
    Understanding and Improving Fast Adversarial Training
    Django2实战示例 第十三章 上线
    Django2实战示例 第十二章 创建API
    Django2实战示例 第十一章 渲染和缓存课程内容
  • 原文地址:https://www.cnblogs.com/Chilam007/p/14732679.html
Copyright © 2011-2022 走看看