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模块就能执行整个测试项目。

posted @ 2020-10-13 20:08  给你一页白纸  阅读(1309)  评论(0编辑  收藏  举报