Skip to content

【实战】基于pageobject模式的测试框架优化实战

基于 Page Object 模式的测试框架优化实战

需求说明

  • 被测应用:雪球 App,请在应用商店直接安装。
  • 雪球 App 介绍

    • 雪球 app 是一款免费版,非常实用的股票平台,提供热点资讯内容和实时行情,提供了证券交流交易,公募、理财服务
    • 雪球 app 功能包括搜索股票,查看行情,交易,浏览热门文章,发帖,登录,注册,等功能
  • 测试场景:搜索股票

  • 完成基于 PO 模式封装的测试框架的搭建与优化。

实战思路

uml diagram

使用 PO 模式封装测试框架

PO 模式六大原则

  • 属性意义
  • 不要暴露页面内部的元素给外部
  • 不需要建模 UI 内的所有元素
  • 方法意义
  • 用公共方法代表 UI 所提供的功能
  • 方法应该返回其他的 PageObject 或者返回用于断言的数据
  • 同样的行为不同的结果可以建模为不同的方法
  • 不要在方法内加断言
构造页面相关类和方法
  • 基础层:对底层工具进行二次封装,例如元素查找方法。
  • 公共业务层:app 的启动配置。
  • 页面层:每个页面或功能模块作为一个类,类中包含该页面的元素和操作方法。
  • 测试用例层:使用 pytest 编写测试用例,调用业务逻辑层的方法进行测试。
  • 公共方法层:一些通用的工具函数,例如日志记录、数据读取等。

uml diagram

填充测试框架

app 启动
# base/xueqiu_app.py

# 雪球 app 相关的操作
class XueqiuApp(BasePage):

    def start(self):
        '''
        启动 app
        :return:
        '''
        # 设置 cpability
        caps = {
            # 设置 app 安装的平台(Android,iOS)
            "platformName": "Android",
            # 设置 appium 驱动
            "appium:automationName": "uiautomator2",
            # 设置设备名称
            "appium:deviceName": "emulator-5554",
            # 设置被测 app 的包名
            "appium:appPackage": "com.xueqiu.android",
            # 设置被测 app 启动页面的 Activity
            "appium:appActivity": ".view.WelcomeActivityAlias",
            # 不清空缓存信息,保存登录信息
            "appium:noReset": True,
            # 强制app重启,整个测试运行之前重启 app
            "appium:forceAppLaunch": True,
            # 跳过安装,权限设置等操作
            "appium:skipDeviceInitialization": True
        }

        # 初始化 driver
        self.driver = webdriver.Remote(
            "http://127.0.0.1:4723",
            options=UiAutomator2Options().load_capabilities(caps)
        )
        self.driver.implicitly_wait(15)
        return self

    def stop(self):
        '''
        停止 app
        :return:
        '''
        self.driver.quit()

    def goto_main(self):
        '''
        进入 app 首页
        :return:
        '''
        from auto_test_app.xueqiu_app_po.page.main_page import MainPage
        return MainPage(self.driver)
BasePage 封装
# base/base_page.py

class BasePage:

    def __init__(self, driver: WebDriver=None):
        self.driver = driver

    def find_ele(self, by, value):
        '''
        查找元素,返回元素
        :param by: 定位方式
        :param value: 元素定位表达式
        :return: 定位到的元素对象
        '''
        step_text = f"查找单个元素的定位:{by},{value}"
        logger.info(step_text)
        ele = self.driver.find_element(by, value)
        return ele

    def find_eles(self, by, value):
        '''
        查找多个元素
        :param by: 定位方式
        :param value: 元素定位表达式
        :return: 定位到的元素列表
        '''
        logger.info(f"查找多个元素的定位:{by},{value}")
        eles = self.driver.find_elements(by, value)
        return eles
业务页面封装

首页封装

# pages/main_page.py

class MainPage(XueqiuApp):

    # 搜索框
    _SEARCH_BAR = AppiumBy.XPATH, "//*[@resource-id='com.xueqiu.android:id/tv_banner']"

    def goto_search_page(self):
        # 点击首页搜索框
        self.find_ele(*self._SEARCH_BAR).click()
        # 跳转到搜索页面
        return SearchPage(self.driver)

搜索页面封装

# pages/search_page.py

class SearchPage(XueqiuApp):

    # 搜索框
    _SEARCH_BAR = AppiumBy.ID,"com.xueqiu.android:id/search_input_text"
    # 第一个搜索结果
    _FIRST_SEARCH_RESULT = AppiumBy.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']"

    def goto_search_result_page(self, search_text):
        # 搜索页面搜索框中输入搜索关键词
        self.find_ele(*self._SEARCH_BAR).send_keys(search_text)
        # 点击第一个搜索结果
        self.find_eles(*self._FIRST_SEARCH_RESULT)[0].click()
        # 跳转到搜索结果页面
        return SearchResultPage(self.driver)

搜索结果页面封装

# pages/search_result_page.py

class SearchResultPage(XueqiuApp):

    def get_search_result(self, search_text):
        # 获取页面中是否包含搜索关键词相关结果
        results = self.find_eles(
            AppiumBy.XPATH,
            f"//*[@text='{search_text}']"
        )
        results_text = []
        if results:
            # 获取每个元素的文本放入列表中
            results_text = [r.text for r in results]
        return results_text
编写测试用例
# testcases/test_search.py

class TestSearch:

    def setup_method(self):
        # 启动 app
        self.app = XueqiuApp()
        # 进入首页
        self.main = self.app.start().goto_main()

    def teardown_method(self):
        # 关闭 driver
        self.app.stop()

    def test_search_stock(self):
        '''
        1 首页点击搜索框
        2 搜索页点击搜索框,输入搜索关键词
        3 点击第一个搜索结果
        4 断言可以找到搜索关键词相关结果
        :return:
        '''
        search_text = "阿里巴巴"
        result = self.main.goto_search_page().\
            goto_search_result_page(search_text).\
            get_search_result(search_text)
        assert search_text in result

