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 框架的运行主入口。

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

同时,我也为大家准备了一份软件测试视频教程(含面试、接口、自动化、性能测试等),就在下方,需要的可以直接去观看,也可以直接点击文末小卡片免费领取资料文档

软件测试视频教程观看处:

2024年Python自动化测试全套保姆级教程,70个项目实战,3天练完,永久白嫖...

3. 工程代码示例

page 包

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

login_page.py

 
  1. from conf.global_var import *

  2. from util.ini_parser import IniParser

  3. from util.find_element_util import *

  4. # 登录页面元素定位及操作

  5. class LoginPage:

  6. def __init__(self, driver):

  7. self.driver = driver

  8. # 初始化跳转登录页面

  9. self.driver.get(LOGIN_URL)

  10. # 初始化指定ini配置文件及指定分组

  11. self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_loginPage")

  12. # 获取frame元素对象

  13. def get_frame_obj(self):

  14. locate_method, locate_exp = self.cf.get_value("loginPage.frame").split(">")

  15. return find_element(self.driver, locate_method, locate_exp)

  16. # 切换frame

  17. def switch_frame(self):

  18. self.driver.switch_to.frame(self.get_frame_obj())

  19. # 获取用户名输入框元素对象

  20. def get_username_input_obj(self):

  21. locate_method, locate_exp = self.cf.get_value("loginPage.username").split(">")

  22. return find_element(self.driver, locate_method, locate_exp)

  23. # 清空用户名输入框操作

  24. def clear_username(self):

  25. self.get_username_input_obj().clear()

  26. # 输入用户名操作

  27. def input_username(self, value):

  28. self.get_username_input_obj().send_keys(value)

  29. # 获取密码输入框元素对象

  30. def get_pwd_input_obj(self):

  31. locate_method, locate_exp = self.cf.get_value("loginPage.password").split(">")

  32. return find_element(self.driver, locate_method, locate_exp)

  33. # 输入密码操作

  34. def input_pwd(self, value):

  35. self.get_pwd_input_obj().send_keys(value)

  36. # 获取登录按钮对象

  37. def get_login_buttion_obj(self):

  38. locate_method, locate_exp = self.cf.get_value("loginPage.loginbutton").split(">")

  39. return find_element(self.driver, locate_method, locate_exp)

  40. # 点击登录按钮操作

  41. def click_login_button(self):

  42. self.get_login_buttion_obj().click()

home_page.py

 
  1. from conf.global_var import *

  2. from util.ini_parser import IniParser

  3. from util.find_element_util import *

  4. # 登录后主页元素定位及操作

  5. class HomePage:

  6. def __init__(self, driver):

  7. self.driver = driver

  8. # 初始化指定ini配置文件及指定分组

  9. self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_homePage")

  10. # 获取“通讯录”按钮对象

  11. def get_contact_button_obj(self):

  12. locate_method, locate_exp = self.cf.get_value("homePage.addressLink").split(">")

  13. return find_element(self.driver, locate_method, locate_exp)

  14. # 点击“通讯录”按钮

  15. def click_contact_button(self):

  16. self.get_contact_button_obj().click()

contact_page.py

 
  1. from conf.global_var import *

  2. from util.ini_parser import IniParser

  3. from util.find_element_util import *

  4. # 通讯录页面元素定位及操作

  5. class ContactPage:

  6. def __init__(self, driver):

  7. self.driver = driver

  8. # 初始化指定ini配置文件及指定分组

  9. self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_contactPersonPage")

  10. # 获取新建联系人按钮对象

  11. def get_contact_create_button_obj(self):

  12. locate_method, locate_exp = self.cf.get_value("contactPersonPage.createButton").split(">")

  13. return find_element(self.driver, locate_method, locate_exp)

  14. # 点击新建联系人按钮

  15. def click_contact_creat_button(self):

  16. self.get_contact_create_button_obj().click()

  17. # 获取姓名输入框对象

  18. def get_name_input_obj(self):

  19. locate_method, locate_exp = self.cf.get_value("contactPersonPage.name").split(">")

  20. return find_element(self.driver, locate_method, locate_exp)

  21. # 输入姓名操作

  22. def input_name(self, value):

  23. self.get_name_input_obj().send_keys(value)

  24. # 获取邮箱输入框对象

  25. def get_email_input_obj(self):

  26. locate_method, locate_exp = self.cf.get_value("contactPersonPage.email").split(">")

  27. return find_element(self.driver, locate_method, locate_exp)

  28. # 输入邮箱操作

  29. def input_email(self, value):

  30. self.get_email_input_obj().send_keys(value)

  31. # 获取星标联系人单选框对象

  32. def get_star_button_obj(self):

  33. locate_method, locate_exp = self.cf.get_value("contactPersonPage.starContacts").split(">")

  34. return find_element(self.driver, locate_method, locate_exp)

  35. # 点击星标联系人操作

  36. def click_star_button(self):

  37. self.get_star_button_obj().click()

  38. # 获取手机输入框对象

  39. def get_phone_input_obj(self):

  40. locate_method, locate_exp = self.cf.get_value("contactPersonPage.phone").split(">")

  41. return find_element(self.driver, locate_method, locate_exp)

  42. # 输入邮箱操作

  43. def input_phone(self, value):

  44. self.get_phone_input_obj().send_keys(value)

  45. # 获取备注输入框对象

  46. def get_remark_input_obj(self):

  47. locate_method, locate_exp = self.cf.get_value("contactPersonPage.otherinfo").split(">")

  48. return find_element(self.driver, locate_method, locate_exp)

  49. # 输入邮箱操作

  50. def input_remark(self, value):

  51. self.get_remark_input_obj().send_keys(value)

  52. # 获取确定按钮对象

  53. def get_confirm_button_obj(self):

  54. locate_method, locate_exp = self.cf.get_value("contactPersonPage.confirmButton").split(">")

  55. return find_element(self.driver, locate_method, locate_exp)

  56. # 点击星标联系人操作

  57. def click_confirm_button(self):

  58. self.get_confirm_button_obj().click()

