UI 自动化测试框架:PO 模式+数据驱动 【详解版】

2023-11-14

目录

1. PO 设计模式简介

什么是 PO 模式?

PO 模式的优点

2. 工程结构简介

工程结构

框架特点

3. 工程代码示例

page 包

action 包

business_process 包

util 包

conf 包


1. PO 设计模式简介

什么是 PO 模式?

PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Page 类,并以页面为单位来写测试用例,实现页面对象和测试用例的分离。

PO 模式的设计思想与面向对象相似,能让测试代码变得可读性更好,可维护性高,复用性高。


PO 模式可以把一个页面分为三个层级:对象库层、操作层、业务层。

  1. 对象库层:封装定位元素的方法。

  2. 操作层:封装对元素的操作。

  3. 业务层:将一个或多个操作组合起来完成一个业务功能。

一条测试用例可能需要多个步骤操作元素,将每一个步骤单独封装成一个方法,在执行测试用例时调用封装好的方法进行操作。

PO 模式的优点

  • 通过页面分层,将测试代码和被测试页面的页面元素及其操作方法进行分离,降低代码冗余。

  • 页面对象与用例分离,业务代码与测试代码分离,降低耦合性。

  • 不同层级分属不同用途,降低维护成本。

  • 代码可阅读性增强,整体流程更为清晰。

2. 工程结构简介

工程结构

整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。

第一层是“测试工具层”:

  • util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作Excel文件等。
  • conf 包:配置文件及全局变量。
  • test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。
  • log 目录:日志输出文件。
  • screenshot_path 目录:异常截图保存目录。

第二层是“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于页面测试,是对页面元素或操作的一个封装。

  • page 包:对象库层及操作层,将所有页面的元素对象定位及其操作分别封装成一个类。

第三层是“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。

  • action 包:组装单个用例的流程。

  • business_process 包:基于业务层和测试数据文件,执行测试用例集合。

  • test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。 
    第四层是“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理、冒烟,回归等测试场景。 

  • main.py:本 PO 框架的运行主入口。

在这里插入图片描述
在这里插入图片描述

框架特点

  1. 通过配置文件,实现页面元素定位方式和测试代码的分离。
  2. 使用 PO 模式,封装了网页中的页面元素,方便测试代码调用,也实现了一处维护全局生效的目标。
  3. 在 excel 文件中定义多组测试数据,每个登录用户都一一对应一个存放联系人数据的 sheet,测试框架可自动调用测试数据完成数据驱动测试。
  4. 实现了测试执行过程中的日志记录功能,可以通过日志文件分析测试脚本执行的情况。
  5. 在 excel 数据文件中,通过设定“测试数据是否执行”列的内容为 y 或 n,自定义选择测试数据,测试执行结束后会在"测试结果列"中显示测试执行的时间和结果,方便测试人员查看。

3. 工程代码示例

page 包

对象库层及操作层,将所有页面的元素对象定位及其操作分别封装成一个类。

login_page.py

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *


# 登录页面元素定位及操作
class LoginPage:

    def __init__(self, driver):
        self.driver = driver
        # 初始化跳转登录页面
        self.driver.get(LOGIN_URL)
        # 初始化指定ini配置文件及指定分组
        self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_loginPage")

    # 获取frame元素对象
    def get_frame_obj(self):
        locate_method, locate_exp = self.cf.get_value("loginPage.frame").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 切换frame
    def switch_frame(self):
        self.driver.switch_to.frame(self.get_frame_obj())

    # 获取用户名输入框元素对象
    def get_username_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("loginPage.username").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 清空用户名输入框操作
    def clear_username(self):
        self.get_username_input_obj().clear()

    # 输入用户名操作
    def input_username(self, value):
        self.get_username_input_obj().send_keys(value)

    # 获取密码输入框元素对象
    def get_pwd_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("loginPage.password").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 输入密码操作
    def input_pwd(self, value):
        self.get_pwd_input_obj().send_keys(value)

    # 获取登录按钮对象
    def get_login_buttion_obj(self):
        locate_method, locate_exp = self.cf.get_value("loginPage.loginbutton").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 点击登录按钮操作
    def click_login_button(self):
        self.get_login_buttion_obj().click()

home_page.py

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *


# 登录后主页元素定位及操作
class HomePage:

    def __init__(self, driver):
        self.driver = driver
        # 初始化指定ini配置文件及指定分组
        self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_homePage")

    # 获取“通讯录”按钮对象
    def get_contact_button_obj(self):
        locate_method, locate_exp = self.cf.get_value("homePage.addressLink").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 点击“通讯录”按钮
    def click_contact_button(self):
        self.get_contact_button_obj().click()

contact_page.py

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *


