UI自动化测试框架:PO模式+数据驱动
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快
1. PO 设计模式简介
什么是 PO 模式?
PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Page 类,并以页面为单位来写测试用例,实现页面对象和测试用例的分离。
PO 模式的设计思想与面向对象相似,能让测试代码变得可读性更好,可维护性高,复用性高。
PO 模式可以把一个页面分为三个层级:对象库层、操作层、业务层。
- 对象库层:封装定位元素的方法。
- 操作层:封装对元素的操作。
- 业务层:将一个或多个操作组合起来完成一个业务功能。
一条测试用例可能需要多个步骤操作元素,将每一个步骤单独封装成一个方法,在执行测试用例时调用封装好的方法进行操作。
PO 模式的优点
- 通过页面分层,将测试代码和被测试页面的页面元素及其操作方法进行分离,降低代码冗余。
- 页面对象与用例分离,业务代码与测试代码分离,降低耦合性。
- 不同层级分属不同用途,降低维护成本。
- 代码可阅读性增强,整体流程更为清晰。
2. 工程结构简介
工程结构
整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。
第一层是“测试工具层”:
- util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作Excel文件等。
- conf 包:配置文件及全局变量。
- test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。
- log 目录:日志输出文件。
- screenshot_path 目录:异常截图保存目录。
第二层是“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于页面测试,是对页面元素或操作的一个封装。
- page 包:对象库层及操作层,将所有页面的元素对象定位及其操作分别封装成一个类。
第三层是“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。
- action 包:组装单个用例的流程。
- business_process 包:基于业务层和测试数据文件,执行测试用例集合。
- test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。
- 第四层是“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理、冒烟,回归等测试场景。
- main.py:本 PO 框架的运行主入口。
框架特点
- 通过配置文件,实现页面元素定位方式和测试代码的分离。
- 使用 PO 模式,封装了网页中的页面元素,方便测试代码调用,也实现了一处维护全局生效的目标。
- 在 excel 文件中定义多组测试数据,每个登录用户都一一对应一个存放联系人数据的 sheet,测试框架可自动调用测试数据完成数据驱动测试。
- 实现了测试执行过程中的日志记录功能,可以通过日志文件分析测试脚本执行的情况。
- 在 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 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日志")
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()
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。
相关文章:
UI自动化测试框架:PO模式+数据驱动
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 1. PO 设计模式简介 什么是 PO 模式? PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Pa…...
第四十六篇 Vision Transformer论文翻译
论文连接:https://arxiv.org/abs/2010.11929 GitHub:https://github.com/google-research/vision_transformer 摘要 虽然Transformer架构已成为自然语言处理任务的实际标准,但其在计算机视觉中的应用仍然有限。在计算机视觉中,注意力机制要么与卷积网络结合使用,要么在保…...
如何在Ubuntu中利用repo和git地址下载获取imx6ull的BSP
01-设置git的用户名和邮箱 git config --global user.name "suwenhao" git config --global user.email "2487872782qq.com"这里不设置的话后面在第5步的repo配置中还是会要求输入,而且以后进行相关操作都要输入,不妨现在就进行配置…...
redis数据结构和内部编码及单线程架构
博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:数据库 JavaEE专栏:JavaEE 软件测试专栏:软件测试 关注博主带你了解更多知识 1. 数据结构和内部编码 Redis会在合适的场景选择合适的内部编码 我们可以通过objectencoding命令查询内部编码 : 2. 单线程架构 …...
2412d,d的6月会议
信息:gtkD的文档位置 原文 总结 DMDARM后端 Razvan问Walter他对DMD的ARM后端的分发,并想知道他是否考虑过其他选择,如整合DMD前端与LDC后端. Walter说,人们写信告诉他,他们喜欢使用DMD,因为它体积小,速度快.多年来,就要求他实现ARM后端. 有的人想写一个,但后来因为太难或太耗…...
提升网站流量的关键:AI在SEO关键词优化中的应用
内容概要 在当今数字时代,提升网站流量已成为每个网站管理员的首要任务。而人工智能的技术进步,为搜索引擎优化(SEO)提供了强有力的支持,尤其是在关键词优化方面。关键词是连接用户需求与网站内容的桥梁,其…...
【模型对比】ChatGPT vs Kimi vs 文心一言那个更好用?数据详细解析,找出最适合你的AI辅助工具!
在这个人工智能迅猛发展的时代,AI聊天助手已经深入我们的工作与生活。你是否曾在选择使用ChatGPT、Kimi或是百度的文心一言时感到一头雾水?每款AI都有其独特的魅力与优势,那么,究竟哪一款AI聊天助手最适合你呢?本文将带…...
利润表在Zebra BI 中的应用(一)
效果如图。本案例采用极简式对比 需要注意的是:如原始数据是一维的,则需要确保比较的各年份所含项目一致,缺失的也要占位,否则会出错! 2022% of Revenue(占收入%) DIVIDE( [值_2022], CALCULA…...
12.09 C++作业2
利用函数重载,实现对整形数组的冒泡排序,对浮点型数组的冒泡排序 #include <iostream>using namespace std;int maopao(int(&ra)[10]) {//求数组长度int len sizeof(ra)/sizeof(ra[0]);int i,j,t;for(int i0;i<len;i){cin >>ra[i];}…...
MongoDB性能监控工具
mongostat mongostat是MongoDB自带的监控工具,其可以提供数据库节点或者整个集群当前的状态视图。该功能的设计非常类似于Linux系统中的vmstat命令,可以呈现出实时的状态变化。不同的是,mongostat所监视的对象是数据库进程。mongostat常用于…...
如何防范顶级应用程序安全威胁
如今的网络攻击数量是五年前的两倍多。因此,掌握最新的应用程序安全威胁对于防止数据泄露、经济损失和声誉受损至关重要。您还需要实施强大的安全实践,以保护应用程序免受最常见和最危险的威胁。 顶级应用程序安全威胁......以及如何防范这些威胁 1. 注…...
Java版-图论-拓扑排序与有向无环图
拓扑排序 拓扑排序说明 对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列…...
汽车一键启动开关 、一键启动按键 、一键启动按钮
汽车一键启动按钮是智能汽车的重要部分,通常用于启动和关闭引擎。 具体功能: 启动引擎:在许多现代汽车中,一键启动按键取代了传统的钥匙启动方式。只需轻轻按下一键启动按钮,车辆电源即被接通,…...
SWIRL:有望成为2025年顶级AI搜索引擎
现在几乎每家公司都会有内部文档系统,如阿里的语雀、钉钉,字节的飞书,Confluence,印象笔记等等都可以提供给B端在局域网部署。因此,如果能把搜索功能做得高效,就能提高自家产品的竞争力。 想象一下…...
QT requested database does not belong to the calling thread.线程中查询数据报错
QT requested database does not belong to the calling thread.线程中查询数据报错 QString name "ttx"; QSqlQueryModel* sql_model; QString sql_comm QString("select * from dssb_moddve_loddt_tab where name%1").arg(name); sql_model->set…...
活着就好20241210
亲爱的朋友们,大家早上好!🌞 今天是10号,星期二,2024年12月的第十天,同时也是第50周的开始,农历甲辰[龙]年十一月初六日。在这晨光熹微的美好时刻,愿那温暖而明媚的阳光轻轻拂过你的…...
Flutter动画(二)内建隐式动画Widget
动画效果介绍中给出了选择动画的决策树: 使用动画框架不在我们讨论的话题内。flutter支持的动画包括隐式动画和显式动画。 隐式动画和显式动画 隐式动画和显示动画是两种不同的动画实现方式,它们的主要区别在于控制权和动画的重复性。 隐式动画&#…...
【人工智能基础08】卷积神经网络习题:卷积神经网络计算、图像填充、卷积的表达与设计
文章目录 1. 卷积核计算2. 卷积神经网络计算3. 卷积核关注的特征问题解答水平边缘检测与水平条纹检测45度条纹检测 4. 图像检测5. 卷积网络是特殊的全连接网络6. 输出矩阵的三种填充方法7. 卷积设计8.9 成像公式10. 卷积的计算次数11. 全连接层的计算 1. 卷积核计算 卷积操作过…...
前端-使用vue-cli脚手架创建项目
1.下载node:2.下载完检查是否安装成功 在cmd中输入:node --version或node -v 再在cmd中输入: npm -v npm默认的仓库地址是在国外,速度慢,所以设置淘宝镜像,速度就提升了,不设淘宝镜像也可以。 3.设置…...
功能篇:JAVA实现自定义注解
在Java中创建自定义注解可以通过使用interface关键字来完成。自定义注解可以包含元素(即参数),并且你可以指定这些元素的默认值、保留策略以及应用的目标。以下是实现自定义注解的基本步骤和示例代码。 ### 自定义注解的组成部分 1. **元素…...
调度系统:Temporal 在大数据领域的局限分析
在大数据领域的任务管理中,Temporal 和 Apache Airflow 各有优劣。要选择更适合的工具,需根据具体需求(如任务复杂性、依赖管理、分布式能力等)权衡。 以下是两者的比较及 Temporal 在大数据领域的局限分析: Tempora…...
保姆级教学 uniapp绘制二维码海报并保存至相册,真机正常展示图片二维码
一、获取二维码 uni.request({url: https://api.weixin.qq.com/wxa/getwxacode?access_token${getStorage("token")},responseType: "arraybuffer",method: "POST",data: {path: "/pages/index/index"},success(res) {// 转换为 Uint…...
不是“我应该做什么”,而是“我想做什么”
1. 识别内心的渴望 首先,我们需要识别自己真正的愿望和激情所在。这可能需要一些时间和自我反思。问自己:在没有任何外界压力的情况下,我真正想做的是什么?是赚钱、生活、旅行、追星,还是其他什么?识别这些…...
【openwrt】openwrt-21.02 基于IP地址使用ipset实现策略路由操作说明
openwrt版本信息 DISTRIB_ID=OpenWrt DISTRIB_RELEASE=21.02-SNAPSHOT DISTRIB_REVISION=r0-6bf6af1d5 DISTRIB_TARGET=mediatek/mt7981 DISTRIB_ARCH=aarch64_cortex-a53 DISTRIB_DESCRIPTION=OpenWrt 21.02-SNAPSHOT r0-6bf6af1d5 DISTRIB_TAINTS=no-all busybox override …...
Linux内核Kernel启动过程
一、内核启动的基本流程 1. 启动加载程序 (Bootloader) 启动加载程序(如GRUB、LILO、syslinux等)负责将内核映像从存储设备加载到内存中,并准备好内核启动所需的环境。 加载内核映像:启动加载程序将压缩的内核映像(如…...
苍穹外卖复习(持续更新)
文章目录 苍穹外卖复习Day01Day02(新增员工) 苍穹外卖复习 Day01 Day02(新增员工)...
子网划分实例
看到有人问这个问题: 想了一下,这是一个子网划分的问题: 处理方法如图: 这是一个子网划分的问题 设备1用三层交换机,端口设置为路由模式,设备2和设备3为傻瓜交换机模式 设备2和设备3下挂设备都是26为掩码&…...
二进制部署Prometheus+grafana+alertmanager+node_exporter
Prometheus 是一个开源的监控和告警工具包,旨在提供高可靠性和可扩展性。它最初由 SoundCloud 开发,现已成为云原生计算基金会(CNCF)的一部分。以下是 Prometheus 的一些关键特性和概念: 1. **时间序列数据库**&#…...
数据结构——图(遍历,最小生成树,最短路径)
目录 一.图的基本概念 二.图的存储结构 1.邻接矩阵 2.邻接表 三.图的遍历 1.图的广度优先遍历 2.图的深度优先遍历 四.最小生成树 1.Kruskal算法 2.Prim算法 五.最短路径 1.单源最短路径--Dijkstra算法 2.单源最短路径--Bellman-Ford算法 3.多源最短路径--Floyd-…...
Android APK打包流程
文章目录 前言1. 资源打包(通过 AAPT)2. 处理 AIDL 文件3. Java 源代码编译4. Dex 转换5. APK 打包6. APK 签名7. APK 对齐总结 前言 Android APK 打包过程包括多个关键步骤,每个环节都提供了不同的操作机会。开发者可以在这些步骤中进行自定…...
《Liunx系统》之基础操作命令
目录 简介 1. 文件和目录操作 1.1 查看当前目录 1.2 列出目录内容 1.3 切换目录 1.4 创建目录 1.5 删除目录 1.6 创建文件 1.7 删除文件 2. 文件内容操作 2.1 查看文件内容 2.2 搜索文件内容 2.3 复制文件 2.4 移动文件 3. 系统信息和进程管理 3.1 查看系统信息…...
linux 编译、交叉编译 opencv+ffmpeg 为动态库
文章目录 x86 编译先编译 ffmpeg再编译 opencv验证 opencv 的安装是否链接了 ffmpeg 交叉编译(目标系统 armv8 即 arrch64)准备交叉编译工具链(arm 版 gcc、g)先编译 ffmpeg再编译 opencv验证 opencv 的安装是否链接了 ffmpeg 参考…...
Robust Univariate Mean Estimation算法简介
Robust Univariate Mean Estimation 是一种统计算法,主要用于在单变量场景中估计样本的均值,同时对异常值(outliers)具有鲁棒性。传统的均值估计使用样本的算术平均值,但它对异常值高度敏感。为了缓解这个问题…...
区块链智能合约( solidity) 安全编程
引言:本文由天玄链开源开发者提供,欢迎报名公益天玄链训练营 https://blockchain.163.com/trainingCamp 一、重入和竞态 重入和竞态在solidity 编程安全中会多次提及,历史上也造成了重大的损失。 1.1 问题分析 竞态的描述不严格…...
断点续传【授权访问】
本文介绍如何使用STS以及签名URL临时授权访问OSS资源。 授权方式 OSS支持多种授权方式进行客户端授权,以下提供了三种不同授权方式的简要说明,并提供了使用相应授权方式实现简单上传的代码示例,您可以根据使用场景对认证和授权的要求&#…...
中华国际游艇会出席第六届地博会助世界酒中国菜地理标志走向全球
中华国际游艇会积极参与第六届知交会暨地博会,助力世界酒中国菜地理标志产品走向全球 第六届粤港澳大湾区知识产权交易博览会暨国际地理标志产品交易博览会于2024年12月9日至11日在中新广州知识城盛大举行。此次盛会汇聚了众多行业精英和企业代表,共同探…...
Python爬虫——HTML中Xpath定位
Xpath是一种路径查询语言。利用一个路径表达式从html文档中找到我们需要的数据位置,进而将其写入到本地或者数据库中。 学习Xpath爬虫,我们首先学习一下python中lxml库 关于库 lxml 终端下载Xpath需要用到的模块 pip install lxml 关于HTML 超文本标…...
Ubuntu防火墙管理(六)——ARP防火墙过滤防御自定义系统服务
起因 在ubuntu24.04中检查arp表,输出异常 arp -a? (10.162.242.142) 位于 74:3a:20:b9:e8:02 [ether] 在 wlp2s0 ? (10.162.0.1) 位于 在 wlp2s0 ubuntu环境中,这是否表示ARP攻击,本地网关为10.162.0.1,可用arptables防御吗&a…...
Halcon_数据类型_ROI_仿射变换_投影变换
文章目录 算子快捷键一、Halcon数据类型Iconic (图标)Control (控制)Tuple (数组) 二、ROI(区域)1.代码创建ROI2.手动创建ROI 三、图形预处理1.图像的变换与矫正平移 -hom_mat2d_translate旋转缩放-HomMat2D:输入的仿射…...
java+ssm+mysql高校学籍管理系统
项目介绍: 使用javassmmysql开发的高校学籍管理系统,系统包含超级管理员,系统管理员、教师、学生角色,功能如下: 超级管理员:管理员管理(可以新增管理员);专业管理&…...
多表设计-一对多一对多-外键
一.多表设计概述: 二.一对多: 1.需求: 根据 页面原型 及 需求文档,完成部门及员工模块的表结构设计 -->部门和员工就是一对多,因为一个部门下会有多个员工,但一个员工只归属一个部门 2.页面原型&…...
Scala的正则表达式(1)
package hfd //正则表达式的应用场景 //1.查找 findAllin //2.验证 matches //3.替换//验证用户名十分合法 //规则: //1.长度在6-12之间 //2.不能数字开头 //3.只能包含数字,大小写字母,下划线 object Test36 {def main(args: Array[String])…...
uniapp扭蛋机组件
做了一个uniapp的扭蛋机组件,可以前往下载地址下载 仅测试了vue2、3、h5页面微信小程序,理论支持全平台 使用方法简单,具有待机动效、抽奖中动效、掉落奖品动效,可以替换奖品图片,足以满足大部分抽奖页面需求。 示例图…...
并发背后的技术与原理
一个Java Web项目能够同时支持多个用户请求,而不会导致数据混乱,这主要得益于Java平台的多线程处理机制、Web容器的请求处理模型以及良好的编程实践。 Java的多线程处理: Java是一种支持多线程的编程语言。在Java Web应用中,每个用…...
HarmonyOS(63) ArkUI 自定义占位组件NodeContainer
NodeContainer 1、前言2、NodeContainer和NodeController3、示例代码3.1、创建@Builder3.2、 创建NodeController3.3、 使用NodeCtroller4、NodeContainer的作用5、FrameNode简介6、BuilderNode简介7、参考资料1、前言 在HarmonyOS(62) ArkUI @Reusable组件复用原理讲了组件复…...
机器学习实战学习笔记:前言与准备
个人学习介绍 该专栏作为《机器学习实战(原书第三版)》的读书笔记,涉及对书本内容的理解和书本内容原有的示例和部分原文。略懂一点点python语法编程环境选用python 3.12.1 ,IDE为 DataSpell (主要)&#…...
Linux应用开发————多线程的互斥与同步——同步
同步和互斥 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。 条件变量机制…...
【人工智能】深度解剖利用人工智能MSA模型
目录 情感分析的应用一、概述二、研究背景三、主要贡献四、模型结构和代码五、数据集介绍六、性能展示七、复现过程 情感分析的应用 近年来社交媒体的空前发展以及配备高质量摄像头的智能手机的出现,我们见证了多模态数据的爆炸性增长,如电影、短视频等…...
Java 环境配置 + IntelliJ IDEA 使用指南
文章目录 一、Java 程序的运行必须经过3 个步骤:编写、编译、运行(1)Java 和 JavaScript 的区别(2)JDK、JRE、JVM 的关系(3)是否需要 Maven? 二、软件下载2.1、JDK下载与安装 —— 是…...
兴业周报|楼市新政效果显著~单周成交破5亿~
香山清琴山庄丙19号(独栋别墅) 稀缺房源:标的物为京城少有的独栋别墅,在连续的 “限墅令” 及相关容积L限制政策下,市场上独栋别墅的新增供应不断减少。 环境优美:香山清琴山庄依山而筑,错落有…...