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,示例目录结构

微信图片_20220424204113.png

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

相关文章
|
7月前
|
设计模式 Java 测试技术
软件测试/测试开发/全日制|Page Object模式:为什么它是Web自动化测试的必备工具
软件测试/测试开发/全日制|Page Object模式:为什么它是Web自动化测试的必备工具
|
7月前
|
设计模式 Java API
【设计模式】JAVA Design Patterns——Active Object(活动对象设计模式)
【设计模式】JAVA Design Patterns——Active Object(活动对象设计模式)
|
设计模式 算法 Java
Object 类详解--代码块--单例设计模式
Object 类详解--代码块--单例设计模式
59 0
|
设计模式 测试技术
软件测试面试题:什么是page object设计模式?
软件测试面试题:什么是page object设计模式?
100 0
|
测试技术
软件测试面试题:page object设置模式中,是否需要在page里定位的方法中加上断言?
软件测试面试题:page object设置模式中,是否需要在page里定位的方法中加上断言?
121 0
|
设计模式 测试技术
软件测试面试题:page object设计模式中,如何实现页面的跳转?
软件测试面试题:page object设计模式中,如何实现页面的跳转?
125 0
|
23天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
2月前
|
Java
Java Object 类详解
在 Java 中,`Object` 类是所有类的根类,每个 Java 类都直接或间接继承自 `Object`。作为所有类的超类,`Object` 定义了若干基本方法,如 `equals`、`hashCode`、`toString` 等,这些方法在所有对象中均可使用。通过重写这些方法,可以实现基于内容的比较、生成有意义的字符串表示以及确保哈希码的一致性。此外,`Object` 还提供了 `clone`、`getClass`、`notify`、`notifyAll` 和 `wait` 等方法,支持对象克隆、反射机制及线程同步。理解和重写这些方法有助于提升 Java 代码的可读性和可维护性。
|
4月前
|
Java
【Java基础面试二十】、介绍一下Object类中的方法
这篇文章介绍了Java中Object类的常用方法,包括`getClass()`、`equals()`、`hashCode()`、`toString()`、`wait()`、`notify()`、`notifyAll()`和`clone()`,并提到了不推荐使用的`finalize()`方法。
【Java基础面试二十】、介绍一下Object类中的方法
|
3月前
|
Python
类与面向对象编程(Object-Oriented Programming, OOP)
类与面向对象编程(Object-Oriented Programming, OOP)
23 0