基于PageObject模式设计的web自动化测试示例

PageObject模式

PageObject模式:顾名思义,就是页面对象。它的核心思想是分层设计, 强调测试、逻辑、数据和驱动相互分离。一般分层会分为:
1.对象库层
2.逻辑层
3.业务层
4.数据层
但是,具体分层,还是要根据系统去设计。

目录结构

下面,是基于PageObject模式,设计课堂派的登录测试。先说一下目录结构:
基于PageObject模式设计的web自动化测试示例

  • Common:存放公共封装类,公共配置文件。
  • Outputs:存放输出,日志、截图、测试报告等。
  • PageLocators:存放各个界面的定位。
  • PageObjects:存放各个界面的方法。
  • TestCases:存放测试用例。
  • TestDatas:存放测试数据。
  • main.py:用例执行入口文件。
    下面根据目录顺序,说明具体内容:

Common目录:

基于PageObject模式设计的web自动化测试示例

  • basepage.py,主要是公共方法的封装,比如元素等待、查找、点击等,并捕获异常,输出日志、截图信息等。具体代码如下:
import os
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from datetime import datetime
from Common.handle_logging import do_log

from Common.dir_config import screenshot_dir

"""目标:封装基本关键字,公共使用方法,对任何一个页面操作都可以实时捕捉异常,输出操作日志,失败截图"""


