UI 自动化测试框架:PO模式+数据驱动

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)# 切换framedef 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 driverdef 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)# 获取联系人数据sheetcontact_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 = Truefor contact_row_data in contact_all_data[1:]:if contact_row_data[CONTACT_IS_EXECUTE_COL].lower() == "n":continuecontact_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 = Falseexcel.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 osclass Excel:def __init__(self, test_data_file_path):# 文件格式校验if not os.path.exists(test_data_file_path):print("Excel工具类初始化失败:【{}】文件不存在!".format(test_data_file_path))returnif 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)# 初始化默认sheetself.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名称切换sheetdef change_sheet(self, sheet_name):if sheet_name not in self.get_sheets():print("sheet切换失败:【{}】指定sheet名称不存在!".format(sheet_name))returnself.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))returnreturn [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))returnself.ws.append(data)# 添加字体颜色if font_color:if font_color in self.color_dict.keys():font_color = self.color_dict[font_color]# 需要设置的单元格长度应与数据长度一致,否则默认与之前行的长度一致count = 0for 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 = 0for cell in self.ws[self.max_row_num()]:if count > len(data) - 1:breakif cell:cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)count += 1# 添加单元格边框if border:bd = Side(style="thin", color="000000")count = 0for cell in self.ws[self.max_row_num()]:if count > len(data) - 1:breakif 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 configparserclass 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_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}年{}月{}日".format(year, month, day)# 返回英文格式的日期:xxxx/xx/xx
def get_english_date():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}/{}/{}".format(year, month, day)# 返回中文格式的时间:xx时xx分xx秒
def get_chinese_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}时{}分{}秒".format(hour, minute, second)# 返回英文格式的时间:xx:xx:xx
def get_english_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif 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_pathexcept: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 = 11if __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()

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/206975.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java多线程-第20章

Java多线程-第20章 1.创建线程 Java是一种支持多线程编程的编程语言。多线程是指在同一程序中同时执行多个独立任务的能力。在Java中&#xff0c;线程是一种轻量级的子进程&#xff0c;它是程序中的最小执行单元。Java的多线程编程可以通过两种方式实现&#xff1a;继承Threa…

玩转大数据:3-Hadoop家族的力量与挑战

引言 Hadoop作为一个强大的大数据处理框架&#xff0c;以其分布式计算和存储能力在业界备受关注。然而&#xff0c;Hadoop在应用场景、适用范围、社区支持以及后续持续发展等方面也面临着一些挑战。本文将围绕Hadoop的生态应用&#xff0c;以及来自其他生态的挑战&#xff0c;…

传统算法: Pygame 实现快速排序

使用 Pygame 模块实现了快速排序的动画演示。首先,它生成一个包含随机整数的数组,并通过 Pygame 在屏幕上绘制这个数组的条形图。接着,通过快速排序算法对数组进行排序,动画效果可视化每一步的排序过程。在排序的过程中,程序选择一个基准元素(pivot),将数组分成两部分,…

【Spring MVC】Filter 过滤器异常处理 HandlerExceptionResolver 分析

文章目录 前言版本说明测试 Demo1、自定义过滤器 DemoFilter2、自定义业务异常 ServiceException3、自定义异常处理类 DemoExceptionHandler4、DemoController5、请求测试 问题分析1、日志打印记录2、Debug 方法 解决方案1、修改自定义过滤器2、请求测试 解决方案分析1、日志打…

springmvc(基础学习整合)

SpringMVC是Spring框架提供的构建Web应用程序的全功能MVC模块。 在SpringMVC的各个组件中&#xff0c;处理器映射器、处理器适配器、视图解析器称为SpringMVC的三大组件。 springMVC基本介绍&#xff1a; http://t.csdnimg.cn/TOzw9 MVC是一种设计思想&#xff0c;将一个应…

键盘打字盲打练习系列之刻意练习——1

一.欢迎来到我的酒馆 盲打&#xff0c;刻意练习! 目录 一.欢迎来到我的酒馆二.选择一款工具三.刻意练习第一步&#xff1a;基准键位练习第二步&#xff1a;字母键位练习第三步&#xff1a;数字符号键位练习 四.矫正坐姿 二.选择一款工具 工欲善其事必先利其器。在开始之前&…

【开源】基于Vue.js的医院门诊预约挂号系统的设计和实现

项目编号&#xff1a; S 033 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S033&#xff0c;文末获取源码。} 项目编号&#xff1a;S033&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 功能性需求2.1.1 数据中心模块2.1.2…

java-Swing界面简析

一、简析&#xff1a; 调用java提供的 java.swing包下的各种类可以实现界面中的各种组件(比如输入框、密码框按钮、单选框、复选框等) 二、java.swing包的关键类&#xff1a; 顶层容器&#xff1a;Jframe(窗口) 中间容器&#xff1a;Jpanel(面板) 基本控件&#xff1a; I…

asla四大开源组件应用示例(alsa-lib、alsa-utils、alsa-tools、alsa-plugins)

