移动端自动化测试:python+appium+pytest+allure+yaml

应用场景:安卓版移动端的自动化测试;以网易音乐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')

6、执行后,allure报告
在这里插入图片描述

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值