# 通讯录页面元素定位及操作
class ContactPage:

    def __init__(self, driver):
        self.driver = driver
        # 初始化指定ini配置文件及指定分组
        self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_contactPersonPage")

    # 获取新建联系人按钮对象
    def get_contact_create_button_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.createButton").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 点击新建联系人按钮
    def click_contact_creat_button(self):
        self.get_contact_create_button_obj().click()

    # 获取姓名输入框对象
    def get_name_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.name").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 输入姓名操作
    def input_name(self, value):
        self.get_name_input_obj().send_keys(value)

    # 获取邮箱输入框对象
    def get_email_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.email").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 输入邮箱操作
    def input_email(self, value):
        self.get_email_input_obj().send_keys(value)

    # 获取星标联系人单选框对象
    def get_star_button_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.starContacts").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 点击星标联系人操作
    def click_star_button(self):
        self.get_star_button_obj().click()

    # 获取手机输入框对象
    def get_phone_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.phone").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 输入邮箱操作
    def input_phone(self, value):
        self.get_phone_input_obj().send_keys(value)

    # 获取备注输入框对象
    def get_remark_input_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.otherinfo").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 输入邮箱操作
    def input_remark(self, value):
        self.get_remark_input_obj().send_keys(value)

    # 获取确定按钮对象
    def get_confirm_button_obj(self):
        locate_method, locate_exp = self.cf.get_value("contactPersonPage.confirmButton").split(">")
        return find_element(self.driver, locate_method, locate_exp)

    # 点击星标联系人操作
    def click_confirm_button(self):
        self.get_confirm_button_obj().click()

action 包

业务层,将一个或多个操作组合起来完成一个业务功能。

case_action.py

from selenium import webdriver
import traceback
import time
from page.contact_page import ContactPage
from page.home_page import HomePage
from page.login_page import LoginPage
from conf.global_var import *
from util.log_util import *


# 初始化浏览器
def init_browser(browser_name):
    if browser_name.lower() == "chrome":
        driver = webdriver.Chrome(CHROME_DRIVER)
    elif browser_name.lower() == "firefox":
        driver = webdriver.Firefox(FIREFOX_DRIVER)
    elif browser_name.lower() == "ie":
        driver = webdriver.Ie(IE_DRIVER)
    else:
        return "Error browser name!"
    return driver


def assert_word(driver, text):
    assert text in driver.page_source


# 登录流程封装
def login(driver, username, pwd, assert_text):
    login_page = LoginPage(driver)
    login_page.switch_frame()
    login_page.clear_username()
    login_page.input_username(username)
    login_page.input_pwd(pwd)
    login_page.click_login_button()
    time.sleep(1)
    assert_word(driver, assert_text)


# 添加联系人流程封装
def add_contact(driver, name, email, phone, is_star, remark, assert_text):
    home_page = HomePage(driver)
    home_page.click_contact_button()
    contact_page = ContactPage(driver)
    contact_page.click_contact_creat_button()
    contact_page.input_name(name)
    contact_page.input_email(email)
    contact_page.input_phone(phone)
    contact_page.input_remark(remark)
    if is_star == "是":
        contact_page.click_star_button()
    contact_page.click_confirm_button()
    time.sleep(2)
    assert_word(driver, assert_text)


def quit(driver):
    driver.quit()


if __name__ == "__main__":
    driver = init_browser("chrome")
    login(driver, "zhangjun252950418", "zhangjun123", "退出")
    add_contact(driver, "铁蛋", "asfhi@123.com", "12222222222", "是", "这是备注", "铁蛋")
    # quit(driver)

business_process 包

基于业务层和测试文件,实现数据驱动的测试执行脚本。

batch_login_process.py

from action.case_action import *
from util.excel_util import *
from conf.global_var import *
from util.datetime_util import *
from util.screenshot import take_screenshot


# 封装测试数据文件中用例的执行逻辑
# 测试数据文件中的每个登录账号
def batch_login(test_data_file, browser_name, account_sheet_name):
        excel = Excel(test_data_file)
        # 获取登录账号sheet页数据
        excel.change_sheet(account_sheet_name)
        account_all_data = excel.get_all_row_data()
        account_headline_data = account_all_data[0]
        for account_row_data in account_all_data[1:]:
            # 执行登录用例
            account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()
            if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":
                continue
            # 初始化浏览器
            driver = init_browser(browser_name)
            try:
                # 默认以"退出"作为断言关键字
                login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")
                info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],
                                                            account_row_data[ACCOUNT_PWD_COL], "退出"))
                account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"
            except:
                error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],
                                                            account_row_data[ACCOUNT_PWD_COL], "退出"))
                account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"
                account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()
                account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)
            # 写入登录用例的测试结果
            excel.change_sheet("测试结果")
            excel.write_row_data(account_headline_data, "red")
            excel.write_row_data(account_row_data)
            excel.save()

            # 切换另一个账号时需先关闭浏览器,否则会自动登录
            driver.quit()