action 包

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

case_action.py

 
  1. from selenium import webdriver

  2. import traceback

  3. import time

  4. from page.contact_page import ContactPage

  5. from page.home_page import HomePage

  6. from page.login_page import LoginPage

  7. from conf.global_var import *

  8. from util.log_util import *

  9. # 初始化浏览器

  10. def init_browser(browser_name):

  11. if browser_name.lower() == "chrome":

  12. driver = webdriver.Chrome(CHROME_DRIVER)

  13. elif browser_name.lower() == "firefox":

  14. driver = webdriver.Firefox(FIREFOX_DRIVER)

  15. elif browser_name.lower() == "ie":

  16. driver = webdriver.Ie(IE_DRIVER)

  17. else:

  18. return "Error browser name!"

  19. return driver

  20. def assert_word(driver, text):

  21. assert text in driver.page_source

  22. # 登录流程封装

  23. def login(driver, username, pwd, assert_text):

  24. login_page = LoginPage(driver)

  25. login_page.switch_frame()

  26. login_page.clear_username()

  27. login_page.input_username(username)

  28. login_page.input_pwd(pwd)

  29. login_page.click_login_button()

  30. time.sleep(1)

  31. assert_word(driver, assert_text)

  32. # 添加联系人流程封装

  33. def add_contact(driver, name, email, phone, is_star, remark, assert_text):

  34. home_page = HomePage(driver)

  35. home_page.click_contact_button()

  36. contact_page = ContactPage(driver)

  37. contact_page.click_contact_creat_button()

  38. contact_page.input_name(name)

  39. contact_page.input_email(email)

  40. contact_page.input_phone(phone)

  41. contact_page.input_remark(remark)

  42. if is_star == "是":

  43. contact_page.click_star_button()

  44. contact_page.click_confirm_button()

  45. time.sleep(2)

  46. assert_word(driver, assert_text)

  47. def quit(driver):

  48. driver.quit()

  49. if __name__ == "__main__":

  50. driver = init_browser("chrome")

  51. login(driver, "zhangjun252950418", "zhangjun123", "退出")

  52. add_contact(driver, "铁蛋", "asfhi@123.com", "12222222222", "是", "这是备注", "铁蛋")

  53. # quit(driver)

business_process 包

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

batch_login_process.py

 
  1. from action.case_action import *

  2. from util.excel_util import *

  3. from conf.global_var import *

  4. from util.datetime_util import *

  5. from util.screenshot import take_screenshot

  6. # 封装测试数据文件中用例的执行逻辑

  7. # 测试数据文件中的每个登录账号

  8. def batch_login(test_data_file, browser_name, account_sheet_name):

  9. excel = Excel(test_data_file)

  10. # 获取登录账号sheet页数据

  11. excel.change_sheet(account_sheet_name)

  12. account_all_data = excel.get_all_row_data()

  13. account_headline_data = account_all_data[0]

  14. for account_row_data in account_all_data[1:]:

  15. # 执行登录用例

  16. account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()

  17. if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":

  18. continue

  19. # 初始化浏览器

  20. driver = init_browser(browser_name)

  21. try:

  22. # 默认以"退出"作为断言关键字

  23. login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")

  24. info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],

  25. account_row_data[ACCOUNT_PWD_COL], "退出"))

  26. account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"

  27. except:

  28. error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],

  29. account_row_data[ACCOUNT_PWD_COL], "退出"))

  30. account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"

  31. account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()

  32. account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)

  33. # 写入登录用例的测试结果

  34. excel.change_sheet("测试结果")

  35. excel.write_row_data(account_headline_data, "red")

  36. excel.write_row_data(account_row_data)

  37. excel.save()

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

  39. driver.quit()

  40. if __name__ == "__main__":

  41. batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")

