结合前面的元素寻找、操作、unittest测试框架,搭建一个完整的自动化框架。本篇旨在框架设计、单机用例执行、输出报告,下篇继续实践Bat批处理执行测试、多设备并发测试。
框架功能
- 数据配置
- 日志输出
- 截图处理
- 基础功能封装(公共方法,查找元素)
- 业务功能
- 数据驱动
- 测试用例封装
- 断言处理和报告输出
测试需求
测试环境
- win10
- appium 1.17.1
- weixin
- 真机
测试用例
登录场景1.用户名 xxx 密码 xxx (登录成功)
登录场景2.用户名 xxx 密码 xxx (登录失败)
框架设计
-
现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
-
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
-
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
-
分享他们的经验,还会分享很多直播讲座和技术沙龙
-
可以免费学习!划重点!开源的!!!
-
qq群号:680748947【暗号:csdn11】
代码实现
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:\Users\v_yddchen\Desktop\chromedriver_win32 77.0\chromedriver.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:\Users\v_yddchen\Desktop\chromedriver_win32 77.0\chromedriver.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批处理命令执行,会不会对用例的执行效率有所提升呢?期待接下来的实践。
总结:
感谢每一个认真阅读我文章的人!!!
作为一位过来人也是希望大家少走一些弯路,如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助。
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
视频文档获取方式:
这份文档和视频资料,对于想从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!以上均可以分享,点下方小卡片即可自行领取。