if __name__ == "__main__":
    batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")

batch_login_and_add_contact_process.py

from action.case_action import *
from util.excel_util import *
from conf.global_var import *
from util.datetime_util import *
from util.screenshot import take_screenshot


# 封装测试数据文件中用例的执行逻辑
# 测试数据文件中每个登录账号下,添加所有联系人数据
def batch_login_and_add_contact(test_data_file, browser_name, account_sheet_name):
        excel = Excel(test_data_file)
        # 获取登录账号sheet页数据
        excel.change_sheet(account_sheet_name)
        account_all_data = excel.get_all_row_data()
        account_headline_data = account_all_data[0]
        for account_row_data in account_all_data[1:]:
            # 执行登录用例
            account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()
            if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":
                continue
            # 初始化浏览器
            driver = init_browser(browser_name)
            # 获取联系人数据sheet
            contact_data_sheet = account_row_data[ACCOUNT_DATA_SHEET_COL]
            try:
                # 默认以"退出"作为断言关键字
                login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")
                info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],
                                                            account_row_data[ACCOUNT_PWD_COL], "退出"))
                account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"
            except:
                error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],
                                                            account_row_data[ACCOUNT_PWD_COL], "退出"))
                account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"
                account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()
                account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)
            # 写入登录用例的测试结果
            excel.change_sheet("测试结果")
            excel.write_row_data(account_headline_data, "red")
            excel.write_row_data(account_row_data)
            excel.save()

            # 执行添加联系人用例
            excel.change_sheet(contact_data_sheet)
            contact_all_data = excel.get_all_row_data()
            contact_headline_data = contact_all_data[0]
            # 在测试结果中,一个账号下的联系人数据标题行仅写一次
            contact_headline_flag = True
            for contact_row_data in contact_all_data[1:]:
                if contact_row_data[CONTACT_IS_EXECUTE_COL].lower() == "n":
                    continue
                contact_row_data[CONTACT_TEST_TIME_COL] = get_english_datetime()
                try:
                    add_contact(driver, contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],
                                contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],
                                contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL])
                    info("添加联系人成功【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, "
                         "备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],
                                                   contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],
                                                   contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))
                    contact_row_data[CONTACT_TEST_RESULT_COL] = "pass"
                except:
                    error("添加联系人失败【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, "
                         "备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],
                                                   contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],
                                                   contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))
                    contact_row_data[CONTACT_TEST_RESULT_COL] = "fail"
                    contact_row_data[CONTACT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()
                    contact_row_data[CONTACT_SCREENSHOT_COL] = take_screenshot(driver)
                # 写入登录用例的测试结果
                excel.change_sheet("测试结果")
                if contact_headline_flag:
                    excel.write_row_data(contact_headline_data, "red")
                    contact_headline_flag = False
                excel.write_row_data(contact_row_data)
                excel.save()

            # 切换另一个账号时需先关闭浏览器,否则会自动登录
            driver.quit()


if __name__ == "__main__":
    batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")

util 包

用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作Excel文件等。

excel_util.py

(openpyxl 版本:3.0.4)

from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font, Side, Border
import os


class Excel:

    def __init__(self, test_data_file_path):
        # 文件格式校验
        if not os.path.exists(test_data_file_path):
            print("Excel工具类初始化失败:【{}】文件不存在!".format(test_data_file_path))
            return
        if not test_data_file_path.endswith(".xlsx") or not test_data_file_path.endswith(".xlsx"):
            print("Excel工具类初始化失败:【{}】文件非excel文件类型!".format(test_data_file_path))
            return
        # 打开指定excel文件
        self.wb = load_workbook(test_data_file_path)
        # 初始化默认sheet
        self.ws = self.wb.active
        # 保存文件时使用的文件路径
        self.test_data_file_path = test_data_file_path
        # 初始化红、绿色,供样式使用
        self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}

    # 查看所有sheet名称
    def get_sheets(self):
        return self.wb.sheetnames

    # 根据sheet名称切换sheet
    def change_sheet(self, sheet_name):
        if sheet_name not in self.get_sheets():
            print("sheet切换失败:【{}】指定sheet名称不存在!".format(sheet_name))
            return
        self.ws = self.wb.get_sheet_by_name(sheet_name)

    # 返回当前sheet的最大行号
    def max_row_num(self):
        return self.ws.max_row

    # 返回当前sheet的最大列号
    def max_col_num(self):
        return self.ws.max_column

    # 获取指定行数据(设定索引从0开始)
    def get_one_row_data(self, row_no):
        if row_no < 0 or row_no > self.max_row_num()-1:
            print("输入的行号【{}】有误:需在0至最大行数之间!".format(row_no))
            return
        # API的索引从1开始
        return [cell.value for cell in self.ws[row_no+1]]

    # 获取指定列数据
    def get_one_col_data(self, col_no):
        if col_no < 0 or col_no > self.max_col_num()-1:
            print("输入的列号【{}】有误:需在0至最大列数之间!".format(col_no))
            return
        return [cell.value for cell in tuple(self.ws.columns)[col_no+1]]

    # 获取当前sheet的所有行数据
    def get_all_row_data(self):
        result = []
        # # API的索引从1开始
        for row_data in self.ws[1:self.max_row_num()]:
            result.append([cell.value if cell.value is not None else "" for cell in row_data])
        return result

    # 追加一行数据
    def write_row_data(self, data, fill_color=None, font_color=None, border=True):
        if not isinstance(data, (list, tuple)):
            print("追加的数据类型有误:需为列号或元组类型!【{}】".format(data))
            return
        self.ws.append(data)
        # 添加字体颜色
        if font_color:
            if font_color in self.color_dict.keys():
                font_color = self.color_dict[font_color]
            # 需要设置的单元格长度应与数据长度一致,否则默认与之前行的长度一致
        count = 0
        for cell in self.ws[self.max_row_num()]:
            if count > len(data) - 1:
                break
            # cell不为None,才能设置样式
            if cell:
                if cell.value in ["pass", "成功"]:
                    cell.font = Font(color=self.color_dict["green"])
                elif cell.value in ["fail", "失败"]:
                    cell.font = Font(color=self.color_dict["red"])
                else:
                    cell.font = Font(color=font_color)
            count += 1
        # 添加背景颜色
        if fill_color:
            if fill_color in self.color_dict.keys():
                fill_color = self.color_dict[fill_color]
            count = 0
            for cell in self.ws[self.max_row_num()]:
                if count > len(data) - 1:
                    break
                if cell:
                    cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)
                count += 1
        # 添加单元格边框
        if border:
            bd = Side(style="thin", color="000000")
            count = 0
            for cell in self.ws[self.max_row_num()]:
                if count > len(data) - 1:
                    break
                if cell:
                    cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)
                count += 1

    # 保存文件
    def save(self):
        self.wb.save(self.test_data_file_path)