batch_login_and_add_contact_process.py

 
  1. from action.case_action import *

  2. from util.excel_util import *

  3. from conf.global_var import *

  4. from util.datetime_util import *

  5. from util.screenshot import take_screenshot

  6. # 封装测试数据文件中用例的执行逻辑

  7. # 测试数据文件中每个登录账号下,添加所有联系人数据

  8. def batch_login_and_add_contact(test_data_file, browser_name, account_sheet_name):

  9. excel = Excel(test_data_file)

  10. # 获取登录账号sheet页数据

  11. excel.change_sheet(account_sheet_name)

  12. account_all_data = excel.get_all_row_data()

  13. account_headline_data = account_all_data[0]

  14. for account_row_data in account_all_data[1:]:

  15. # 执行登录用例

  16. account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()

  17. if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":

  18. continue

  19. # 初始化浏览器

  20. driver = init_browser(browser_name)

  21. # 获取联系人数据sheet

  22. contact_data_sheet = account_row_data[ACCOUNT_DATA_SHEET_COL]

  23. try:

  24. # 默认以"退出"作为断言关键字

  25. login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")

  26. info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],

  27. account_row_data[ACCOUNT_PWD_COL], "退出"))

  28. account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"

  29. except:

  30. error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],

  31. account_row_data[ACCOUNT_PWD_COL], "退出"))

  32. account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"

  33. account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()

  34. account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)

  35. # 写入登录用例的测试结果

  36. excel.change_sheet("测试结果")

  37. excel.write_row_data(account_headline_data, "red")

  38. excel.write_row_data(account_row_data)

  39. excel.save()

  40. # 执行添加联系人用例

  41. excel.change_sheet(contact_data_sheet)

  42. contact_all_data = excel.get_all_row_data()

  43. contact_headline_data = contact_all_data[0]

  44. # 在测试结果中,一个账号下的联系人数据标题行仅写一次

  45. contact_headline_flag = True

  46. for contact_row_data in contact_all_data[1:]:

  47. if contact_row_data[CONTACT_IS_EXECUTE_COL].lower() == "n":

  48. continue

  49. contact_row_data[CONTACT_TEST_TIME_COL] = get_english_datetime()

  50. try:

  51. add_contact(driver, contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],

  52. contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],

  53. contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL])

  54. info("添加联系人成功【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, "

  55. "备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],

  56. contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],

  57. contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))

  58. contact_row_data[CONTACT_TEST_RESULT_COL] = "pass"

  59. except:

  60. error("添加联系人失败【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, "

  61. "备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],

  62. contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],

  63. contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))

  64. contact_row_data[CONTACT_TEST_RESULT_COL] = "fail"

  65. contact_row_data[CONTACT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()

  66. contact_row_data[CONTACT_SCREENSHOT_COL] = take_screenshot(driver)

  67. # 写入登录用例的测试结果

  68. excel.change_sheet("测试结果")

  69. if contact_headline_flag:

  70. excel.write_row_data(contact_headline_data, "red")

  71. contact_headline_flag = False

  72. excel.write_row_data(contact_row_data)

  73. excel.save()

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

  75. driver.quit()

  76. if __name__ == "__main__":

  77. batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")

util 包

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

excel_util.py
(openpyxl 版本:3.0.4)

 
  1. from openpyxl import load_workbook

  2. from openpyxl.styles import PatternFill, Font, Side, Border

  3. import os

  4. class Excel:

  5. def __init__(self, test_data_file_path):

  6. # 文件格式校验

  7. if not os.path.exists(test_data_file_path):

  8. print("Excel工具类初始化失败:【{}】文件不存在!".format(test_data_file_path))

  9. return

  10. if not test_data_file_path.endswith(".xlsx") or not test_data_file_path.endswith(".xlsx"):

  11. print("Excel工具类初始化失败:【{}】文件非excel文件类型!".format(test_data_file_path))

  12. return

  13. # 打开指定excel文件

  14. self.wb = load_workbook(test_data_file_path)

  15. # 初始化默认sheet

  16. self.ws = self.wb.active

  17. # 保存文件时使用的文件路径

  18. self.test_data_file_path = test_data_file_path

  19. # 初始化红、绿色,供样式使用

  20. self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}

  21. # 查看所有sheet名称

  22. def get_sheets(self):

  23. return self.wb.sheetnames

  24. # 根据sheet名称切换sheet

  25. def change_sheet(self, sheet_name):

  26. if sheet_name not in self.get_sheets():

  27. print("sheet切换失败:【{}】指定sheet名称不存在!".format(sheet_name))

  28. return

  29. self.ws = self.wb.get_sheet_by_name(sheet_name)

  30. # 返回当前sheet的最大行号

  31. def max_row_num(self):

  32. return self.ws.max_row

  33. # 返回当前sheet的最大列号

  34. def max_col_num(self):

  35. return self.ws.max_column

  36. # 获取指定行数据(设定索引从0开始)

  37. def get_one_row_data(self, row_no):

  38. if row_no < 0 or row_no > self.max_row_num()-1:

  39. print("输入的行号【{}】有误:需在0至最大行数之间!".format(row_no))

  40. return

  41. # API的索引从1开始

  42. return [cell.value for cell in self.ws[row_no+1]]

  43. # 获取指定列数据

  44. def get_one_col_data(self, col_no):

  45. if col_no < 0 or col_no > self.max_col_num()-1:

  46. print("输入的列号【{}】有误:需在0至最大列数之间!".format(col_no))

  47. return

  48. return [cell.value for cell in tuple(self.ws.columns)[col_no+1]]

  49. # 获取当前sheet的所有行数据

  50. def get_all_row_data(self):

  51. result = []

  52. # # API的索引从1开始

  53. for row_data in self.ws[1:self.max_row_num()]:

  54. result.append([cell.value if cell.value is not None else "" for cell in row_data])

  55. return result

  56. # 追加一行数据

  57. def write_row_data(self, data, fill_color=None, font_color=None, border=True):

  58. if not isinstance(data, (list, tuple)):

  59. print("追加的数据类型有误:需为列号或元组类型!【{}】".format(data))

  60. return

  61. self.ws.append(data)

  62. # 添加字体颜色

  63. if font_color:

  64. if font_color in self.color_dict.keys():

  65. font_color = self.color_dict[font_color]

  66. # 需要设置的单元格长度应与数据长度一致,否则默认与之前行的长度一致

  67. count = 0

  68. for cell in self.ws[self.max_row_num()]:

  69. if count > len(data) - 1:

  70. break

  71. # cell不为None,才能设置样式

  72. if cell:

  73. if cell.value in ["pass", "成功"]:

  74. cell.font = Font(color=self.color_dict["green"])

  75. elif cell.value in ["fail", "失败"]:

  76. cell.font = Font(color=self.color_dict["red"])

  77. else:

  78. cell.font = Font(color=font_color)

  79. count += 1

  80. # 添加背景颜色

  81. if fill_color:

  82. if fill_color in self.color_dict.keys():

  83. fill_color = self.color_dict[fill_color]

  84. count = 0

  85. for cell in self.ws[self.max_row_num()]:

  86. if count > len(data) - 1:

  87. break

  88. if cell:

  89. cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)

  90. count += 1

  91. # 添加单元格边框

  92. if border:

  93. bd = Side(style="thin", color="000000")

  94. count = 0

  95. for cell in self.ws[self.max_row_num()]:

  96. if count > len(data) - 1:

  97. break

  98. if cell:

  99. cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)

  100. count += 1

  101. # 保存文件

  102. def save(self):

  103. self.wb.save(self.test_data_file_path)

  104. if __name__ == "__main__":

  105. from conf.global_var import *

  106. excel = Excel(TEST_DATA_FILE_PATH)

  107. excel.change_sheet("登录1")

  108. # print(excel.get_all_row_data())

  109. excel.write_row_data((1,2,"嘻哈",None,"ddd"), "red", "green")

  110. excel.save()