文章目录 alsa设备文件/dev/snd//sys/class/sound/proc/asoundalsa-lib示例1alsa-utilsalsa-toolsalsa-plugins参考alsa设备文件 /dev/snd/ alsa设备文件目录位于,/dev/snd,如下所示 root@xboard:~#ls /dev/snd -l total 0 drwxr-xr-x 2 root root 60 Nov 6 2023 …

《合成孔径雷达成像算法与实现》_使用CS算法对RADARSAT-1数据进行成像

CSA 简介&#xff1a;Chirp Scaling 算法 (简称 CS 算法&#xff0c;即 CSA) 避免了 RCMC 中的插值操作。该算法基于 Scaling 原理&#xff0c;通过对 chirp 信号进行频率调制&#xff0c;实现了对信号的尺度变换或平移。基于这种原理&#xff0c;可以通过相位相乘代替时域插值…

redis相关题

1 什么是Redis Redis(Remote Dictionary Server) 是⼀个使⽤ C 语⾔编写的&#xff0c;开源的&#xff08;BSD许可&#xff09;⾼性能⾮关系型&#xff08;NoSQL&#xff09;的键值对数据库。Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串&#xff0c;…

代理模式 1、静态代理 2、动态代理 jdk自带动态代理 3、Cglib代理

文章目录 代理模式1、静态代理2、动态代理jdk自带动态代理 3、Cglib代理 来和大家聊聊代理模式 代理模式 代理模式&#xff1a;即通过代理对象访问目标对象&#xff0c;实现目标对象的方法。这样做的好处是&#xff1a;可以在目标对象实现的基础上&#xff0c;增强额外的功能操…

探索接口测试:SOAP、RestFul规则、JMeter及市面上的接口测试工具

引言 在当今软件开发领域&#xff0c;接口测试扮演着至关重要的角色。随着系统变得日益复杂和互联&#xff0c;对于内部和外部接口的测试变得愈发关键。接口测试不仅仅是验证接口的正确性&#xff0c;更是确保系统的稳定性、安全性和性能优越性的关键一环。 本篇博客将带您深入…

【Linux】进程间通信——system V共享内存、共享内存的概念、共享内存函数、system V消息队列、信号量

文章目录 进程间通信1.system V共享内存1.1共享内存原理1.2共享内存数据结构1.3共享内存函数 2.system V消息队列2.1消息队列原理 3.system V信号量3.1信号量原理3.2进程互斥 4.共享内存的使用示例 进程间通信 1.system V共享内存 1.1共享内存原理 共享内存区是最快的IPC形式…

【多传感器融合】BEVFusion: 激光雷达和视觉融合框架 NeurIPS 2022

前言 BEVFusion其实有两篇&#xff0c; 【1】BEVFusion: A Simple and Robust LiDAR-Camera Fusion Framework. NeurIPS 2022 | 北大&阿里提出 【2】BEVFusion: Multi-Task Multi-Sensor Fusion with Unified Bird’s-Eye View Representation 2022 | MIT提出 本文先分…

Flutter桌面应用开发之毛玻璃效果

目录 效果实现方案依赖库支持平台实现步骤注意事项话题扩展 毛玻璃效果&#xff1a;毛玻璃效果是一种模糊化的视觉效果&#xff0c;常用于图像处理和界面设计中。它可以通过在图像或界面元素上应用高斯模糊来实现。使用毛玻璃效果可以增加图像或界面元素的柔和感&#xff0c;同…

Word 小知识之 docx 和 doc 的区别

下面我们从4个方面为大家总结了有关于docx和doc的区别&#xff0c;一起来看一看&#xff1a; 1. 文件格式 doc和docx的区别中较大的区别就是文件格式不同&#xff0c;一个是二进制一个为XML格式。doc&#xff1a;是早期的Word文档格式&#xff0c;采用二进制文件格式。这种…

Android Studio Giraffe版本遇到的问题

背景 上周固态硬盘挂了&#xff0c;恢复数据之后&#xff0c;重新换了新的固态安装了Win11系统&#xff0c;之前安装的是Android Studio 4.x的版本&#xff0c;这次也是趁着新的系统安装新的Android开发工具。 版本如下&#xff1a; 但是打开以前的Android旧项目时&#xff…

Windows本地搭建Emby媒体库服务器并实现远程访问「内网穿透」

文章目录 1.前言2. Emby网站搭建2.1. Emby下载和安装2.2 Emby网页测试 3. 本地网页发布3.1 注册并安装cpolar内网穿透3.2 Cpolar云端设置3.3 Cpolar内网穿透本地设置 4.公网访问测试5.结语 1.前言 在现代五花八门的网络应用场景中&#xff0c;观看视频绝对是主力应用场景之一&…

【长文干货】Python可视化教程

文章目录 数据介绍Matplotlib散点图折线图柱形图直方图 Seaborn散点图折线图柱形图直方图 Bokeh散点图折线条形图交互式 Plotly基本组合优化&#xff1a;定制化下拉菜单 总结 数据介绍 在这个小费数据集中&#xff0c;我们记录了20世纪90年代初期餐厅顾客在两个半月内给出的小…