优化测试框架

数据驱动

准备测试数据。

# datas/stock_name.yaml

- 阿里巴巴
- 平安银行

定义读取 yaml 文件的工具方法。

class Utils:

    @classmethod
    def get_yaml_data(cls, yaml_path):
        '''
        读取 yaml 文件的数据
        :param yaml_path: yaml 文件的路径
        :return:
        '''
        with open(yaml_path, "r", encoding="utf-8") as f:
            datas = yaml.safe_load(f)
        logger.info(f"读取的 yaml 文件的数据为 {datas}")
        return datas

测试用例中读取 yaml 文件,完成数据驱动。

class TestSearch:

    def setup_method(self):
        # 获取雪球 app 的实例
        self.app = XueqiuApp()
        # 启动 app,进入首页
        self.main = self.app.start().goto_main()

    def teardown_method(self):
        # 关闭 driver
        self.app.stop()

    @pytest.mark.parametrize(
        "search_text", 
        Utils.get_yaml_data(Utils.get_file_path("./datas/stock_name.yaml"))
    )
    def test_search_stock(self, search_text):
        '''
        1. 首页点击搜索框
        2. 搜索页点击搜索框,输入搜索关键词
        3. 点击第一个搜索结果
        4. 断言可以找到搜索关键词相关结果
        :return:
        '''
        result = self.main.goto_search_page().\
            input_search_key(search_text).\
            get_search_result(search_text)
        assert search_text in result
黑名单处理
# 定义弹窗黑名单
black_list = [
    (AppiumBy.XPATH, "//*[@text=['取消']"),
    (AppiumBy.XPATH, "//*[@text=['关闭']")
]


# 黑名单处理装饰器
# 传入 fun 相当于要去装饰的方式 find_ele(self, by, value)
def black_wrapper(fun):

    def run(*args, **kwargs):
        # 相当于拿到了传入参数的第一个值:self(BasePage 类的实例)
        basepage = args[0]
        basepage.driver.implicitly_wait(1)
        try:
            logger.info(f"开始查找元素 {args[1]}, {args[2]}")
            basepage.driver.implicitly_wait(15)
            return fun(*args, **kwargs)
        except Exception as e:
            logger.info(f"没有找到元素,处理异常 {e}")
            # 截图
            image_path = basepage.screenshot()
            # 添加到 allure 报告
            allure.attach.file(
                image_path,
                name="查找元素异常截图",
                attachment_type=allure.attachment_type.PNG
            )
            # 保存页面源码
            pagesource_path = basepage.save_pagesource()
            allure.attach.file(
                pagesource_path,
                name="查找元素异常页面源码",
                attachment_type=allure.attachment_type.TEXT
            )
            # 处理黑名单
            for b in black_list:
                # 查找黑名单列表中的每一个元素
                eles = basepage.driver.find_elements(*b)
                if len(eles) > 0:
                    # 找到了黑名单中的元素
                    basepage.driver.find_elements(*b)[0].click()
                    basepage.driver.implicitly_wait(15)
                    return fun(*args, **kwargs)
            logger.info(f"遍历黑名单,仍未找到元素")
            basepage.driver.implicitly_wait(15)
            raise e
    return run

在基类中使用定义好的黑名单装饰器装饰查找元素的方法。

# base/base_page.py

class BasePage:

    def __init__(self, driver: WebDriver=None):
        self.driver = driver

    @black_wrapper
    def find_ele(self, by, value):
        '''
        查找元素,返回元素
        :param by: 定位方式
        :param value: 定位表达式
        :return: 找到的元素
        '''
        info = f"查找元素,定位方式为 {by}, 定位表达式为 {value}"
        logger.info(info)
        with allure.step(info):
            ele = self.driver.find_element(by, value)
        return ele

    @black_wrapper
    def find_eles(self, by, value):
        '''
        查找多个元素
        :param by: 定位方式
        :param value: 定位表达式
        :return: 找到的元素列表
        '''
        info = f"查找多个元素,定位方式为 {by}, 定位表达式为 {value}"
        logger.info(info)
        with allure.step(info):
            eles = self.driver.find_elements(by, value)
        return eles

测试报告

添加描述信息
@allure.feature("雪球搜索")
class TestSearch:

    def setup_method(self):
        # 获取雪球 app 的实例
        self.app = XueqiuApp()
        # 启动 app,进入首页
        self.main = self.app.start().goto_main()

    def teardown_method(self):
        # 关闭 driver
        self.app.stop()

    @allure.story("雪球搜索股票")
    @allure.title("参数化搜索股票名称 {search_text}")
    @pytest.mark.parametrize(
        "search_text", 
        Utils.get_yaml_data(Utils.get_file_path("./datas/stock_name.yaml"))
    )
    def test_search_stock(self, search_text):
        '''
        1. 首页点击搜索框
        2. 搜索页点击搜索框,输入搜索关键词
        3. 点击第一个搜索结果
        4. 断言可以找到搜索关键词相关结果
        :return:
        '''
        result = self.main.goto_search_page().\
            input_search_key(search_text).\
            get_search_result(search_text)
        assert search_text in result
生成测试报告
# 运行测试用例,搜集执行结果
pytest --alluredir=./results --clean-alluredir
# 启动本地服务查看 allure 报告
allure serve ./results
# 生成静态报告
allure generate --clean report/html report -o report/html

总结

  • 使用 PageObject 模式封装 app 自动化测试框架
  • 优化测试框架
  • 测试数据的数据驱动
  • 异常处理(弹窗黑名单),异常截图
  • 日志记录
  • 报告生成