find_element_util.py

 
  1. from selenium.webdriver.support.ui import WebDriverWait

  2. # 显式等待一个对象

  3. def find_element(driver, locate_method, locate_exp):

  4. # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)

  5. return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp))

  6. # 显式等待一组对象

  7. def find_elements(driver, locate_method, locate_exp):

  8. # 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)

  9. return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))

ini_parser.py

 
  1. import configparser

  2. class IniParser:

  3. # 初始化打开指定ini文件并指定编码

  4. def __init__(self, file_path, section):

  5. self.cf = configparser.ConfigParser()

  6. self.cf.read(file_path, encoding="utf-8")

  7. self.section = section

  8. # 获取所有分组名称

  9. def get_sections(self):

  10. return self.cf.sections()

  11. # 获取指定分组的所有键

  12. def get_options(self):

  13. return self.cf.options(self.section)

  14. # 获取指定分组的键值对

  15. def get_items(self):

  16. return self.cf.items(self.section)

  17. # 获取指定分组的指定键的值

  18. def get_value(self, key):

  19. return self.cf.get(self.section, key)

datetime_util.py

 
  1. import time

  2. # 返回中文格式的日期:xxxx年xx月xx日

  3. def get_chinese_date():

  4. year = time.localtime().tm_year

  5. if len(str(year)) == 1:

  6. year = "0" + str(year)

  7. month = time.localtime().tm_mon

  8. if len(str(month)) == 1:

  9. month = "0" + str(month)

  10. day = time.localtime().tm_mday

  11. if len(str(day)) == 1:

  12. day = "0" + str(day)

  13. return "{}年{}月{}日".format(year, month, day)

  14. # 返回英文格式的日期:xxxx/xx/xx

  15. def get_english_date():

  16. year = time.localtime().tm_year

  17. if len(str(year)) == 1:

  18. year = "0" + str(year)

  19. month = time.localtime().tm_mon

  20. if len(str(month)) == 1:

  21. month = "0" + str(month)

  22. day = time.localtime().tm_mday

  23. if len(str(day)) == 1:

  24. day = "0" + str(day)

  25. return "{}/{}/{}".format(year, month, day)

  26. # 返回中文格式的时间:xx时xx分xx秒

  27. def get_chinese_time():

  28. hour = time.localtime().tm_hour

  29. if len(str(hour)) == 1:

  30. hour = "0" + str(hour)

  31. minute = time.localtime().tm_min

  32. if len(str(minute)) == 1:

  33. minute = "0" + str(minute)

  34. second = time.localtime().tm_sec

  35. if len(str(second)) == 1:

  36. second = "0" + str(second)

  37. return "{}时{}分{}秒".format(hour, minute, second)

  38. # 返回英文格式的时间:xx:xx:xx

  39. def get_english_time():

  40. hour = time.localtime().tm_hour

  41. if len(str(hour)) == 1:

  42. hour = "0" + str(hour)

  43. minute = time.localtime().tm_min

  44. if len(str(minute)) == 1:

  45. minute = "0" + str(minute)

  46. second = time.localtime().tm_sec

  47. if len(str(second)) == 1:

  48. second = "0" + str(second)

  49. return "{}:{}:{}".format(hour, minute, second)

  50. # 返回中文格式的日期时间

  51. def get_chinese_datetime():

  52. return get_chinese_date() + " " + get_chinese_time()

  53. # 返回英文格式的日期时间

  54. def get_english_datetime():

  55. return get_english_date() + " " + get_english_time()

  56. if __name__ == "__main__":

  57. print(get_chinese_datetime())

  58. print(get_english_datetime())

