zoukankan      html  css  js  c++  java
  • Page Object设计模式

    一,引入问题

    在之前的博客中,测试脚本是使用线性模式来编写的,如下:
    注意:本博客所有代码仅为示例

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    import logging
    from appium import webdriver
    from selenium.webdriver.support import expected_conditions as ec
    from selenium.webdriver.support.ui import WebDriverWait
    from appium.webdriver.common.mobileby import MobileBy as By
    
    logging.basicConfig(filename='./testLog.log', level=logging.INFO,
                        format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')
    
    def android_driver():
        desired_caps = {
            "platformName": "Android",
            "platformVersion": "10",
            "deviceName": "PCT_AL10",
            "appPackage": "com.ss.android.article.news",
            "appActivity": ".activity.MainActivity",
            "unicodeKeyboard": True,
            "resetKeyboard": True,
            "noReset": True,
        }
        logging.info("启动今日头条APP...")
        driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)
        return driver
    
    def is_toast_exist(driver, text, timeout=20, poll_frequency=0.1):
        '''
        判断toast是否存在,是则返回True,否则返回False
        '''
        try:
            toast_loc = (By.XPATH, ".//*[contains(@text, %s)]" % text)
            WebDriverWait(driver, timeout, poll_frequency).until(
                ec.presence_of_element_located(toast_loc)
            )
            return True
        except:
            return False
    
    def login_test(driver):
        '''登录今日头条操作'''
        logging.info("开始登陆今日头条APP...")
        try:
            driver.find_element_by_id("com.ss.android.article.news:id/bu").send_keys("xxxxxxxx")   # 输入账号
            driver.find_element_by_id("com.ss.android.article.news:id/c5").send_keys("xxxxxxxx")   # 输入密码
            driver.find_element_by_id("com.ss.android.article.news:id/a2o").click() # 点击登录
        except Exception as e:
            logging.error("登录错误,原因为:{}".format(e))
        # 断言是否登录成功
        toast_el = is_toast_exist(driver, "登录成功")
        assert toast_el, True
        logging.info("登陆成功...")
    
    if __name__ == '__main__':
        driver = android_driver()
        login_test(driver)
    

    但是,这种线性模式存在以下等缺点:

    • 元素定位属性和代码混杂在一起,不方便后续维护

    • 公共模块和业务模块混合在一起,显得代码冗余

    • 适用测试场景太单一

    在业务场景较为简单时这样写似乎没问题,但一旦遇到产品需求变更、业务逻辑比较复杂,需要维护的时就会非常麻烦。

    二,优化思路

    • 将公共方法(如:is_toast_exist(),日志记录器等)抽离出来,放入单独模块

    • 将元素定位方法、元素属性值、测试业务代码分离

    • 登录操作单独封装成一个模块

    • 使用Unittest单元测试框架管理并执行测试用例

    基于以上思路,我们就需要引入Page Object测试设计模式。

    三,Page Object 设计模式

    Page Object模式是Selenium中的一种测试设计模式,是Selenium、appium自动化测试项目的最佳设计模式之一。Page Object的通常的做法是,将公共方法、逻辑操作(元素定位、操作步骤)、测试用例、测试数据和测试驱动相互分离,可以理解为将测试项目进行如下分层:

    • 公共方法层

    • 逻辑操作层(元素定位,测试步骤)

    • 测试用例层(测试业务)

    • 测试数据层

    • 测试驱动层(执行测试用例)

    公共方法层,包括公共方法或基础方法。

    逻辑操作层,主要是将每一个页面或该页面需要测试的某个功能涉及到的元素设计为一个class。

    测试用例层,只需调用逻辑操作层中对应页面的class即可。

    测试数据层,即测试数据分离,包括配置数据和测试数据,如Capabilities、登录账号密码。

    测试驱动层,执行整个测试并生成测试报告。

    四,Page Object + Unittest 测试项目示例

    使用Page Object模式,Unittest管理测试用例。unittest框架请参考博客Unittest单元测试框架

    1,公共方法层

    封装App启动的Capabilities配置信息,baseDriver.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    import yaml
    from appium import webdriver
    from common.baseLog import logger
    
    def android_driver():
        stream = open("../config/desired_caps", "r")
        data = yaml.load(stream, Loader=yaml.FullLoader)
    
        desired_caps = {}
        desired_caps["platformName"] = data["Android"],
        desired_caps["platformVersion"] = data["platformVersion"],
        desired_caps["deviceName"] = data["deviceName"],
        desired_caps["appPackage"] = data["appPackage"],
        desired_caps["appActivity"] = data["appActivity"],
        desired_caps["unicodeKeyboard"] = data["unicodeKeyboard"],
        desired_caps["resetKeyboard"] = data["resetKeyboard"],
        desired_caps["noReset"] = data["noReset"],
        desired_caps["automationName"] = data["automationName"]
    
        # 启动app
        try:
            driver = webdriver.Remote('http://' + str(data['ip']) + ':' + str(data['port']) + '/wd/hub', desired_caps)
            logger.info("APP启动成功...")
            driver.implicitly_wait(8)
            return driver
        except Exception as e:
            logger.error("APP启动失败,原因是:{}".format(e))
    
    if __name__ == '__main__':
        android_driver()
    

    封装基础类,basePage.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    from common.baseLog import logger
    from selenium.webdriver.support.ui import WebDriverWait
    from appium.webdriver.common.mobileby import MobileBy as By
    from selenium.webdriver.support import expected_conditions as EC
    
    class BasePage:
        def __init__(self, driver):
            self.driver = driver
    
        def get_visible_element(self, locator, timeout=20):
            '''获取可视元素'''
            try:
                return WebDriverWait(self.driver, timeout).until(
                    EC.visibility_of_element_located(locator)
                )
            except Exception as e:
                logger.error("获取元素失败:{}".format(e))
    
        def is_toast_exist(driver, text, timeout=20, poll_frequency=0.1):
            '''
            判断toast是否存在,是则返回True,否则返回False
            '''
            try:
                toast_loc = (By.XPATH, ".//*[contains(@text, %s)]" % text)
                WebDriverWait(driver, timeout, poll_frequency).until(
                    EC.presence_of_element_located(toast_loc)
                )
                return True
            except:
                return False
    

    日志模块baseLog.py请参考博客Python日志采集

    2,逻辑操作层

    封装登录,login_page.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    from common.baseLog import logger
    from common.basePage import BasePage
    from appium.webdriver.common.mobileby import MobileBy as By
    
    class LoginPage(BasePage):
    
        username_inputBox = (By.ID, "com.ss.android.article.news:id/bu")    # 登录页用户名输入框
        password_inputBox = (By.ID, "com.ss.android.article.news:id/c5")    # 登录页密码输入框
        loginBtn = (By.ID, "com.ss.android.article.news:id/a2o")    # 登录页登录按钮
    
        def login_action(self, username, password):
            logger.info("开始登录...")
            logger.info("输入用户名:{}".format(username))
            self.get_visible_element(self.username_inputBox).send_keys(username)
            logger.info("输入密码:{}".format(password))
            self.get_visible_element(self.password_inputBox).send_keys(password)
            self.get_visible_element(self.loginBtn).click()
    

    3,测试用例层

    封装setUp、tearDown,baseTest.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    import time
    import unittest
    from common.baseDriver import android_driver
    
    class StartEnd(unittest.TestCase):
        def setUp(self) -> None:
            self.driver = android_driver()
    
        def tearDown(self) -> None:
            time.sleep(2)
            self.driver.close_app()
    

    封装测试用例,test_login.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    from common.baseLog import logger
    from common.baseTest import StartEnd
    from page.login_page import LoginPage
    
    class LoginTest(StartEnd):
    
        def test_login_right(self):
            logger.info("正确的账号、密码登录")
            l = LoginPage(self.driver)
            l.login_action("13838380000", "123456")
            result = l.is_toast_exist("登录成功")
            self.assertTrue(result)
    
        def test_login_error(self):
            logger.info("正确的账号、错误的密码登录")
            l = LoginPage(self.driver)
            l.login_action("13838380000", "111111")
            result = l.is_toast_exist("密码错误")
            self.assertTrue(result)
    

    4,测试数据层

    Capabilities配置数据,desired_caps.yml

    appActivity: .activity.MainActivity
    appPackage: com.ss.android.article.news
    deviceName: newDeviceName
    platformName: Android
    platformVersion: newPlatformVersion
    automationName: UiAutomator2
    unicodeKeyboard: true
    resetKeyboard: true
    noReset: true
    ip: 127.0.0.1
    port: 4723
    

    测试用例test_login.py中,正确的账号、正确密码、错误密码也可以配置在Yaml文件中,即数据分离,使用时读取即可。Yaml文件的使用可参考博客Python读写Yaml文件

    5,测试驱动层

    执行测试模块,run.py

    # -*- coding:utf-8 -*-
    # @author: 给你一页白纸
    
    import time
    import unittest
    import HTMLTestRunner
    
    now = time.strftime("%Y-%m-%d_%H_%M_%S")
    report_dir = './report/'
    fp = open(report_dir + now + "_report.html", 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(stream=fp,
                                           title="App自动化测试报告",
                                           description="测试用例情况")
    
    test_dir='./testcase'
    suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
    runner.run(suite)
    fp.close()
    

    6,示例目录结构

    运行run.py模块就能执行整个测试项目。

  • 相关阅读:
    关于C#登录三层
    SQL 语句关于分页的写法
    C# 如何去掉button按钮的边框线
    20151220
    继承
    对象的旅行
    多态
    封装
    OO大原则
    javascript
  • 原文地址:https://www.cnblogs.com/lfr0123/p/13810852.html
Copyright © 2011-2022 走看看