if __name__ == "__main__":
    from conf.global_var import *
    excel = Excel(TEST_DATA_FILE_PATH)
    excel.change_sheet("登录1")
    # print(excel.get_all_row_data())
    excel.write_row_data((1,2,"嘻哈",None,"ddd"), "red", "green")
    excel.save()

find_element_util.py

from selenium.webdriver.support.ui import WebDriverWait


# 显式等待一个对象
def find_element(driver, locate_method, locate_exp):
    # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)
    return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp))


# 显式等待一组对象
def find_elements(driver, locate_method, locate_exp):
    # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)
    return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))

ini_parser.py

import configparser


class IniParser:

    # 初始化打开指定ini文件并指定编码
    def __init__(self, file_path, section):
        self.cf = configparser.ConfigParser()
        self.cf.read(file_path, encoding="utf-8")
        self.section = section

    # 获取所有分组名称
    def get_sections(self):
        return self.cf.sections()

    # 获取指定分组的所有键
    def get_options(self):
        return self.cf.options(self.section)

    # 获取指定分组的键值对
    def get_items(self):
        return self.cf.items(self.section)

    # 获取指定分组的指定键的值
    def get_value(self, key):
        return self.cf.get(self.section, key)

datetime_util.py

import time


# 返回中文格式的日期:xxxx年xx月xx日
def get_chinese_date():
    year = time.localtime().tm_year
    if len(str(year)) == 1:
        year = "0" + str(year)
    month = time.localtime().tm_mon
    if len(str(month)) == 1:
        month = "0" + str(month)
    day = time.localtime().tm_mday
    if len(str(day)) == 1:
        day = "0" + str(day)
    return "{}年{}月{}日".format(year, month, day)


# 返回英文格式的日期:xxxx/xx/xx
def get_english_date():
    year = time.localtime().tm_year
    if len(str(year)) == 1:
        year = "0" + str(year)
    month = time.localtime().tm_mon
    if len(str(month)) == 1:
        month = "0" + str(month)
    day = time.localtime().tm_mday
    if len(str(day)) == 1:
        day = "0" + str(day)
    return "{}/{}/{}".format(year, month, day)


# 返回中文格式的时间:xx时xx分xx秒
def get_chinese_time():
    hour = time.localtime().tm_hour
    if len(str(hour)) == 1:
        hour = "0" + str(hour)
    minute = time.localtime().tm_min
    if len(str(minute)) == 1:
        minute = "0" + str(minute)
    second = time.localtime().tm_sec
    if len(str(second)) == 1:
        second = "0" + str(second)
    return "{}时{}分{}秒".format(hour, minute, second)