log_util.py

 
  1. import logging

  2. import logging.config

  3. from conf.global_var import *

  4. # 日志配置文件:多个logger,每个logger指定不同的handler

  5. # handler:设定了日志输出行的格式

  6. # 以及设定写日志到文件(是否回滚)?还是到屏幕

  7. # 还定了打印日志的级别

  8. logging.config.fileConfig(LOG_CONF_FILE_PATH)

  9. logger = logging.getLogger("example01")

  10. def debug(message):

  11. logging.debug(message)

  12. def info(message):

  13. logging.info(message)

  14. def warning(message):

  15. logging.warning(message)

  16. def error(message):

  17. logging.error(message)

  18. if __name__ == "__main__":

  19. debug("hi")

  20. info("gloryroad")

  21. warning("hello")

  22. error("这是一个error日志")

screenshot.py

  1. import logging

  2. import logging.config

  3. from conf.global_var import *

  4. # 日志配置文件:多个logger,每个logger指定不同的handler

  5. # handler:设定了日志输出行的格式

  6. # 以及设定写日志到文件(是否回滚)?还是到屏幕

  7. # 还定了打印日志的级别

  8. logging.config.fileConfig(LOG_CONF_FILE_PATH)

  9. logger = logging.getLogger("example01")

  10. def debug(message):

  11. logging.debug(message)

  12. def info(message):

  13. logging.info(message)

  14. def warning(message):

  15. logging.warning(message)

  16. def error(message):

  17. logging.error(message)

  18. if __name__ == "__main__":

  19. debug("hi")

  20. info("gloryroad")

  21. warning("hello")

  22. error("这是一个error日志")

conf 包

配置文件及全局变量。

