应用场景:安卓版移动端的自动化测试;以网易音乐APP为列
说明:demo中有两个测试用例:一个是流程测试(成功),一个用例失败时保存截图,并以附件的形式保存在allure报告中
话不多多说,直接上代码
一、以PO的分层结构
二、分别介绍各个层
1.commons层:
deal_log.py:
import logging
from datetime import datetime
from configs.config import *
class Logs:
# logging模块默认设置的日志级别是warning,而debug和info的级别是低于warning的,所以不会打印这两种日志信息
def __init__(self, logger=None):
# 1.定义日志收集器
# 创建日志器logger对象以及日志级别
self.logger = logging.getLogger(logger) # 初始化日志类,定义一个日志收集器
self.logger.setLevel(logging.DEBUG) # 设置收集器的级别,不设定的话,默认收集warning及以上级别的日志
# self.logger.setLevel("DEBUG") # 也可以设置日志级别
# self.logTime = datetime.now().strftime("%Y_%m_%d_%H_%M")
self.logTime = datetime.now().strftime("%Y_%m_%d")
self.logPath = LogPath
self.logName = self.logPath + self.logTime + '.log'
# self.logName = r'../logs\{}.log'.format(time.strftime('%Y-%m-%d-%H:%M:%S', time.localtime())) # r是防止字符转转义,保留原有的样子(注意:返回上一次的路径是两个..)
# 创建处理器handler以及日志级别
# conlHandler = logging.StreamHandler() # 此处理器是输出到控制台
fileHandler = logging.FileHandler(self.logName, 'a', encoding='utf-8') # 此处理器是输出到文件
# conlHandler.setLevel('DEBUG') # 设置控制台输出日志级别ERROR 或DEBUG
fileHandler.setLevel('INFO') # 设置文件输出日志级别 ERROR'
# 设置日志输出格式
# %(asctime)s:打印日志的时间, %(filename)s:打印当前执行程序名, %(levelname)s:打印日志级别名称,
# %(lineno)s:打印日志的当前行号, %(message)s:打印日志信息, %(name)s:Logger的名字
# conlFormatter = logging.Formatter(
# "%(asctime)s-%(lineno)d-[%(levelname)s]-[msg]: %(message)s") # 输出到控制台的格式
fileFormatter = logging.Formatter(
"%(asctime)s-%(name)s-%(lineno)d-[%(levelname)s]-[msg]: %(message)s") # 输出到文件中的格式
# 用setFormatter()将上面设置的formatter配置到handler中
# conlHandler.setFormatter(conlFormatter) # 配置控制台日志输出格式
fileHandler.setFormatter(fileFormatter) # 配置文件日志输出格式
# 用addHandler()将配置好格式的Haddler添加到logger中,进行过滤
# self.logger.addHandler(conlHandler)
self.logger.addHandler(fileHandler)
# 添加下面一句,在记录日志之后移除句柄
# self.logger.removeHandler(ch)
# self.logger.removeHandler(fh)
# 关闭处理器
# conlHandler.close()
fileHandler.close()
def get_log(self):
return self.logger
deal_operate.py
# @Fuction :主要是页面的一些操作
import allure
from datetime import datetime
from appium import webdriver
from commons.deal_log import Logs
from configs.config import ImagePath
class Operates():
# 初始化页面的操作
def __init__(self):
"""
构造函数,创建必要的实例变量
"""
self.log = Logs().get_log() # 初始化一个log对象
# def init_driver(self):
# """
# 告诉appium自动化测试相关的配置项
# :return: 返回驱动
# """
caps = {
# 被测APP所处平台-操作系统
'platformName': 'Android',
# 操作系统版本
'platformVersion': '10',
# 设备明后才能——可以随表填写,但是必须要有
'deviceName': 'wangyimusic',
# 被测APP的信息————打开某个APP后输入命令:adb shell dumpsys activity recents | findstr intent
# cmd上展示的第一行命令:com.android.mediacenter/.PageActivity
# 包名——代表被测app在设备上的地址
'appPackage': 'com.netease.cloudmusic',
# 入口信息——被测app入口
'appActivity': '.activity.LoadingActivity',
# 禁止app在自动化后重置
'noReset': True,
# 设置命令超时时间,超过后driver会关闭
'newCommandTimeout': 3600,
# 指定驱动——UI2,安卓5以下用uiautomator1,以上用uiautomator2
'automationName': 'UiAutomator2',
# 支持中文
'unicodeKeyboard': True, # 使用 Unicode 输入法
'resetKeyboard': True, # 在设定了 `unicodeKeyboard` 关键字的 Unicode 测试结束后,重置输入法到原有状态
}
# 启动被测试app,启动之前打开appium server
self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', caps) # 如果访问的是本机就用localhost或127.0.0.1, wd/hub是固定的
self.driver.implicitly_wait(10) # 设置隐士等待10s
# return driver
def click(self, *locator):
"""
找到元素,并点击
:param locator: 定位器
:return:
"""
try:
self.driver.find_element(*locator).click()
except Exception:
self.log.error("定位点击元素失败!")
def click_ele_exist(self, *locator):
"""
当定为元素出现时,定位元素,并点击
:param locator: 定位器
:return:
"""
try:
self.driver.find_element(*locator)
btns = self.driver.find_elements(*locator)
# 如果出现用户协议弹出按钮
if btns:
btns[0].click()
except Exception:
self.log.error("定位有时出现的点击元素失败!")
def input_text(self, value, *locator):
"""
定位元素,并完成输入
:param text:
:param locator:
:return:
"""
try:
self.driver.find_element(*locator).send_keys(str(value))
except Exception:
self.log.info("定位输入元素失败!")
def get_text(self, *locator):
"""
获取文本元素
:param locator:
:return:
"""
try:
ele = self.driver.find_element(*locator)
except Exception:
self.log.error("获取元素文本失败!")
else:
return ele.text
def getImage(self, image_name):
"""
生成用例失败的截图,并将截图展示到allure报告中
:param image_name: 截图的名称
:return:
"""
try:
nowTime = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
NewPicture = ImagePath + '\\' + nowTime + '_' + image_name + '.png' # 保存图片为png格式
self.driver.get_screenshot_as_file(NewPicture)
allure.attach.file(NewPicture, attachment_type=allure.attachment_type.PNG) # 将截图作为附件上传到allure测试报告中
except Exception:
self.log.error(u'截图失败!')
def getElements(self, *locator):
"""
根据某种方式定位到多个元素
:return:
"""
try:
eles = self.driver.find_elements(*locator)
except Exception:
self.log.error("定位多个元素失败!")
else:
return eles
def new_swap(self, start_x, start_y, end_x, end_y):
"""
重新疯转swap()函数
滑动页面
:param start_x: 当前位置的横坐标
:param start_y: 当前位置的竖坐标
:param end_x: 滑动后位置的横坐标
:param end_y: 滑动后位置的竖坐标
:return:
"""
try:
self.driver.swap(start_x, start_y, end_x, end_y)
except Exception:
self.log.error("滑动页面失败!")
def new_keyEvent(self, key):
"""
重新封装keyEvent()函数
根据不同的键值,来确定系统的操作
:param key:
:return:
"""
try:
self.driver.keyevent(key)
except Exception:
self.log.error("系统键操作失败!")
def quit(self):
"""
退出浏览器
:return:
"""
try:
self.driver.quit()
except Exception:
self.log.error("退出浏览器失败!")
deal_yaml.py
# 获取项目路径、项目的各环境的url,处理数据文件的方法,框架执行相关日志功能的实现方法
import os, configparser
# os: 获取操作系统级别的目录/文件夹的操作和文件的操作(读取,写入)
# configparser: 在python中的主要功能是读取配置文件config.ini
# 获取config.ini路径
import yaml
from configs.config import DataPath
#def congfig_path():
# return os.path.split(os.path.realpath(__file__))[0].split('C')[0]
# 获取当前文件的路径
#curPath = os.path.dirname(os.path.realpath(__file__))
# 返回config.ini路径
#return os.path.join(curPath, "config.ini")
# 返回config.ini中的testUrl
#def config_url(key, value):
# 创建配置文件管理对象
#config = configparser.ConfigParser()
# 读取config.ini文件
#config.read(congfig_path(), encoding="utf-8")
#return config.get(key, value)
def read_yaml(key):
"""
读取yaml文件
:return:
"""
try:
# with open(path, encoding='utf-8') as f:
# eles = yaml.safe_load(f)
path = DataPath # 配置在congfig中的相对路径
# path = '../datas/test02.yaml'
openYaml = open(path, 'r', encoding='UTF-8')
data = yaml.load(openYaml, Loader=yaml.FullLoader)
return data[key]
except Exception:
print(u"未找到yaml文件")
2.configs层
config.py
DataPath = "./datas/test01.yaml" # yaml个数的测试数据的存放路径
LogPath = "./logs/" # 日志文件存放路径
ImagePath = "./pictures/" # 截图存放路径
3.datas层
test01.yaml
musicProcedure: # 测试App流程
-
title: APP流程成功
description: 流程成功的用例,用来测试添加歌单并添加歌曲,查看添加的歌曲,最后删除歌单的流程
caseDatas:
stepName1: 点击“我的”按钮,进入该页面 # 也可也直接写在代码中
myBtn: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/bottomNav"]/android.view.ViewGroup[3]'] # 函数中定位的数据和输入的值
stepName2: 创建歌单("测试歌单001")
songListBtn: ['id','com.netease.cloudmusic:id/create']
inputlistName: ['id','com.netease.cloudmusic:id/etPlaylistName']
finishlistBtn: ['id','com.netease.cloudmusic:id/tvCreatePlayListComplete']
stepName3: 进入每日推荐列表,选取两首歌曲添加到新建的歌单中
discoveryBtn: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/bottomNav"]/android.view.ViewGroup[1]']
dailyRecommend: ['xpath','//*[@text="每日推荐"]']
songNames: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/daily_rv"]/android.widget.LinearLayout/android.widget.ImageView[3]']
collectSong: ['xpath','//*[@text="收藏到歌单"]']
listBtn: ['xpath','//*[@text="测试歌单001"]']
stepName4: 查看添加的歌曲
myPage: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/bottomNav"]/android.view.ViewGroup[3]']
checkList: ['xpath','//*[@text="测试歌单001"]']
checkSong: ['id','com.netease.cloudmusic:id/songName']
stepName5: 删除添加的歌单
findList: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/my_recycler_view"]/android.widget.FrameLayout[1]/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ImageView']
deleteBtn: ['xpath','//*[@text="删除"]']
confirmBtn: ['id','com.netease.cloudmusic:id/buttonDefaultPositive']
checkList:
-
title: 测试歌单名不在歌单列表时失败截图处理
description: 主要目的是将失败截图保存在picture中,且以附件的形式上传在allure测试报告中
caseDatas:
stepName1: 点击“我的”按钮,进入该页面 # 也可也直接写在代码中
myBtn: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/bottomNav"]/android.view.ViewGroup[3]'] # 函数中定位的数据和输入的值
stepName2: 获取歌单列表歌单名
listNames: ['xpath','//*[@resource-id="com.netease.cloudmusic:id/my_recycler_view"]/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView']
4.test_cases层
conftest.py
import pytest
@pytest.fixture(scope='session')
def setListName():
Name = '测试歌单001'
print('获取歌单的名字---')
yield Name
test_app.py
import allure
import pytest
import time
from commons.deal_log import Logs
from commons.deal_operate import Operates
from commons.deal_yaml import read_yaml
class Test_WangYiMusic:
def setup_class(self):
"""
构造函数,创建类象时会执行
:return:
"""
print("执行setup_class-------")
self.baseDriver = Operates() # 创建一个对象
self.log = Logs().get_log() # 获取一个对象的get_log()函数
def teardown_class(self):
"""
执行完类中的测试用例后进行的清理工作
:param self:
:return:
"""
print("结束teardown_class—————————")
self.baseDriver.quit()
# 测试网易音乐APP某个流程: 建立一个新的歌单(测试歌单)- 进入“每日精选”-选取其中的两首歌曲,添加到新建的歌单中-进入歌单查看歌曲是否成功-最善删除新建的歌单
@allure.story('网易音乐APP的成功流程') # 同一个分组,比如成功和失败
@pytest.mark.parametrize('data', read_yaml('musicProcedure'))
def test_appPocedure(self, data, setListName):
self.log.info("执行---test_appPocedure------")
allure.dynamic.title(data['title']) # allure:获取yaml文件中的title
allure.description(data['description']) # allure: 获取yaml文件中的description
print('获取data的title数据:------', data['title'])
caseData = data['caseDatas']
listName = setListName
# 1.进入“我的”页面
with allure.step(caseData['stepName1']):
self.baseDriver.click(*caseData['myBtn'])
# 2、创建新的歌单,并返回到 我的 页面
with allure.step(caseData['stepName2']):
# 2.1、点击 创建歌单
self.baseDriver.click(*caseData['songListBtn'])
# 2.2、输入歌单名称——测试歌单
self.baseDriver.input_text(listName, *caseData['inputlistName'])
time.sleep(2)
# 2.3、点击 提交按钮
self.baseDriver.click(*caseData['finishlistBtn'])
# 2.4、回到我的,利用系统返回键
time.sleep(1)
self.baseDriver.new_keyEvent(4)
# 3、进入每日推荐列表,选取两首歌曲添加到新建的歌单中
with allure.step(caseData['stepName3']):
# 3.1、点击发现按钮
self.baseDriver.click(*caseData['discoveryBtn'])
# 3.2、进入每日推荐
self.baseDriver.click(*caseData['dailyRecommend'])
# 3.3、前三首歌曲添加到测试歌单
# 获取前三首歌曲的添加操作菜单按钮,然后重复添加歌曲过程
time.sleep(2)
print('开始选择两首歌曲')
options = self.baseDriver.getElements(*caseData['songNames'])[3:5] # 列表前两个元素,可以通过切片ele[:3] (切片选取)
for option in options:
# 点击菜单
print("开始执行循环")
time.sleep(2)
option.click()
time.sleep(2)
self.baseDriver.click(*caseData['collectSong'])
# print("点击测试歌单")
time.sleep(1)
self.baseDriver.click(*caseData['listBtn'])
time.sleep(1)
# 4 查看添加的歌曲
with allure.step(caseData['stepName4']):
# 4.1、返回到 发现 页面
self.baseDriver.new_keyEvent(4)
# 4.2、进入 我的 页面
self.baseDriver.click(*caseData['myPage'])
# 4.3、 点击 测试歌单001
time.sleep(1)
self.baseDriver.click(*caseData['checkList'])
time.sleep(2)
# 4.4、 查看歌曲,获取所有歌名
songs = self.baseDriver.getElements(*caseData['checkSong'])
time.sleep(5)
print('歌单内的歌曲信息是:')
for song in songs:
print(song.text)
# 5、删除创建的歌单——测试歌单002
with allure.step(caseData['stepName5']):
# 5.1 返回
self.baseDriver.new_keyEvent(4)
time.sleep(3)
# 5.2 点击第一个新建的歌单的选线——测试歌单002
self.baseDriver.click(*caseData['findList'])
time.sleep(1)
# 5.3 点击删除选项
self.baseDriver.click(*caseData['deleteBtn'])
# 5.4 点击删除确认弹框上的删除按钮
time.sleep(2)
self.baseDriver.click(*caseData['confirmBtn'])
@allure.story('将失败用例进行截图:歌单列表中没有该歌单') # 同一个分组,比如成功和失败
@pytest.mark.parametrize('data', read_yaml('checkList'))
def test_checkListName(self, data):
self.log.info("执行---test_checkListName------")
allure.dynamic.title(data['title']) # allure:获取yaml文件中的title
allure.description(data['description']) # allure: 获取yaml文件中的description
print('获取data的title数据:------', data['title'])
caseData = data['caseDatas']
# 1.进入“我的”页面
with allure.step(caseData['stepName1']):
self.baseDriver.click(*caseData['myBtn'])
# 2、检核个订单列表中是否有该歌单
with allure.step(caseData['stepName2']):
print('进入歌单列表页')
try:
listNames = self.baseDriver.getElements(*caseData['listNames'])
tempList = []
for listName in listNames:
tempList.append(listName.text)
print(tempList)
assert 'music' in tempList # 失败用例
except AssertionError:
time.sleep(1)
print('执行失败:执行截图操作')
self.baseDriver.getImage('失败用例的截图') # 添加失败截图的功能
assert False
else:
print("没有捕捉到异常")
assert True
5、run层
# !/usr/bin python3
# encoding : utf-8 -*-
# @author : Shan Shan
# @software : PyCharm
# @file : run.py
# @Time : 2021/6/8 16:21
import os
import pytest
# pytest.main(["test_cases/test_procedure002.py", '-s'])
pytest.main(['-s', 'test_cases/test_app.py', '--alluredir', './temp']) # 生成allure报告,并放在./temp
os.system('allure generate ./temp -o ./reports --clean')