结合前面的元素寻找、操作、unittest测试框架,搭建一个完整的自动化框架。本篇旨在框架设计、单机用例执行、输出报告,下篇继续实践Bat批处理执行测试、多设备并发测试。
框架功能
- 数据配置
- 日志输出
- 截图处理
- 基础功能封装(公共方法,查找元素)
- 业务功能
- 数据驱动
- 测试用例封装
- 断言处理和报告输出
测试需求
测试环境
- win10
- appium 1.17.1
- weixin
- 真机
测试用例
登录场景1.用户名 xxx 密码 xxx (登录成功)
登录场景2.用户名 xxx 密码 xxx (登录失败)
框架设计
代码实现
1.日志输出
# -*- coding:utf-8 -*- # __author__ = "Cc" import logging import time class OutputLog: critical = logging.CRITICAL # 级别最高,什么也不输出 fatal = logging.FATAL error = logging.ERROR warning = logging.WARNING info = logging.INFO debug = logging.DEBUG @classmethod def output_log(cls, log_level=debug): my_logging = logging.getLogger(__name__) my_logging.setLevel(log_level) if not my_logging.handlers: local_time = time.localtime() file_name1 = time.strftime('%Y-%m-%d', local_time) file_name2 = r"Logging\" file_name = file_name2 + file_name1 + ".log" file_handler = logging.FileHandler(file_name, "a", encoding="utf-8") # 输出日志到磁盘文件 file_handler.setLevel(log_level) formatter = logging.Formatter("%(asctime)s--%(levelname)s--%(process)d--" "%(thread)d--%(threadName)s--%(funcName)s--%(lineno)d--%(lineno)d : %(message)s") file_handler.setFormatter(formatter) my_logging.addHandler(file_handler) return my_logging
遇到的问题和解决方法
举个调用的例子:
OutputLog.output_log().debug("==============开始测试,连接手机==============") OutputLog.output_log().debug("==============第二次调用==============")
上面代码,第一行日志输出只输出了一次,第二行输出了两次,原因是我在一开始实现时,每次调用都会重新创建一个handles,使用完后没有删除,同一log对象有多个handles,日志会重复输出,所以我在创建handles前先加以判断:if not my_logging.handlers,如果存在则不重新创建了。
2.设备初始化
2.1设备信息
保存设备信息在devices.yaml中,可以通过修改此文件,修改设备信息。
oppo_findx_pro: appActivity: com.tencent.mm.ui.LauncherUI appPackage: com.tencent.mm autoGrantPermissions: true automationName: UiAutomator2 chromeOptions: androidProcess: com.tencent.mm:toolsmp chromedriverExecutable: C:Usersv_yddchenDesktopchromedriver_win32 77.0chromedriver.exe deviceName: dd noReset: false platFormVersion: 10 platformName: Android resetKeyboard: true udid: 648d4f29 unicodeKeyboard: true oppo_reno: appActivity: com.tencent.mm.ui.LauncherUI appPackage: com.tencent.mm autoGrantPermissions: true automationName: UiAutomator2 chromeOptions: androidProcess: com.tencent.mm:toolsmp chromedriverExecutable: C:Usersv_yddchenDesktopchromedriver_win32 77.0chromedriver.exe deviceName: df93a63a noReset: false platFormVersion: 9 platformName: Android resetKeyboard: true udid: df93a63a unicodeKeyboard: true
2.2 初始化
初始化操作
# 初始化设备 # -*- coding:utf-8 -*- # __author__ = "Cc" from appium import webdriver import yaml from OutputLog import OutputLog from login import Login import time class InitDevices: def __init__(self, file_name, device_name): self.file_name = file_name self.device_name = device_name def read_devices(self): """ 获取设备信息 :return: """ try: OutputLog.output_log().debug("尝试获取设备信息") with open(self.file_name, 'r', encoding='utf-8') as f: all_devices = yaml.safe_load(f.read()) except IOError: OutputLog.output_log().error("设备文件读取错误") else: msg = str(all_devices[self.device_name]) OutputLog.output_log().debug(msg) return all_devices[self.device_name] def init_devices(self, device_info): """ 初始化设备 :param device_info: :return: """ return webdriver.Remote("http://localhost:4723/wd/hub", device_info) if __name__ == "__main__": OutputLog.output_log().debug("==============开始测试,连接手机==============") devices_object = InitDevices('devices.yaml', 'oppo_findx_pro') devices_info = devices_object.read_devices() devices = devices_object.init_devices(devices_info) OutputLog.output_log().debug("连接成功") # 连接成功,开始找元素 file_name = 'screenshots/' + '测试' + '.png' devices.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)录")') devices.get_screenshot_as_file(file_name) devices.implicitly_wait(5) login_els = Login(devices) time.sleep(2) time.sleep(2)
3.获取测试数据
3.1 数据准备
3.2 读取数据
# 读取测试数据 # -*- coding:utf-8 -*- # __author__ = "Cc" import csv class ReadData: def __init__(self, file_name): self.file_name = file_name def read_data(self): """ 注意文件不能有中文,否则会报错 :return: 二维数组data """ with open(self.file_name, 'r', encoding='utf-8') as f: csv_reader = csv.reader(f) head = next(csv_reader) # print(head) data = [[]] if len(data): data.clear() # 如果没有这一步,data会存在一个空值 for data1 in csv_reader: data.append(data1) else: for data1 in csv_reader: data.append(data1) # print(data) return data if __name__ == "__main__": re = ReadData("login_msg.csv") re.read_data()
4.公共方法
寻找元素的公共方法的封装
# 基类,查找元素 # -*- coding:utf-8 -*- # __author__ = "Cc" from appium.webdriver import webdriver from selenium.webdriver.common.by import By from OutputLog import OutputLog class BaseFindEl: def __init__(self, devices): """ 传入设备 :param devices: """ self.devices = devices def find_el_by_text(self, **kw): """ 根据传入text时关键字参数的名称,决定调用text的哪一个方法 :param kw: 查找元素的text :return: 返回找到的元素 """ if 'text' in kw: text = kw['text'] path = 'new UiSelector().text("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) elif 'textContains' in kw: text = kw['textContains'] path = 'new UiSelector().textContains("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) elif 'textStarsWith' in kw: text = kw['textStarsWith'] path = 'new UiSelector().textStarsWith("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) elif 'textMatches' in kw: text = kw['textMatches'] path = 'new UiSelector().textMatches("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) else: OutputLog.output_log().error("没有匹配到查找方法") def find_el_by_class_name(self, **kw): """ 根据传入的className查找元素 :param kw: className :return: 找到的元素 """ if 'className' in kw: text = kw['className'] path = 'new UiSelector().className("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) elif 'classNameContains' in kw: text = kw['classNameContains'] path = 'new UiSelector().classNameContains("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) else: OutputLog.output_log().error("没有匹配到查找方法") def find_el_by_resource_id(self, **kw): """ 根据传入的resourceId查找元素 :param kw: :return: """ if 'resourceId' in kw: text = kw['resourceId'] path = 'new UiSelector().resourceId("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) elif 'resourceIdMatches' in kw: text = kw['resourceIdMatches'] path = 'new UiSelector().resourceIdMatches("{}")'.format(text) return self.devices.find_element_by_android_uiautomator(path) else: OutputLog.output_log().error("没有匹配到查找方法") def find_el_by_multi_values(self, **values): """ 组合多个属性 :param values: :return: """ pass
5.业务功能
# 查找登录界面所有的元素 # -*- coding:utf-8 -*- # __author__ = "Cc" from BaseFindEl import BaseFindEl from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import TimeoutException from selenium.webdriver.support.ui import WebDriverWait from OutputLog import OutputLog class Login(BaseFindEl): def __init__(self, devices): BaseFindEl.__init__(self, devices) def login(self, text="登录"): """ 点击登录 :param text: :return: """ self.devices.implicitly_wait(3) try: btn = self.find_el_by_text(text=text) except NoSuchElementException: msg = "错误:没有找到{}控件".format(text) OutputLog.output_log().error(msg) else: OutputLog.output_log().debug("进入到登录界面") btn.click() def sign_in(self, text="注册"): """ :param text: 注册 :return: """ self.devices.implicitly_wait(3) try: btn = self.find_el_by_text(text=text) except NoSuchElementException: msg = "错误:没有找到{}控件".format(text) OutputLog.output_log().error(msg) else: OutputLog.output_log().debug("进入到注册界面") btn.click() def switch_to_username(self, text_contains="用微信号"): """ :param text_contains: 切换到微信号/QQ号登录 :return: """ try: btn = self.find_el_by_text(textContains=text_contains) except NoSuchElementException: msg = "错误:没有找到{}控件".format(text_contains) OutputLog.output_log().error(msg) else: OutputLog.output_log().debug("切换到微信号输入界面") btn.click() def user_edit(self, text_contains="请填写微信号"): """ 寻找账号输入框 :param text_contains: :return: 账号阿输入edit """ try: username_edit = self.find_el_by_text(textContains=text_contains) except NoSuchElementException: msg = "错误:没有找到{}控件".format(text_contains) OutputLog.output_log().error(msg) else: OutputLog.output_log().debug("找到了账号输入编辑框") return username_edit def pwd_edit(self, text_contains="请填写密码"): """ 寻找账号输入框 :param text_contains: :return: 账号阿输入edit """ try: pwd_edit = self.find_el_by_text(textContains=text_contains) except NoSuchElementException: msg = "错误:没有找到{}控件".format(text_contains) OutputLog.output_log().error(msg) else: OutputLog.output_log().debug("找到了账号输入编辑框") return pwd_edit def input_msg(self, user_name, pwd): """ 输入信息 :param user_name: 账号名称 :param pwd: 密码 :return: """ name_edit = self.user_edit() name_edit.clear() pwd_edit = self.pwd_edit() pwd_edit.clear() name_edit.send_keys(user_name) pwd_edit.send_keys(pwd) def find_toast(self, text="正在"): text_1 = "//*[contains(@text,'{}')]".format(text) toast = WebDriverWait(self.devices, 5, 0.00000001).until(lambda x: x.find_element_by_xpath(text_1)) return toast.text def login_fail(self, screenshots_nam): """ 处理登录失败的弹窗 :param screenshots_nam: 截图保存的名称 :return: """ try: WebDriverWait(self.devices, 3). until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textContains("密码错误")')) except TimeoutException: OutputLog.output_log().error("测试失败,没有找到登录失败的弹窗") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) return 0 else: OutputLog.output_log().debug("出现登录失败的弹窗") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) self.find_el_by_text(text='确定').click() return 1 def authorization_actions(self, screenshots_nam): try: WebDriverWait(self.devices, 3). until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)权限申请")')) except TimeoutException: OutputLog.output_log().debug("没有出现权限申请弹窗") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) else: OutputLog.output_log().debug("权限申请提示") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) self.find_el_by_text(text='我知道了').click() def phone_authorization_actions(self, screenshots_nam, action='允许'): """ :param screenshots_nam: 保存的截图 :param action: 允许或者拒绝 :return: """ try: WebDriverWait(self.devices, 4). until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)电话权限")')) except TimeoutException: OutputLog.output_log().debug("没有出现电话权限申请弹窗") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) else: OutputLog.output_log().debug("电话权限申请提示") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) self.find_el_by_text(text=action).click() def sd_card_authorization_actions(self, screenshots_nam, action='允许'): """ :param screenshots_nam: 保存的截图 :param action: 允许或者拒绝 :return: """ try: WebDriverWait(self.devices, 4). until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)空间权限")')) except TimeoutException: OutputLog.output_log().debug("没有出现空间权限申请弹窗") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) else: OutputLog.output_log().debug("空间权限申请提示") file_name = 'screenshots/' + screenshots_nam + '.png' self.devices.get_screenshot_as_file(file_name) self.find_el_by_text(text=action).click()
6.用例执行和报告输出
# 执行用例 # -*- coding:utf-8 -*- # __author__ = "Cc" from InitDevices import InitDevices from OutputLog import OutputLog from read_msg import ReadData from login import Login import unittest import time import os from selenium.webdriver.support.ui import WebDriverWait from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import TimeoutException import HTMLReport class LoginUnittest(unittest.TestCase): devices_object = None devices = None data = None index0 = 0 login_object = None def __init__(self, *args, **kwargs): """每个用例执行前,__init__都会执行一次""" super().__init__(*args, **kwargs) @classmethod def setUpClass(cls): """ 初始化设备,读取测试数据,获取一个测试对象 :return: """ OutputLog.output_log().debug("==============开始测试,连接手机==============") cls.devices_object = InitDevices('devices.yaml', 'oppo_findx_pro') devices_info = cls.devices_object.read_devices() cls.devices = cls.devices_object.init_devices(devices_info) # 返回设备对象 OutputLog.output_log().debug("连接成功") # 连接成功,开始操作 cls.data = ReadData("login_msg.csv").read_data() # 获取登录数据 cls.index0 = 0 cls.login_object = Login(cls.devices) @classmethod def tearDownClass(cls): """ devices.quit() :return: """ # OutputLog.output_log().debug("测试结束") # cls.devices.quit() f = os.popen(r"adb shell dumpsys activity top | findstr ACTIVITY", "r") # 获取当前界面的Activity current_activity = f.read() f.close() print(current_activity) # cmd输出结果 # 用in方法 判断一个字符串是否包含某字符 appackage_name = 'com.ximalaya.ting.android' if appackage_name in current_activity: cls.drivers.quit() else: pass def setUp(self): """ 每个用例执行前执行,这里切换登录方式 :return: """ LoginUnittest.login_object.login() time.sleep(1) LoginUnittest.login_object.switch_to_username() def tearDown(self): """ 每个用例执行后执行,os.system("adb shell pm clear com.tencent.mm"),执行成功返回0 :return: """ time.sleep(1) if not os.system("adb shell pm clear com.tencent.mm"): # os.system("adb shell pm grant com.tencent.mm") OutputLog.output_log().debug("清除应用数据") LoginUnittest.index0 = LoginUnittest.index0 + 1 time.sleep(3) # LoginUnittest.devices.start_activity('com.tencent.mm', '.ui.LauncherUI') os.system('adb shell am start com.tencent.mm/.ui.LauncherUI') else: OutputLog.output_log().debug("清除应用数据失败") time.sleep(2) # 测试登录失败 def test_login_1(self): user_name = LoginUnittest.data[LoginUnittest.index0][0] pwd = LoginUnittest.data[LoginUnittest.index0][1] LoginUnittest.login_object.input_msg(user_name, pwd) msg = "登录信息" + user_name + pwd OutputLog.output_log().debug(msg) LoginUnittest.login_object.login() # 登录 # try: # btn = WebDriverWait(LoginUnittest.devices, 7). # until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().text("通讯录")')) # except TimeoutException: # OutputLog.output_log().debug('登录失败') # self.assertEqual(1, 1, '登录失败') file_name = "test_login_1" + "登录失败" result = LoginUnittest.login_object.login_fail(file_name) self.assertEqual(result, 1, '失败') def test_login_2(self): user_name = LoginUnittest.data[LoginUnittest.index0][0] pwd = LoginUnittest.data[LoginUnittest.index0][1] LoginUnittest.login_object.input_msg(user_name, pwd) msg = "登录信息" + user_name + pwd OutputLog.output_log().debug(msg) LoginUnittest.login_object.login() file_name_1 = 'test_login_2' + '权限申请提醒' LoginUnittest.login_object.authorization_actions(file_name_1) file_name_3 = 'test_login_2' + '存储权限申请提醒' LoginUnittest.login_object.phone_authorization_actions(file_name_3) file_name_2 = 'test_login_2' + '电话权限申请提醒' LoginUnittest.login_object.phone_authorization_actions(file_name_2) try: btn = WebDriverWait(LoginUnittest.devices, 7). until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().text("通讯录")')) except TimeoutException: OutputLog.output_log().debug('登录失败') else: self.assertEqual(btn.text, '通讯录', '测试成功') if __name__ == '__main__': test_suite = unittest.TestSuite() tests = [LoginUnittest('test_login_1'), LoginUnittest('test_login_2')] test_suite.addTests(tests) # runner = unittest.TextTestRunner() runner = HTMLReport.TestRunner( report_file_name="login_reports", output_path="login_report", title="登录功能测试报告", description="测试登录功能", thread_count=1, thread_start_wait=0, tries=0, delay=0, back_off=1, retry=False, sequential_execution=True, lang="cn" ) runner.run(test_suite)
7.执行结果
HTMLReport默认会输出一份.html报告文件和一份日志文件。只执行了两个用例,但是耗时0:2:28,代码还有待优化,Bat批处理命令执行,会不会对用例的执行效率有所提升呢?期待接下来的实践。
参考链接:https://sutune.me/2018/05/10/appium-autoTest-frame/ 这位博主的文章很不错,很适合入门。