class BasePage:

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

	# 等待元素可见方法封装
    def wait_element_visible(self, loc, img_doc, timeout=20, frequency=0.5):
        do_log.info("在{}等待元素{}可见".format(img_doc, loc))
        start_time = datetime.now()
        try:
            WebDriverWait(self.driver, timeout, frequency).until(EC.visibility_of_element_located(loc))
        except Exception as e:
            # 要异常截图 - 通过截图名称,知道是那个页面,那个模块失败的。
            self.save_screenshot(img_doc)
            # 异常日志捕获
            do_log.error("等待元素{}可见失败。".format(loc))
            # 抛出异常
            raise e
        else:
            do_log.info("等待元素{}可见成功。".format(loc))
            end_time = datetime.now()
            do_log.info("等待时长为:{}".format((end_time-start_time).seconds))
            
	# 等待元素存在方法封装
    def wait_page_contains_element(self, loc, img_doc, timeout=20, frequency=0.5):
        do_log.info("在{}等待元素{}存在。".format(img_doc, loc))
        start_time = datetime.now()
        try:
            WebDriverWait(self.driver, timeout, frequency).until(EC.presence_of_element_located(loc))
        except Exception as e:
            self.save_screenshot(img_doc)
            do_log.error("等待元素{}存在失败。".format(loc))
            raise e
        else:
            do_log.info("等待元素{}存在成功。".format(loc))
            end_time = datetime.now()
            do_log.info("等待的时长为:{}".format((end_time-start_time).seconds))

	# 获取元素方法封装
    def get_element(self, loc, img_doc):
        """
        查找元素。
        loc:元素定位
        img_doc: 图片描述
        """
        do_log.info("在{}查找元素{}".format(img_doc, loc))
        start_time = datetime.now()
        try:
            ele = self.driver.find_element(*loc)
        except Exception as e:
            self.driver.save_screenshot(img_doc)
            do_log.error("查找元素{}失败。".format(loc))
            raise e
        else:
            do_log.error("查找元素{}成功。".format(loc))
            end_time = datetime.now()
            do_log.info("查找元素的时长为:{}".format((end_time - start_time).seconds))
            return ele

	# 点击元素方法封装
    def click_element(self, loc, img_doc, timeout=20, frequency=0.5):
        """
        前提:元素可见,找到元素
        """
        self.wait_element_visible(loc, img_doc, timeout, frequency)
        ele = self.get_element(loc, img_doc)
        do_log.info("在{}点击元素{}".format(img_doc, loc))
        try:
            ele.click()
            do_log.info("元素{}点击成功。".format(loc))
        except Exception as e:
            self.save_screenshot(img_doc)
            do_log.error("元素{}点击失败。".format(loc))
            raise e

	# 输入内容方法封装
    def input_text(self, loc, img_doc, text, timeout=20, frequency=0.5):
        """
        前提:元素可见,找到元素
        """
        self.wait_element_visible(loc, img_doc, timeout, frequency)
        ele = self.get_element(loc, img_doc)
        do_log.info("在{}的输入框{},输入:{}".format(img_doc, loc, text))
        try:
            ele.send_keys(text)
            do_log.info("元素{}输入内容{}成功".format(loc, text))
        except Exception as e:
            self.save_screenshot(img_doc)
            do_log.error("元素{}输入文本失败。".format(loc))
            raise e

	# 获取元素的文本内容方法封装
    def get_element_text(self, loc, img_doc, timeout=20, frequency=0.5):
        """
        前提:元素存在,找到元素
        """
        self.wait_page_contains_element(loc, img_doc, timeout, frequency)
        ele = self.get_element(loc, img_doc)
        do_log.info("在{}的获取元素{}的文本值".format(img_doc, loc))
        try:
            text = ele.text
        except Exception as e:
            self.save_screenshot(img_doc)
            do_log.error("获取文本值失败。")
            raise e
        else:
            do_log.info("获取元素{}文本内容成功,获取到的文本元素内容为:{}".format(loc, text))
            return text

	# 获取元素属性的方法封装
    def get_element_attr(self, loc, attr_name, img_doc, timeout=20, frequency=0.5):
        """
        前提:元素存在,找到元素
        """
        self.wait_page_contains_element(loc, img_doc, timeout, frequency)
        ele = self.get_element(loc, img_doc)
        do_log.info("在{}获取元素{}的属性{}。".format(img_doc, loc, attr_name))
        try:
            value = ele.get_attribute(attr_name)
        except Exception as e:
            self.save_screenshot(img_doc)
            do_log.error("获取元素{}属性值失败。".format(loc))
            raise e
        else:
            do_log.info("获取到的元素{}属性值成功,属性值为:{}".format(loc, value))
            return value

	# 保存截图的方法
    def save_screenshot(self, img_doc):
        # 存储到指定目录下
        # filename = screenshot_dir + "{}_{}.png".format(datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S'), img_doc)
        filename = os.path.join(screenshot_dir,
                                "{}_{}.png".format(datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S'), img_doc))
        self.driver.save_screenshot(filename)
        do_log.info("页面截图文件,保存在{}".format(filename))

  • handle_logging.py,日志的封装,定义日志输出信息,主要代码如下:
import logging
import os
from datetime import datetime
from Common.common_conf import logger_name, log_filename, logger_level, console_level, simple_formatter, file_level, verbose_formatter
from Common.dir_config import logs_dir


class HandleLog:
    """
    封装日志处理的类
    """
    def __init__(self):

        # 定义日志收起器, 创建logger对象
        self.case_logger = logging.getLogger(logger_name)

        # 日志等级 NOTSET(0), DEBUG(10), INFO(20), WARNING(30), ERROR(40), CRITICAL(50)
        # 设置之后,只能收集当前等级及以上的日志信息。如设置为warning级别,只能手机warning、error、critical等级的
        # case_logger.setLevel(logging.DEBUG)
        self.case_logger.setLevel(logger_level)

        # 定义日志输出渠道
        # 输出到控制台
        console_handler = logging.StreamHandler(console_level)
        # 输出到文件
        print("log输出路径:{}".format(logs_dir))
        log_name = os.path.join(logs_dir, datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S') + log_filename)

        file_handler = logging.FileHandler(log_name,
                                           encoding='utf-8')

        # 日志输出渠道的等级, 日志输出的等级,不能高于收集器的等级
        # console_handler.setLevel(logging.ERROR) 与 console_handler.setLevel("DEBUG")等价
        console_handler.setLevel(console_level)
        file_handler.setLevel(file_level)

        # 定义日志显示的格式
        # 简单格式
        simple = logging.Formatter(simple_formatter)
        # 稍微详细的格式
        verbose = logging.Formatter(verbose_formatter)

        # 控制台显示简单日志
        console_handler.setFormatter(simple)
        # 文件显示稍微详细的日志
        file_handler.setFormatter(verbose)

        # 将日志收集器与输出渠道对接
        self.case_logger.addHandler(console_handler)
        self.case_logger.addHandler(file_handler)

    def get_logger(self):
        return self.case_logger

# 创建对象,可以直接调用
do_log = HandleLog().get_logger()


if __name__ == "__main__":
    do_log.debug("debug")
  • common_conf.py,存放公共的配置信息,比如,我这里只配置了log的设置:
# 日志收集器名称
logger_name = "case_log"
# 日志收集器的级别
logger_level = "DEBUG"

# 输出的日志名称
log_filename = "cases.log"
# 输出到控制台的日志级别
console_level = "ERROR"
# 输出到日志文 件中的级别
file_level = "DEBUG"
# 日志输出内容
simple_formatter = "%(asctime)s - [%(levelname)s] - [msg]: %(message)s"
verbose_formatter = "%(asctime)s - [%(levelname)s] - %(lineno)d - %(name)s - [msg]: %(message)s"
  • dir_config.py,项目路径的配置文件:
import os


# 项目根目录
base_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]
print(base_dir)
# 截图存放地址
screenshot_dir = os.path.join(base_dir, "Outputs\\screenshots")
# 日志存放地址
logs_dir = os.path.join(base_dir, "Outputs\\logs")
# 报告存放地址
reports_dir = os.path.join(base_dir, "Outputs\\reports")
# 测试用例存放地址
test_cases_dir = os.path.join(base_dir, "TestCases")

Outputs目录

基于PageObject模式设计的web自动化测试示例

  • logs存放日志文件
  • reports存放测试报告
  • screenshots存放错误截图

PageLocators

基于PageObject模式设计的web自动化测试示例

  • login_page_locators.py,登录界面的元素定位:
from selenium.webdriver.common.by import By


class LoginPageLocators:
    """
    账号登录的定位
    """
    # 账户输入框
    user_input_box = (By.XPATH, '//input[@class="el-input__inner" and @placeholder="请输入邮箱/手机号/账号"]')
    # 密码输入框
    password_input_box = (By.XPATH, '//input[@class="el-input__inner" and @placeholder="请输入密码"]')
    # 登录按钮
    login_button = (By.XPATH, '//div[@class="login-tab"]/div/div/button')

    # 不输入密码时的提示信息: 请输入密码 的定位
    not_input_password_err_msg = (By.XPATH, '//*[@class="el-form-item__error"]')

    # 不输入账户时弹出的提示信息:用户名不能为空 的定位
    user_is_null_msg = (By.XPATH, '//*[text()="用户名不能为空"]')

    # 输入不存在的用户时,弹出的提示信息:用户不存在 的定位
    user_not_exist_msg = (By.XPATH, '//*[text()="用户不存在"]')
  • main_page_locators.py, 主页元素定位:
from selenium.webdriver.common.by import By


class MainPageLocator:

    # 用户头像的元素定位
    user_logo = (By.XPATH, '//img[@class="avatar"]')

PageObjects

基于PageObject模式设计的web自动化测试示例

  • login_page.py,登录界面的操作方法:
from PageLocators.login_page_locators import LoginPageLocators
from Common.basepage import BasePage
from selenium.webdriver.common.keys import Keys


class LoginPage(BasePage):

    def account_login(self, username, password):
    	# 账户登录,登录方法
        # 输入用户名
        self.input_text(LoginPageLocators.user_input_box, "登录界面_账户登录_账户输入框", username)
        # 输入密码
        self.input_text(LoginPageLocators.password_input_box, "登录界面_账户登录_密码输入框", password)
        # 点击 登录按钮
        # self.click_element(LoginPageLocators.login_button, "登录界面_账户登录_登录按钮")
        # 点击enter键
        self.get_element(LoginPageLocators.login_button, "登录界面_账户登录_点击enter").send_keys(Keys.ENTER)

    def get_no_password_err_msg(self):
        """
        不输入密码时,会出现文本提示信息:请输入密码
        """
        text = self.get_element_text(LoginPageLocators.not_input_password_err_msg, "登录页_不输入密码_提示信息")
        # 返回错误提示信息
        return text

    def get_alert_err_msg(self, locator):
        """
        不输入用户名、密码错误、账户不存在时,点击登录,会弹出提示框提示,获取提示框中的错误信息
        """
        text = self.get_element_text(locator, "登录页_不输入用户名、密码错误、账户不存在_弹出错误信息")
        # 返回错误提示信息
        return text
  • main.py,主页中的操作方法:
from Common.basepage import BasePage
from PageLocators.main_page_locator import MainPageLocator


class MainPage(BasePage):

    def if_user_login_is_exist(self):
        """
        判断元素是否存在,存在返回True, 不存在返回False
        :return boolean
        """
        try:
            self.wait_element_visible(MainPageLocator.user_logo, "主页_用户头像元素")
        except TimeoutError:
            return False
        else:
            return True

TestCases

基于PageObject模式设计的web自动化测试示例

  • test_login.py,存放测试登录的用例(设计了4条用例):

import unittest
from selenium import webdriver
from PageLocators.login_page_locators import LoginPageLocators
from PageObjects.login_page import LoginPage
from PageObjects.main_page import MainPage
from TestDatas import common_datas as cd
from TestDatas import login_datas as ld
from Common.handle_logging import do_log


class TestLogin(unittest.TestCase):

    def setUp(self) -> None:
        do_log.info("打开浏览器")
        # 打开谷歌浏览器,访问课堂派
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.driver.get(cd.login_url)
        self.lp = LoginPage(self.driver)

    def tearDown(self) -> None:
        do_log.info("退出浏览器")
        # 退出浏览器会话
        self.driver.quit()

    def test_login_success(self):
        """
        测试场景:登录成功
        """
        do_log.info("执行用例:test_login_success")
        self.lp.account_login(ld.normal_datas['user'], ld.normal_datas['passwd'])
        self.assertTrue(MainPage(self.driver).if_user_login_is_exist())

    def test_login_no_password(self):
        """
        测试场景:不输入密码
        """
        do_log.info("执行用例:test_login_no_password")
        self.lp.account_login(ld.login_no_password_datas['user'], ld.login_no_password_datas['passwd'])
        self.assertEqual(ld.login_no_password_datas['check'], self.lp.get_no_password_err_msg())

    def test_login_no_user(self):
        """
        测试场景:不输入账户
        """
        do_log.info("执行用例:test_login_no_user")
        self.lp.account_login(ld.login_no_user_datas['user'], ld.login_no_user_datas['passwd'])
        self.assertTrue(ld.login_no_user_datas['check'], self.lp.get_alert_err_msg(LoginPageLocators.user_is_null_msg))

    def test_login_user_not_exist(self):
        """
        测试场景:输入的账户不存在
        """
        do_log.info("执行用例:test_login_user_not_exist")
        self.lp.account_login(ld.login_user_not_exist['user'], ld.login_user_not_exist['passwd'])
        self.assertTrue(ld.login_user_not_exist['check'], self.lp.get_alert_err_msg(LoginPageLocators.user_not_exist_msg))

TestDatas

基于PageObject模式设计的web自动化测试示例

  • common_datas.py, 公共数据:
# 登录地址
login_url = '登录地址'

# 登录信息
test_datas = {"user": "账户信息", "passwd": "密码"}

# 正常场景的数据
normal_datas = {
    "user": "账户", "passwd": "密码"
}

# 异常场景的数据
login_no_password_datas = {
    "user": "账户", "passwd": "", "check": "请输入密码"
}

login_no_user_datas = {
    "user": "", "passwd": "密码", "check": "用户名不能为空"
}

login_user_not_exist = {
    "user": "不存在的账户", "passwd": "密码", "check": "用户不存在"
}

main.py

入口文件:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Time : 2021/10/16 22:03
# @Author : admin
# @File : main.py
# @Software: PyCharm


import unittest
import os
from datetime import datetime
from HTMLTestRunner import HTMLTestRunner
from Common.dir_config import reports_dir, test_cases_dir


# 指定路径下查找用例, 可以输入. 代表当前目录
suit = unittest.defaultTestLoader.discover(test_cases_dir)

"""
以日期命名报告,避免覆盖
"""
str_now = datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S')
print("report的输出路径:{}".format(reports_dir))
name = os.path.join(reports_dir, str_now +"_report.html")

report = open(name, mode='wb')
"""
verbosity 代表报告的详细程度, 0 report 2 , 2是最详细的
"""
runner = HTMLTestRunner(stream=report, title='课堂派的web的报告', verbosity=2, description='测试课堂派的web报告')
runner.run(suit)
report.close()

上一篇:KNN实现鸢尾花数据集的可视化


下一篇:ruby on rails 添加vote