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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351