elements_repository.ini

 
  1. [126mail_loginPage]

  2. loginPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')]

  3. loginPage.username=xpath>//input[@name='email']

  4. loginPage.password=xpath>//input[@name='password']

  5. loginPage.loginbutton=id>dologin

  6. [126mail_homePage]

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

  8. [126mail_contactPersonPage]

  9. contactPersonPage.createButton=xpath>//span[text()='新建联系人']

  10. contactPersonPage.name=xpath>//a[@title='编辑详细姓名']/preceding-sibling::div/input

  11. contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input

  12. contactPersonPage.starContacts=xpath>//span[text()='设为星标联系人']/preceding-sibling::span/b

  13. contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input

  14. contactPersonPage.otherinfo=xpath>//textarea

  15. contactPersonPage.confirmButton=xpath>//span[.='确 定']

global_var.py

 
  1. import os

  2. # 工程根路径

  3. PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

  4. # 元素定位方法的ini配置文件路径

  5. ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "elements_repository.ini")

  6. # 驱动路径

  7. CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe"

  8. IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe"

  9. FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe"

  10. # 测试使用的浏览器

  11. BROWSER_NAME = "chrome"

  12. # 登录主页

  13. LOGIN_URL = "https://mail.126.com"

  14. # 日志配置文件路径

  15. LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "logger.conf")

  16. # 测试用例文件路径

  17. TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "测试用例.xlsx")

  18. # 截图保存路径

  19. SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "screenshot_path")

  20. # 单元测试报告输出目录

  21. UNITTEST_REPORT_PATH = os.path.join(PROJECT_ROOT_PATH, "report")

  22. # 登录账号sheet页数据列号

  23. ACCOUNT_USERNAME_COL = 1

  24. ACCOUNT_PWD_COL = 2

  25. ACCOUNT_DATA_SHEET_COL = 3

  26. ACCOUNT_IS_EXECUTE_COL = 4

  27. ACCOUNT_TEST_TIME_COL = 5

  28. ACCOUNT_TEST_RESULT_COL = 6

  29. ACCOUNT_TEST_EXCEPTION_INFO_COL = 7

  30. ACCOUNT_SCREENSHOT_COL = 8

  31. # 联系人sheet页数据列号

  32. CONTACT_NAME_COL = 1

  33. CONTACT_EMAIL_COL = 2

  34. CONTACT_IS_STAR_COL = 3

  35. CONTACT_PHONE_COL = 4

  36. CONTACT_REMARK_COL = 5

  37. CONTACT_ASSERT_KEYWORD_COL = 6

  38. CONTACT_IS_EXECUTE_COL = 7

  39. CONTACT_TEST_TIME_COL = 8

  40. CONTACT_TEST_RESULT_COL = 9

  41. CONTACT_TEST_EXCEPTION_INFO_COL = 10

  42. CONTACT_SCREENSHOT_COL = 11

  43. if __name__ == "__main__":

  44. print(PROJECT_ROOT_PATH)

logger.conf

 
  1. ###############################################

  2. [loggers]

  3. keys=root,example01,example02

  4. [logger_root]

  5. level=DEBUG

  6. handlers=hand01,hand02

  7. [logger_example01]

  8. handlers=hand01,hand02

  9. qualname=example01

  10. propagate=0

  11. [logger_example02]

  12. handlers=hand01,hand03

  13. qualname=example02

  14. propagate=0

  15. ###############################################

  16. [handlers]

  17. keys=hand01,hand02,hand03

  18. [handler_hand01]

  19. class=StreamHandler

  20. level=INFO

  21. formatter=form01

  22. args=(sys.stderr,)

  23. [handler_hand02]

  24. class=FileHandler

  25. level=DEBUG

  26. formatter=form01

  27. args=('.\\log\\126_mail_test.log', 'a')

  28. [handler_hand03]

  29. class=handlers.RotatingFileHandler

  30. level=INFO

  31. formatter=form01

  32. args=('.\\log\\126_mail_test.log', 'a', 10*1024*1024, 5)

  33. ###############################################

  34. [formatters]

  35. keys=form01,form02

  36. [formatter_form01]

  37. format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s

  38. datefmt=%Y-%m-%d %H:%M:%S

  39. [formatter_form02]

  40. format=%(name)-12s: %(levelname)-8s %(message)s

  41. datefmt=%Y-%m-%d %H:%M:%S

test_data 目录

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

log 目录

日志输出文件:126_mail_test.log

 
  1. ...

  2. ...

  3. 2021-02-23 16:59:15 log_util.py[line:19] INFO 登录成功【用户名:zhangjun252950418, 密码:zhangjun123, 断言关键字:退出】

  4. 2021-02-23 16:59:20 log_util.py[line:19] INFO 添加联系人成功【姓名:lily, 邮箱:lily@qq.com, 手机号:135xxxxxxx1, 是否星标联系人:是, 备注:常联系人, 断言关键字:lily@qq.com】

  5. 2021-02-23 16:59:24 log_util.py[line:27] ERROR 添加联系人失败【姓名:张三, 邮箱:zhangsan@qq.com, 手机号:158xxxxxxx3, 是否星标联系人:否, 备注:不常联系人, 断言关键字:zhangsan@qq.comxx】

  6. 2021-02-23 16:59:27 log_util.py[line:19] INFO 添加联系人成功【姓名:李四, 邮箱:lisi@qq.com, 手机号:157xxxxxx9, 是否星标联系人:否, 备注:, 断言关键字:李四】

  7. ...

  8. ...