# 返回英文格式的时间:xx:xx:xx
def get_english_time():
    hour = time.localtime().tm_hour
    if len(str(hour)) == 1:
        hour = "0" + str(hour)
    minute = time.localtime().tm_min
    if len(str(minute)) == 1:
        minute = "0" + str(minute)
    second = time.localtime().tm_sec
    if len(str(second)) == 1:
        second = "0" + str(second)
    return "{}:{}:{}".format(hour, minute, second)


# 返回中文格式的日期时间
def get_chinese_datetime():
    return get_chinese_date() + " " + get_chinese_time()


# 返回英文格式的日期时间
def get_english_datetime():
    return get_english_date() + " " + get_english_time()


if __name__ == "__main__":
    print(get_chinese_datetime())
    print(get_english_datetime())

log_util.py

import logging
import logging.config
from conf.global_var import *


# 日志配置文件:多个logger,每个logger指定不同的handler
# handler:设定了日志输出行的格式
#          以及设定写日志到文件(是否回滚)?还是到屏幕
#          还定了打印日志的级别
logging.config.fileConfig(LOG_CONF_FILE_PATH)
logger = logging.getLogger("example01")


def debug(message):
    logging.debug(message)


def info(message):
    logging.info(message)


def warning(message):
    logging.warning(message)


def error(message):
    logging.error(message)


if __name__ == "__main__":
    debug("hi")
    info("gloryroad")
    warning("hello")
    error("这是一个error日志")

screenshot.py

import traceback
import os
from util.datetime_util import *
from conf.global_var import *


# 截图函数
def take_screenshot(driver):
    # 创建当前日期目录
    dir = os.path.join(SCREENSHOT_PATH, get_chinese_date())
    if not os.path.exists(dir):
        os.makedirs(dir)
    # 以当前时间为文件名
    file_name = get_chinese_time()
    file_path = os.path.join(dir, file_name+".png")
    try:
        driver.get_screenshot_as_file(file_path)
        # 返回截图文件的绝对路径
        return file_path
    except:
        print("截图发生异常【{}】".format(file_path))
        traceback.print_exc()
        return file_path

conf 包

配置文件及全局变量。

elements_repository.ini

[126mail_loginPage]
loginPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')]
loginPage.username=xpath>//input[@name='email']
loginPage.password=xpath>//input[@name='password']
loginPage.loginbutton=id>dologin

[126mail_homePage]
homePage.addressLink=xpath>//div[text()='通讯录']

[126mail_contactPersonPage]
contactPersonPage.createButton=xpath>//span[text()='新建联系人']
contactPersonPage.name=xpath>//a[@title='编辑详细姓名']/preceding-sibling::div/input
contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input
contactPersonPage.starContacts=xpath>//span[text()='设为星标联系人']/preceding-sibling::span/b
contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input
contactPersonPage.otherinfo=xpath>//textarea
contactPersonPage.confirmButton=xpath>//span[.='确 定']

global_var.py

import os


# 工程根路径
PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# 元素定位方法的ini配置文件路径
ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "elements_repository.ini")

# 驱动路径
CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe"
IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe"
FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe"

# 测试使用的浏览器
BROWSER_NAME = "chrome"

# 登录主页
LOGIN_URL = "https://mail.126.com"

# 日志配置文件路径
LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "logger.conf")

# 测试用例文件路径
TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "测试用例.xlsx")

# 截图保存路径
SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "screenshot_path")

# 单元测试报告输出目录
UNITTEST_REPORT_PATH = os.path.join(PROJECT_ROOT_PATH, "report")

# 登录账号sheet页数据列号
ACCOUNT_USERNAME_COL = 1
ACCOUNT_PWD_COL = 2
ACCOUNT_DATA_SHEET_COL = 3
ACCOUNT_IS_EXECUTE_COL = 4
ACCOUNT_TEST_TIME_COL = 5
ACCOUNT_TEST_RESULT_COL = 6
ACCOUNT_TEST_EXCEPTION_INFO_COL = 7
ACCOUNT_SCREENSHOT_COL = 8

# 联系人sheet页数据列号
CONTACT_NAME_COL = 1
CONTACT_EMAIL_COL = 2
CONTACT_IS_STAR_COL = 3
CONTACT_PHONE_COL = 4
CONTACT_REMARK_COL = 5
CONTACT_ASSERT_KEYWORD_COL = 6
CONTACT_IS_EXECUTE_COL = 7
CONTACT_TEST_TIME_COL = 8
CONTACT_TEST_RESULT_COL = 9
CONTACT_TEST_EXCEPTION_INFO_COL = 10
CONTACT_SCREENSHOT_COL = 11


