全网最牛,Appium自动化测试框架-关键字驱动+数据驱动实战(二)
目录:导读
前言
util 包
util 包属于第一层的测试工具层:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。
global_var.py
本模块用于定义测试过程中所需的全局变量。
import os
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# APP配置信息路径
INI_FILE_PATH = os.path.join(PROJECT_DIR, "conf", "desired_caps_config.ini")
# 异常截图路径
EXCEPION_PIC_PATH = os.path.join(PROJECT_DIR, "exception_pic")
# 日志配置文件路径
LOG_CONF_FILE_PATH = os.path.join(PROJECT_DIR, "conf", "logger.conf")
# 测试数据文件路径
TEST_DATA_FILE_PATH = os.path.join(PROJECT_DIR, "test_data", "test_case.xlsx")
# 测试报告存放路径
TEST_REPORT_FILE_DIR = os.path.join(PROJECT_DIR, "test_report")
# Appium server地址
APPIUM_SERVER = 'http://localhost:4723/wd/hub'
# 测试数据文件中,测试用例sheet中部分列对应的数字序号
TESTCASE_CASE_NAME_COL_NO = 0
TESTCASE_FRAMEWORK_TYPE_COL_NO = 1
TESTCASE_CASE_STEP_SHEET_NAME_COL_NO = 2
TESTCASE_DATA_SOURCE_SHEET_NAME_COL_NO = 3
TESTCASE_IS_EXECUTE_COL_NO = 4
TESTCASE_TEST_TIME_COL_NO = 5
TESTCASE_TEST_RESULT_COL_NO = 6
# 用例步骤sheet中,部分列对应的数字序号
CASESTEP_NAME_COL_NO = 0
CASESTEP_ACTION_COL_NO = 1
CASESTEP_LOCATE_METHOD_COL_NO = 2
CASESTEP_LOCATE_EXPRESSION_COL_NO = 3
CASESTEP_OPERATION_VALUE_COL_NO = 4
CASESTEP_IS_EXECUTE_COL_NO = 5
CASESTEP_TEST_TIME_COL_NO = 6
CASESTEP_TEST_RESULT_COL_NO = 7
CASESTEP_EXCEPTION_INFO_COL_NO = 8
CASESTEP_EXCEPTION_PIC_DIR_COL_NO = 9
# 数据源sheet中,是否执行列对应的数字编号
DATASOURCE_DATA = 0
DATASOURCE_KEYWORD = 1
DATASOURCE_IS_EXECUTE = 2
DATASOURCE_TEST_TIME = 3
DATASOURCE_TEST_RESULT = 4
# 测试执行结果统计
TOTAL_CASE = 0
PASS_CASE = 0
FAIL_CASE = 0
if __name__ == "__main__":
print(PROJECT_DIR)
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))
excel_util.py
本模块封装了对 excel 的读写操作(openpyxl 版本:3.0.4)。
import os
from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font, Side, Border
from util.datetime_util import *
from util.global_var import *
from util.log_util import *
# 支持excel读写操作的工具类
class Excel:
# 初始化读取excel文件
def __init__(self, file_path):
if not os.path.exists(file_path):
return
self.wb = load_workbook(file_path)
# 初始化默认sheet
self.ws = self.wb.active
self.data_file_path = file_path
# 初始化颜色字典,供设置样式用
self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}
def get_all_sheet(self):
return self.wb.get_sheet_names()
# 打开指定sheet
def get_sheet(self, sheet_name):
if sheet_name not in self.get_all_sheet():
print("sheet名称【%s】不存在!" % sheet_name)
return
self.ws = self.wb.get_sheet_by_name(sheet_name)
return True
# 获取最大行号
def get_max_row_no(self):
# openpyxl的API的行、列索引默认都从1开始
return self.ws.max_row
# 获取最大列号
def get_max_col_no(self):
return self.ws.max_column
# 获取所有行数据
def get_all_row_data(self, head_line=True):
# 是否需要标题行数据的标识,默认需要
if head_line:
min_row = 1 # 行号从1开始,即1为标题行
else:
min_row = 2
result = []
# min_row=None:默认获取标题行数据
for row in self.ws.iter_rows(min_row=min_row, max_row=self.get_max_row_no(), max_col=self.get_max_col_no()):
result.append([cell.value for cell in row])
return result
# 获取指定行数据
def get_row_data(self, row_num):
# 0 为标题行
return [cell.value for cell in self.ws[row_num+1]]
# 获取指定列数据
def get_col_data(self, col_num):
# 索引从0开始
return [cell.value for cell in tuple(self.ws.columns)[col_num]]
# 追加行数据且可以设置样式
def write_row_data(self, data, font_color=None, border=True, fill_color=None):
if not isinstance(data, (list, tuple)):
print("写入数据失败:数据不为列表或元组类型!【%s】" % data)
self.ws.append(data)
# 设置字体颜色
if font_color:
if font_color.lower() in self.color_dict.keys():
font_color = self.color_dict[font_color]
# 设置单元格填充颜色
if fill_color:
if fill_color.lower() in self.color_dict.keys():
fill_color = self.color_dict[fill_color]
# 设置单元格边框
if border:
bd = Side(style="thin", color="000000")
# 记录数据长度(否则会默认与之前行最长数据行的长度相同,导致样式超过了该行实际长度)
count = 0
for cell in self.ws[self.get_max_row_no()]:
# 设置完该行的实际数据长度样式后,则退出
if count > len(data) - 1:
break
if font_color:
cell.font = Font(color=font_color)
# 如果没有设置字体颜色,则默认给执行结果添加字体颜色
else:
if cell.value is not None and isinstance(cell.value, str):
if cell.value.lower() == "pass" or cell.value == "成功":
cell.font = Font(color=self.color_dict["green"])
elif cell.value.lower() == "fail" or cell.value == "失败":
cell.font = Font(color=self.color_dict["red"])
if border:
cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)
if fill_color:
cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)
count += 1
# 指定行插入数据(行索引从0开始)
def insert_row_data(self, row_no, data, font_color=None, border=True, fill_color=None):
if not isinstance(data, (list, tuple)):
print("写入数据失败:数据不为列表或元组类型!【%s】" % data)
for idx, cell in enumerate(self.ws[row_no+1]): # 此处行索引从1开始
cell.value = data[idx]
# 生成写入了测试结果的excel数据文件
def save(self, save_file_name, timestamp):
save_dir = os.path.join(TEST_REPORT_FILE_DIR, get_chinese_date())
if not os.path.exists(save_dir):
os.mkdir(save_dir)
save_file = os.path.join(save_dir, save_file_name + "_" + timestamp + ".xlsx")
self.wb.save(save_file)
info("生成测试结果文件:%s" % save_file)
return save_file
if __name__ == "__main__":
from util.global_var import *
from util.datetime_util import *
excel = Excel(TEST_DATA_FILE_PATH)
excel.get_sheet("测试结果统计")
# print(excel.get_all_row_data())
# excel.write_row_data(["4", None, "嘻哈"], "green", True, "red")
excel.insert_row_data(1, [1,2,3])
excel.save(get_timestamp())
ini_reader.py
本模块封装了对 ini 配置文件的读取操作。
import os
import configparser
# 读取ini文件的工具类
class IniParser:
# 初始化打开ini文件
def __init__(self, file_path):
if not os.path.exists(file_path):
print("ini文件【%s】不存在!" % file_path)
return
self.cf = configparser.ConfigParser()
self.cf.read(file_path, encoding="utf-8")
# 获取所有分组
def get_sections(self):
return self.cf.sections()
# 获取指定分组的所有键
def get_options(self, section):
return self.cf.options(section) # 注意,获取的键会自动转小写
# 获取指定分组的所有键值对
def get_items(self, section):
return dict(self.cf.items(section)) # 注意,获取的键会自动转小写
# 获取指定分组指定键的值
def get_value(self, section, option):
return self.cf.get(section, option)
if __name__ == "__main__":
from util.global_var import *
p = IniParser(INI_FILE_PATH)
print(p.get_sections())
print(p.get_options("desired_caps"))
print(p.get_items("desired_caps"))
print(p.get_value("desired_caps", "deviceName"))
email_util.py
本模块封装了邮件发送功能。(示例代码中的用户名/密码已隐藏)
import yagmail
import traceback
from util.log_util import *
def send_mail(attachments_report_name, receiver, subject, content):
try:
# 连接邮箱服务器
# 注意:若使用QQ邮箱,则password为授权码而非邮箱密码;使用其它邮箱则为邮箱密码
# encoding设置为GBK,否则中文附件名会乱码
yag = yagmail.SMTP(user="*****@163.com", password="*****", host="smtp.163.com", encoding='GBK')
# 收件人、标题、正文、附件(若多个收件人或多个附件,则可使用列表)
yag.send(to=receiver, subject=subject, contents=content, attachments=attachments_report_name)
# 可简写:yag.send("****@163.com", subject, contents, report)
info("测试报告邮件发送成功!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))
except:
error("测试报告邮件发送失败!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))
error(traceback.format_exc())
if __name__ == "__main__":
send_mail("e:\\code.txt", "xxxxxx@qq.com", "测试邮件", "正文")
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()
# 返回时间戳
def get_timestamp():
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)
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(year, month, day, hour, minute, second)
if __name__ == "__main__":
print(get_chinese_datetime())
print(get_english_datetime())
get_desired_caps.py
本模块实现了获取 ini 配置文件中的 Appium 创建 Session 的配置信息。
from util.ini_reader import IniParser
from util.global_var import INI_FILE_PATH
def get_desired_caps():
pcf = IniParser(INI_FILE_PATH)
items = pcf.get_items("desired_caps") # 获取的键会自动转成小写
desired_caps = {
"platformName": items.get("platformname"),
"platformVersion": items.get("platformversion"),
"deviceName": items.get("devicename"),
"appPackage": items.get("apppackage"),
"appActivity": items.get("appactivity"),
"unicodeKeyboard": items.get("unicodekeyboard"),
"autoAcceptAlerts": items.get("autoacceptalerts"),
"resetKeyboard": items.get("resetkeyboard"),
"noReset": items.get("noreset"),
"newCommandTimeout": items.get("newcommandtimeout")
}
return desired_caps
if __name__ == "__main__":
from util.global_var import *
print(get_desired_caps())
log_util.py
封装了日志打印输出、级别设定等功能。
import logging
import logging.config
from util.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日志")
report_util.py
生成测试结果文件并发送邮件。
from util.email_util import send_mail
from util.log_util import *
from util.datetime_util import *
# 生成测试报告并发送邮件
def create_excel_report_and_send_email(excel_obj, receiver, subject, content):
"""
:param excel_obj: excel对象用于保存文件
:param timestamp: 用于文件命名的时间戳
:return: 返回excel测试报告文件名
"""
time_stamp = get_timestamp()
report_path = excel_obj.save(subject, time_stamp)
send_mail(report_path, receiver, subject+"_"+time_stamp, content)
conf 目录
conf 目录属于第一层测试工具层,用于存储各配置文件。
desired_caps_config.ini
本配置文件存储了 Appium 创建 Session 的配置信息。
[desired_caps]
platformName=Android
platformVersion=6
deviceName=3DN6T16B26001805
appPackage=com.xsteach.appedu
appActivity=com.xsteach.appedu.StartActivity
unicodeKeyboard=True
autoAcceptAlerts=True
resetKeyboard=True
noReset=True
newCommandTimeout=6000
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=('E:\\pycharm_project_dir\\AppAutoTest\\log\\app_test.log', 'a')
[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('E:\\pycharm_project_dir\\AppAutoTest\\log\\app_test.log', 'a', 10*1024*1024, 5)
###############################################
[formatters]
keys=form01,form02
[formatter_form01]
format=%(asctime)s [%(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 目录
test_data 目录用于存放测试数据文件(Excel),存储了用例步骤、用例执行关键字、数据源等测试数据。



main.py
本模块是本框架的运行主入口,属于第四层“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景。
基于 business_process/main_process.py 中的模块用例 sheet 执行函数或主 sheet 执行函数,组装测试场景。
可直接用代码组装测试场景,也可根据 excel 数据文件的用例集合和用例步骤的维护来设定测试场景。
完成测试执行后生成测试结果文件并发送邮件。
from bussiness_process.main_process import *
from util.report_util import *
# 组装测试场景
# 冒烟测试
def smoke_test(report_name):
excel, _ = suite_process(TEST_DATA_FILE_PATH, "进入主页")
excel, _ = suite_process(excel, "登录")
excel, _ = suite_process(excel, "退出")
# 生成测试报告并发送邮件
create_excel_report_and_send_email(excel, ['xxxxxxxxx@163.com', 'xxxxxxxx@qq.com'], report_name, "请查收附件:app自动化测试报告")
# 全量测试:执行主sheet的用例集
def suite_test(report_name):
excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")
create_excel_report_and_send_email(excel, ['xxxxxxx@163.com', 'xxxxxxx@qq.com'], report_name, "请查收附件:app自动化测试报告")
if __name__ == "__main__":
# smoke_test("APP自动化测试报告_冒烟测试")
suite_test("APP自动化测试报告_全量测试")
test_report 目录
本目录用于存放测试结果文件。



exception_pic 目录
本目录用于存放失败用例的截图。

log 目录
本目录用于存放日志输出文件(日志内容同时也会输出到控制台)。
log/app_test.log:

| 下面是我整理的2023年最全的软件测试工程师学习知识架构体系图 |
一、Python编程入门到精通

二、接口自动化项目实战

三、Web自动化项目实战

四、App自动化项目实战

五、一线大厂简历

六、测试开发DevOps体系

七、常用自动化测试工具

八、JMeter性能测试

九、总结(尾部小惊喜)
不论起点如何,只要拥有激情与毅力,奋斗就是通往成功的道路;不论困难多大,只要心怀信念与勇气,挑战就是成长的机会。努力奋斗,追逐梦想,将创造属于自己的辉煌人生!
每一次努力,都是迈向辉煌的一步;每一次奋斗,都是砥砺前行的力量。不辜负青春,铭记初心,扬起梦想的风帆,执着追逐,终将驶向成功的彼岸。
不论身处何地,只要心怀梦想,就别停止追逐;不管困难多大,只要坚持奋斗,就能开创未来。披荆斩棘,勇往直前,舞动青春的旋律,绽放人生的辉煌!