screenshot_path 目录

异常截图保存目录:

main.py

本 PO 框架的运行主入口。

  1. from business_process.batch_login import *

  2. from business_process.batch_login_and_add_contact import *

  3. from conf.global_var import *

  4. # 示例组装:冒烟测试

  5. def smoke_test():

  6. batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")

  7. # 示例组装:全量测试

  8. def full_test():

  9. batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")

  10. if __name__ == "__main__":

  11. # smoke_test()

  12. full_test()

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

这些资料,对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云计算项目五:部署数据库服务mysql |部署共享存储服务NFS | 配置网站服务

部署数据库服务mysql |部署共享存储服务NFS | 配置网站服务 案例1:配置逻辑卷步骤一:创建LV步骤二:格式化案例2:配置数据库服务器步骤一:安装软件MySQL服务软件(2台数据库服务器都要安装)步骤二:挂载lv设备步骤三:启动服务步骤四:管理员登录案例3:配置主从同步步骤一…

wsl利用netsh端口转发实现http代理

1、端口转发 netsh interface portproxy add v4tov4 listenaddress192.168.1.102 listenport10086 connectaddress127.0.0.1 connectport99992 端口检查 上面命令执行完成后&#xff0c;检查命令是否执行成功 netsh interface portproxy show all检查端口是否正常监听 nets…

Spring什么是控制反转IOC和依赖注入DI的关系?什么是IOC容器?IOC容器管理组件的例子

控制反转IOC的概念 控制反转IOC 是Spring的一个思想&#xff0c;我们具象化到它是一个容器&#xff0c;包含并管理组件对象的生命周期&#xff0c;容器主动的将资源注入给需要的组件&#xff0c;开发人员不需要知道容器是如何创建资源对象的&#xff0c;只需要提供接收资源的…

threejs学习

重要概念&#xff08;场景、相机、渲染器&#xff09; 如下图所示&#xff0c;我们最终看到浏览器上生成的内容是通过虚拟场景和虚拟相机被渲染器渲染后的结果&#xff0c;下面首先介绍这三个概念&#xff0c;将贯穿所有简单复杂的threejs项目。 场景 Scene 虚拟的3D场景&a…

【论文+App试玩+图像到视频】2311.Animate-anyone:上传1张图片为任何人制作动画(用于角色动画的一致且可控的图像到视频合成)(暂未开源)

项目主页&#xff1a;https://humanaigc.github.io/animate-anyone/ 论文: Animate Anyone: Consistent and Controllable Image-to-Video Synthesis for Character Animation 摩尔线程复现代码&#xff1a;https://github.com/MooreThreads/Moore-AnimateAnyone 原作者讲解&am…

Go 定时器:如何避免潜在的内存泄漏陷阱

这篇文章将探讨的是 Go 中如何高效使用 timer&#xff0c;特别是与select 一起使用时&#xff0c;如何防止潜在的内存泄漏问题。 引出问题 先看一个例子&#xff0c;我们在 Go 中的 select 使用定时器&#xff0c;实现为消息监听加上超时能力。 核心代码&#xff0c;如下所示…

linux clickhouse 安装

1、官网下载clickhouse安装包 下载地址&#xff0c; clickhouse分lts和stable版本&#xff0c;lts是长期版本&#xff0c;一般选择安装lts版本。 其中clickhouse-server是clickhouse服务&#xff0c;就是用来访问数据存储数据&#xff0c;clickhouse-client是用来通过命令访问数…

【操作系统和计网从入门到深入】(四)基础IO和文件系统

前言 这个专栏其实是博主在复习操作系统和计算机网络时候的笔记&#xff0c;所以如果是博主比较熟悉的知识点&#xff0c;博主可能就直接跳过了&#xff0c;但是所有重要的知识点&#xff0c;在这个专栏里面都会提到&#xff01;而且我也一定会保证这个专栏知识点的完整性&…

Vue2 - keep-alive 作用和原理

目录 1&#xff0c;介绍和作用2&#xff0c;原理3&#xff0c;使用场景3.1&#xff0c;效果展示3.2&#xff0c;实现思路 1&#xff0c;介绍和作用 <!-- 非活跃的组件将会被缓存&#xff01; --> <keep-alive><component :is"activeComponent" />…

将AWS iot消息数据发送Kinesis Firehose Stream存向S3