if __name__ == "__main__":
    print(PROJECT_ROOT_PATH)

logger.conf

###############################################
[loggers]
keys=root,example01,example02
[logger_root]
level=DEBUG
handlers=hand01,hand02

[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0

[logger_example02]
handlers=hand01,hand03
qualname=example02
propagate=0

###############################################
[handlers]
keys=hand01,hand02,hand03

[handler_hand01]
class=StreamHandler
level=INFO
formatter=form01
args=(sys.stderr,)

[handler_hand02]
class=FileHandler
level=DEBUG
formatter=form01
args=('.\\log\\126_mail_test.log', 'a')

[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('.\\log\\126_mail_test.log', 'a', 10*1024*1024, 5)

###############################################
[formatters]
keys=form01,form02

[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=%Y-%m-%d %H:%M:%S

[formatter_form02]
format=%(name)-12s: %(levelname)-8s %(message)s
datefmt=%Y-%m-%d %H:%M:%S

test_data 目录

测试用例.xlsx:包含测试数据输入、测试结果输出

在这里插入图片描述
在这里插入图片描述

log 目录

日志输出文件:126_mail_test.log

...
...
2021-02-23 16:59:15 log_util.py[line:19] INFO 登录成功【用户名:zhangjun252950418, 密码:zhangjun123, 断言关键字:退出】
2021-02-23 16:59:20 log_util.py[line:19] INFO 添加联系人成功【姓名:lily, 邮箱:lily@qq.com, 手机号:135xxxxxxx1, 是否星标联系人:是, 备注:常联系人, 断言关键字:lily@qq.com】
2021-02-23 16:59:24 log_util.py[line:27] ERROR 添加联系人失败【姓名:张三, 邮箱:zhangsan@qq.com, 手机号:158xxxxxxx3, 是否星标联系人:否, 备注:不常联系人, 断言关键字:zhangsan@qq.comxx】
2021-02-23 16:59:27 log_util.py[line:19] INFO 添加联系人成功【姓名:李四, 邮箱:lisi@qq.com, 手机号:157xxxxxx9, 是否星标联系人:否, 备注:, 断言关键字:李四】
...
...

screenshot_path 目录

异常截图保存目录:

在这里插入图片描述

main.py

本 PO 框架的运行主入口。

from business_process.batch_login import *
from business_process.batch_login_and_add_contact import *
from conf.global_var import *


# 示例组装:冒烟测试
def smoke_test():
    batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")


# 示例组装:全量测试
def full_test():
    batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")


if __name__ == "__main__":
    # smoke_test()
    full_test()

以下是我收集到的比较好的学习教程资源,虽然不是什么很值钱的东西,如果你刚好需要,可以评论区,留言【777】直接拿走就好了

各位想获取资料的朋友请点赞 + 评论 + 收藏,三连!

三连之后我会在评论区挨个私信发给你们~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

UI 自动化测试框架:PO 模式+数据驱动 【详解版】 的相关文章

  • 软件测试|使用matplotlib绘制多种折线图

    简介 在数据可视化领域 Matplotlib是一款非常强大的Python库 它可以用于绘制各种类型的图表 包括折线图 本文将介绍如何使用Matplotlib创建多种不同类型的折线图 并提供示例代码 创建模版 在绘图之前 我们可以先创建我们的
  • 软件测试|SQLAlchemy环境安装与基础使用

    简介 SQLAlchemy 是一个强大的 Python 库 用于与关系型数据库进行交互 它提供了高度抽象的对象关系映射 ORM 工具 允许使用 Python 对象来操作数据库 而不必编写原生SQL查询 本文将介绍如何安装 SQLAlchem
  • 软件测试|Selenium StaleElementReferenceException 异常分析与解决

    简介 Selenium 是一个流行的自动化测试工具 用于模拟用户与网页交互 然而 当我们在使用 Selenium 时 可能会遇到一个常见的异常 即 StaleElementReferenceException 这个异常通常在我们尝试与网页上
  • 软件测试|使用Python读写yaml文件,你会了吗?

    简介 YAML YAML Ain t Markup Language 是一种可读的数据序列化格式 它常用于配置文件和数据交换 Python 提供了许多库来处理 YAML 文件 在本文中 我们将探讨如何使用 PyYAML 库来读取和写入 YA
  • 软件测试|教你如何使用Python绘制出奥运五环旗

    简介 我们之前介绍过使用turtle来绘制正多边形 但是绘制正多边形只是turtle模块最基础的使用 我们可以使用turtle模块绘制出更多不一样的精彩图形 本文就来给大家介绍一个比较简单的turtle绘图实例 绘制奥运五环旗 初始化参数
  • 软件测试|教你使用Python下载图片

    前言 我一直觉得Windows系统默认的桌面背景不好看 但是自己又没有好的资源可以进行替换 突然我一个朋友提醒了我 网络上的图片这么多 你甚至可以每天换很多个好看的背景 但是如果让我手动去设置的话 我觉得太麻烦了 我不如使用技术手段将图片下
  • 软件测试|如何使用selenium处理iframe富文本输入框

    简介 在网页开发中 富文本框是常见的元素 用于输入富文本内容 如富文本编辑器或邮件编辑器 如果我们要使用Python和Selenium进行自动化测试或操作这种富文本框 可能会遇到一些挑战 本文将详细介绍如何使用Python和Selenium
  • 盲猜你不懂H5架构和原生架构的区别

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 跨平台UI自动化框架:Airtest,游戏开发和应用测试的利器

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 如何写好一个错误报告

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • msyql 异常,别干着急,70%的问题都在这里!

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 摸爬滚打多年的打工人,总结了三条职场真理,绝不假大空!

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读2 3k次 点赞85次 收藏11次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自
  • 软件测试中的白盒测试,这些技巧你知道吗?

    对于很多刚开始学习软件测试的小伙伴来说 如果能尽早将黑盒 白盒测试弄明白 掌握两种测试的结论和基本原理 将对自己后期的学习有较好的帮助 今天 我们就来聊聊黑盒 白盒测试的相关话题 1 黑盒测试的方法和小结 最常见黑盒测试方法包括 边界值 等
  • Python常用的自动化小脚本!

    一 list转json string转json 可以使用Python内置的 json 模块将列表 List 和字符串 String 转换成JSON格式 List转JSON假设我们有一个列表 List my list apple banana
  • 做测试不会 SQL?超详细的 SQL 查询语法教程来啦!

    前言 作为一名测试工程师 工作中在对测试结果进行数据比对的时候 或多或少要和数据库打交道的 要和数据库打交道 那么一些常用的sql查询语法必须要掌握 最近有部分做测试小伙伴表示sql查询不太会 问我有没有sql查询语法这一块的文档可以学习
  • 软件测试/测试开发/全日制/测试管理丨Android WebView 技术原理

    Android WebView是一个内置的组件 允许在Android应用中嵌套显示Web内容 Android WebView的技术原理涉及到使用WebKit引擎来渲染Web内容 并提供一系列API和回调函数 使得开发人员可以控制和定制Web
  • Web自动化测试 —— capability参数配置

    一 capability概述 capability是webdriver支持的标准命令之外的扩展命令 配置信息 配置web驱动属性 如浏览器名称 浏览器平台 结合selenium gird完成分布式 兼容性测试 官网地址 https www
  • 一文让你快速写出高效的软件测试用例

    前言 编写测试用例的目的就是确保测试过程全面高效 有据可查 但要编写出高效的测试用例 需要搞清楚什么是测试用例 以及如何编写出高效的测试用例 接下来将从以下几个部分来进行展开 1 什么是测试用例 2 如何编写测试用例 3 软件测试学习资源分
  • 深入解析 YAML 配置文件:从语法到最佳实践

    一 认识YAML YAML YAML Ain t Markup Language 是一种人类可读的数据序列化语言 它的设计目标是使数据在不同编程语言之间交换和共享变得简单 YAML采用了一种简洁 直观的语法 以易于阅读和编写的方式表示数据结
  • 用Python实现高效数据记录!Web自动化技术助你告别重复劳动!

    自动化关键数据记录 简介 关键数据记录是 Web 自动化测试中的关键部分 它们提供了关于系统行为和执行过程的详细信息 有助于验证用例的正确性 排查问题和确保应用程序的质量 行为日志 行为日志是一种用于记录系统或应用程序的操作和事件的技术 它

随机推荐

  • SnakeGame(贪吃蛇游戏)

    目录 一 前言 二 项目介绍 1 游戏的操作方式 2 开发的过程中的注意事项 1 图像的左右问题 2 摄像头的画面尺寸问题 三 游戏的实现要点 1 选择第三方库 2 找到关键点并标记 3 创建一个类来保存关于游戏的所有功能 4 定义函数进行
  • spring & bean

    1 spring的三种实例化bean方法 2 配置spring管理bean的作用域 3 spring管理的bean的生命周期 bean什么时候进行实例化 spring容器启动的时候 还是在调用geatBean 方法的时候 延时初始化 指定b
  • chatgpt赋能python:Python创建5×5矩阵(Matrix)教程

    Python创建5 5矩阵 Matrix 教程 Python是一种广泛使用的高级编程语言 其具有简单易用 可读性强 支持多种编程范式等特点 已经成为数据分析 科学计算与机器学习等领域必备的编程工具之一 在Python的诸多应用场景中 创建矩
  • OpenCV4-图像二值化

    OpenCV4 图像二值化 图像二值化 1 环境配置 OpenCV版本 OpenCV4 1 2 图像二值化 二值图像 图像中的像素灰度值无论在什么数据类型中都只有最大值和最小值两种取值 这种 非黑即白 的图像称为二值图像 将非二值图像经过计
  • Java基础——函数与数组

    上一节中给大家留得习题都完成的怎么样呀 有什么问题就给阿Q留言吧 阿Q会知无不言 言无不尽的 今天让我们进一步学习一下java中的函数与数组吧 函数 也称之为方法 1 定义在类中 有特定功能的一段小程序 可以提高代码的复用性和维护性 2 函
  • 毕业项目SSM框架配置文件之SqlMapConfig.xml

  • Registry学习资料

    官网 https registry project readthedocs io en latest index html
  • npm安装、使用方法

    文章目录 npm安装 使用方法 1 npm介绍 2 npm查看版本 3 npm查看所有命令列表 4 npm查看所有命令用法 5 npm查看配置 6 npm永久换源 7 npm查看换源是否成功 8 npm创建模块 9 npm查看项目已安装的所
  • 【springboot】@RequestBody和@ResponseBody以及@PathVariable的使用及区别

    1 RequestBody和 ResponseBody RequestBody 通过 requestBody可以将请求体中的JSON字符串绑定到相应的bean上 当然 也可以将其分别绑定到对应的字符串上 实质上接受的是一个JSON的字符串
  • 基于Linux用C语言实现TCP/UDP图片和文件传输(socket)

    目录 一 TCP实现 1 服务端 2 客户端 二 UDP实现 1 服务端 2 客户端 一 TCP实现 传输控制协议 TCP Transmission Control Protocol 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设
  • Python-turtle库

    star py import turtle turtle pensize 5 turtle color red turtle penup turtle goto 150 150 turtle pendown turtle begin fil
  • Opencv之Unsharp Mask(USM)锐化

    1 介绍 USM技术是通过增强图像的高频部分的内容来提高视觉效果 用具体的公式表达即为 y n m x n m z n m 其中 x n m 为输入图像 y n m 为输出图像 而z n m 为校正信号 一般是通过对x进行高通滤波获取 是用
  • Shader入门指南

    Unity3D Shader入门指南 一 分类 Unity3d2014 04 28 17 40 16人阅读 评论 0 收藏 举报 unity3dShader3d渲染 目录 本文转载自 http onevcat com 2013 07 sha
  • 25岁竟要求产品经验10年?我一直以为是个段子,没想到居然是真的

    你是不是以为工作3年要求有10年工作经验是个段子 今天告诉你这是真事儿 25岁要求工作经验10年 你好歹给人家工作5年的机会吧 这样也好说剩下的5年工作经验靠加班 HR这样说我没法接啊 哈哈 按照对方逻辑来 毕竟25岁都没有 相关工作经验最
  • TCP拥塞控制原理

    一 拥塞控制的一般原理 1 产生拥塞的原因 在某段时间 若对网络中某一资源的需求超过了该资源所能提供的可用部分 网络的性能就会变坏 即对资源的需求 可用资源 注意拥塞控制与流量控制的区别 拥塞控制是防止过多的数据注入网络中 使得网络中路由器
  • CentOS7平台命令安装Anaconda3、配置Python3开发环境

    要在 CentOS 7 上安装 Anaconda3 您可以按照以下步骤进行操作 1 下载 Anaconda3 安装包 首先 访问 Anaconda 官方网站以获取最新版本的 Anaconda3 安装包的下载链接 可以使用 wget 命令来下
  • 【数据库原理及应用教程(第4版

    文章目录 一 选择题 二 填空题 三 简答题 Reference 一 选择题 1 2 3 4 5 6 7 8 9 10 B C B D A 1 在 SQL Server 中不是对象的是 A 用户 B 数据 C 表 D 数据类型 2 声明了变
  • studio和solo哪个好_beats studio3 和solo3的区别再哪里?

    首先两款机子在外观上就有很大的区别 前者是solo3 后者是studio3 仔细观察可以发现solo3的耳罩比较小 是圆形的 studio3的是椭圆形的 两款都是头戴式 外观设计 录音师是包耳的 SOLO是压耳的 体积的话 录音师大 SOL
  • async与await

    async await 是ES7提出的基于Promise的解决异步的最终方案 一 async async是一个加在函数前的修饰符 被async定义的函数会默认返回一个Promise对象resolve的值 因此对async函数可以直接then
  • UI 自动化测试框架:PO 模式+数据驱动 【详解版】

    目录 1 PO 设计模式简介 什么是 PO 模式 PO 模式的优点 2 工程结构简介 工程结构 框架特点 3 工程代码示例 page 包 action 包 business process 包 util 包 conf 包 1 PO 设计模式