观看此文章之前&#xff0c;请先学习AWS iot的数据收集&#xff1a; 使用Linux SDK客户端向AWS Iot发送数据-CSDN博客 1、工作原理&#xff1a; 1.1 规则 规则可让您的设备与 AWS 服务进行交互。分析规则并根据物品发送的消息执行操作。您可以使用规则来支持任务&#xff0…

前端开发提高效率的两大工具

一、浏览器中的开发者工具 怎么启动开发者工具&#xff1f; 在浏览器中按下F12或者鼠标右键点击检查 怎么利用&#xff08;常用的几点&#xff09;&#xff1f; 1、元素 点击标红的图标可以用于在页面选择元素&#xff0c;同时右侧会找到元素在前端代码中的位置 点击下方红…

2023 IoTDB Summit:中核武汉核电运行技术股份有限公司主管工程师方华建《IoTDB在核电数字化转型过程的应用实践》...

12 月 3 日&#xff0c;2023 IoTDB 用户大会在北京成功举行&#xff0c;收获强烈反响。本次峰会汇集了超 20 位大咖嘉宾带来工业互联网行业、技术、应用方向的精彩议题&#xff0c;多位学术泰斗、企业代表、开发者&#xff0c;深度分享了工业物联网时序数据库 IoTDB 的技术创新…

Pycharm终端显示PS而不显示虚拟环境venv

PS表示当前使用的是powershell.exe&#xff0c;如果你要显示虚拟环境名&#xff0c;则要改为cmd.exe 解决办法&#xff1a; 打开File-settings-Tools-Terminal-shell path 在文件中找到设置&#xff0c;在工具中找到终端 把第四个Shell路径设置为cmd.exe 3. 点击确定&#xf…

c#算法(10)——求点到直线的距离

前言 在上位机软件开发领域,特别是机器视觉领域,经常会遇到尺寸测量的场景,比如让我们求一个点到一条直线的距离,我们已知了直线上的两个点的坐标,然后又已知了直线外的一个点的坐标,那么如何求出该直线外的一点到直线的距离呢?本文就是来讲解如何求点到直线的距离的,…

uniapp page宽度设置为750rpx,子元素宽度100%,大小不一致

uniapp page宽度设置为750rpx&#xff0c;子元素宽度100%&#xff0c;大小不一致。 原因是我在page加了margin: 0 auto;去掉就正常了&#xff08;但是如果在超大屏幕还是会出现&#xff0c;我猜是使用rpx导致的&#xff0c;rpx渲染成页面时会转成精确到一个小数点几位数的rem&a…

【jetson笔记】vscode远程调试

vscode安装插件 vscode安装远程插件Remote-SSH 安装完毕点击左侧远程资源管理器 打开SSH配置文件 添加如下内容&#xff0c;Hostname为jetson IP&#xff0c;User为登录用户名需替换为自己的 Host aliasHostName 192.168.219.57User jetson配置好点击连接&#xff0c;控制台输…

详细Nginx和PHP-FPM的进程间通信使用

工作中考虑到PHP-FPM效率&#xff0c;发现PHP-FPM和NGINX的进程通信不止配置端口这一种方式:bowtie: Nginx和PHP-FPM的进程间通信有两种方式,一种是TCP,一种是UNIX Domain Socket. 其中TCP是IP加端口,可以跨服务器.而UNIX Domain Socket不经过网络,只能用于Nginx跟PHP-FPM都在同…

递归和尾递归(用C语言解斐波那契和阶乘问题)

很多人都对递归有了解&#xff0c;但是为尾递归很少&#xff0c;所以这次来专门讲一讲关于尾递归的一些问题。 什么是尾递归 如果一个函数中所有递归形式的调用都出现在函数的末尾&#xff0c;我们称这个递归函数是尾递归的。因为在一些题目的做法中&#xff0c;我们可以发现…

uniapp scroll-view用法[下拉刷新,触底事件等等...](4)

前言:可滚动视图区域。用于区域滚动 话不多说 直接上官网属性 官网示例 讲一下常用的几个 scroll 滚动时触发 scrolltoupper 滚动到顶部或左边&#xff0c;会触发 scrolltoupper 事件 scrolltolower 滚动到底部或右边&#xff0c;会触发 scrolltolower 事件 1.纵向滚动…

面向对象、封装、继承、多态、JavaBean

二、面向对象 什么是对象 什么是对象&#xff1f;之前我们讲过&#xff0c;对象就是计算机中的虚拟物体。例如 System.out&#xff0c;System.in 等等。然而&#xff0c;要开发自己的应用程序&#xff0c;只有这些现成的对象还远远不够。需要我们自己来创建新的对象。 1. 抽…