Appium+Python+pytest自动化测试框架的实战

news2024/12/25 13:35:10

本文主要介绍了Appium+Python+pytest自动化测试框架的实战,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

先简单介绍一下目录,再贴一些代码,代码里有注释

Basic目录下写的是一些公共的方法,Data目录下写的是测试数据,image存的是测试失败截图,Log日志文件,Page测试的定位元素,report测试报告,Test测试用例,pytest.ini是pytest启动配置文件,requirements.txt需要安装的py模块,run.py运行文件


Basic/base.py

里面封装了 一些方法,元素的点击,输入,查找,还有一些自己需要的公共方法也封装在里面,如果你们有别的需要可以自己封装调用

# coding=utf-8
import random
import allure
import pymysql
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from Basic import Log
import os

log = Log.MyLog()


class Base(object):
    def __init__(self, driver):
        self.driver = driver

    # 自定义一个元素查找方法
    def find_element(self, feature,timeout=5, poll=1.0):
        # feature = By.XPATH,"//*[@text='显示']"
        """
        依据用户传入的元素信息特征,然后返回当前用户想要查找元素
        :param feature: 元组类型,包含用户希望的查找方式,及该方式对应的值
        :return: 返回当前用户查找的元素
        """
        by = feature[0]
        value = feature[1]
        wait = WebDriverWait(self.driver, timeout, poll)
        if by == By.XPATH:
            # print( "说明了用户想要使用 xpath 路径的方式来获取元素" )
            value = self.make_xpath(value)
        return wait.until(lambda x: x.find_element(by,value))

    def find_elements(self, feature):
        wait = WebDriverWait(self.driver, 5, 1)
        return wait.until(lambda x: x.find_elements(feature[0], feature[1]))

    def click_element(self, loc):
        '''
            封装点击操作函数
        '''
        self.find_element(loc).click()

    def input_text(self, loc, text):
        '''
            封装输入操作函数
        '''
        self.fm = self.find_element(loc)
        self.fm.clear()  # 需要先清空输入框,防止有默认内容
        self.fm.send_keys(text)

    # 自定义了一个可以自动帮我们拼接 xpath 路径的工具函数
    def make_xpath(self, feature):
        start_path = "//*["
        end_path = "]"
        res_path = ""

        if isinstance(feature, str):

            # 如果是字符串 我们不能直接上来就拆我们可以判断一下它是否是默认正确的 xpath 写法
            if feature.startswith("//*["):
                return feature

            # 如果用户输入的是字符串,那么我们就拆成列表再次进行判断
            split_list = feature.split(",")
            if len(split_list) == 2:
                # //*[contains(@text,'设')]
                res_path = "%scontains(@%s,'%s')%s" % (start_path, split_list[0], split_list[1], end_path)
            elif len(split_list) == 3:
                # //[@text='设置']
                res_path = "%s@%s='%s'%s" % (start_path, split_list[0], split_list[1], end_path)
            else:
                print("请按规则使用")
        elif isinstance(feature, tuple):
            for item in feature:
                # 默认用户在元组当中定义的数据都是字符串
                split_list2 = item.split(',')
                if len(split_list2) == 2:
                    res_path += "contains(@%s,'%s') and " % (split_list2[0], split_list2[1])
                elif len(split_list2) == 3:
                    res_path += "@%s='%s' and " % (split_list2[0], split_list2[1])
                else:
                    print("请按规则使用")
            andIndex = res_path.rfind(" and")
            res_path = res_path[0:andIndex]
            res_path = start_path + res_path + end_path
        else:
            print("请按规则使用")

        return res_path

    def assert_ele_in(self, text, element):
        '''
            封装断言操作函数
        '''
        try:
            assert text in self.find_element(element).text
            assert 0
        except Exception:
            assert 1

    def get_assert_text(self, element):
        ele = self.find_element(element, timeout=5, poll=0.1)
        return ele.text

    # 自定义一个获取 toast内容的方法
    def get_toast_content(self, message):
        tmp_feature = By.XPATH, "//*[contains(@text,'%s')]" % message
        ele = self.find_element(tmp_feature)
        return ele.text

    # 自定义一个工具函数,可以接收用户传递的部分 toast 信息,然后返回一个布尔值,来告诉
    # 用户,目标 toast 到底是否存在
    def is_toast_exist(self, mes):
        # 拿着用户传过来的 message 去判断一下包含该内容的 toast 到底是否存在。
        try:
            self.get_toast_content(mes)
            return True
        except Exception:
            # 如果目标 toast 不存在那么就说明我们的实际结果和预期结果不一样
            # 因此我们想要的是断言失败
            return False

    def get_mysql(self,  table, value):
        '''连接数据库'''
        # 打开数据库连接
        db = pymysql.connect(host='', port=, db=, user='', passwd='', charset='utf8')
        # 使用 cursor() 方法创建一个游标对象 cursor
        cursor = db.cursor()
        try:
            # 使用 execute()  方法执行 SQL 查询
            cursor.execute(value)
            db.commit()
        except Exception as e:
            print(e)
            db.rollback()
        # 使用 fetchone() 方法获取单条数据.
        data = cursor.fetchone()
        # 关闭数据库连接
        db.close()
        return data

    def get_xpath(self, value):
        '''封装获取xpath方法'''
        text = By.XPATH, '//*[@text="%s"]' % value
        return text

    # 自定义一个获取当前设备尺寸的功能
    def get_device_size(self):
        x = self.driver.get_window_size()["width"]
        y = self.driver.get_window_size()["height"]
        return x, y

    # 自定义一个功能,可以实现向左滑屏操作。
    def swipe_left(self):
        start_x = self.get_device_size()[0] * 0.9
        start_y = self.get_device_size()[1] * 0.5
        end_x = self.get_device_size()[0] * 0.4
        end_y = self.get_device_size()[1] * 0.5
        self.driver.swipe(start_x, start_y, end_x, end_y)

    # 自定义一个功能,可以实现向上滑屏操作。
    def swipe_up(self):
        start_x = self.get_device_size()[0] * 1/2
        start_y = self.get_device_size()[1] * 1/2
        end_x = self.get_device_size()[0] * 1/2
        end_y = self.get_device_size()[1] * 1/7
        self.driver.swipe(start_x, start_y, end_x, end_y, 500)

    # 切换到微信
    def switch_weixxin(self):
        self.driver.start_activity("com.tencent.mm", ".ui.LauncherUI")

    # 切换到医生端
    def switch_doctor(self):
        self.driver.start_activity("com.rjjk_doctor", ".MainActivity")

    # 切换到销售端
    def switch_sale(self):
        self.driver.start_activity("com.rjjk_sales", ".MainActivity")

    def switch_webview(self):
        # 切换到webview
        print(self.driver.contexts)
        time.sleep(5)
        self.driver.switch_to.context("WEBVIEW_com.tencent.mm:tools")
        print("切换成功")
        time.sleep(3)

    # 自定义根据坐标定位
    def taptest(self, a, b):
        # 设定系数,控件在当前手机的坐标位置除以当前手机的最大坐标就是相对的系数了
        # 获取当前手机屏幕大小X,Y
        X = self.driver.get_window_size()['width']
        Y = self.driver.get_window_size()['height']
        # 屏幕坐标乘以系数即为用户要点击位置的具体坐标
        self.driver.tap([(a * X, b * Y)])

    # 自定义截图函数
    def take_screenShot(self):
        '''
        测试失败截图,并把截图展示到allure报告中
        '''
        tm = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(time.time()))
        self.driver.get_screenshot_as_file(
            os.getcwd() + os.sep + "image/%s.png" % tm)
        allure.attach.file(os.getcwd() + os.sep + "image/%s.png" %
                           tm, attachment_type=allure.attachment_type.PNG)

    # 自定义随机生成11位手机号
    def create_phone(self):
        # 第二位数字
        second = [3, 4, 5, 7, 8][random.randint(0, 4)]
        # 第三位数字
        third = {
            3: random.randint(0, 9),
            4: [5, 7, 9][random.randint(0, 2)],
            5: [i for i in range(10) if i != 4][random.randint(0, 8)],
            7: [i for i in range(10) if i not in [4, 9]][random.randint(0, 7)],
            8: random.randint(0, 9),
        }[second]
        # 最后八位数字
        suffix = random.randint(9999999, 100000000)
        # 拼接手机号
        return "1{}{}{}".format(second, third, suffix)

Basic/deiver.py
APP启动的前置条件,一个是普通的app,一个是微信公众号,配置微信公众号自动化测试和一般的APP是有点区别的,微信需要切换webview才能定位到公众号

from appium import webdriver


def init_driver():
    desired_caps = {}
    # 手机 系统信息
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '9'
    # 设备号
    desired_caps['deviceName'] = 'emulator-5554'
    # 包名
    desired_caps['appPackage'] = ''
    # 启动名
    desired_caps['appActivity'] = ''
    desired_caps['automationName'] = 'Uiautomator2'
    # 允许输入中文
    desired_caps['unicodeKeyboard'] = True
    desired_caps['resetKeyboard'] = True
    desired_caps['autoGrantPermissions'] = True
    desired_caps['noReset'] = False
    # 手机驱动对象
    driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
    return driver


def driver_weixin():
    desired_caps = {}
    # 手机 系统信息
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '9'
    # 设备号
    desired_caps['deviceName'] = ''
    # 包名
    desired_caps['appPackage'] = 'com.tencent.mm'
    # 启动名
    desired_caps['appActivity'] = '.ui.LauncherUI'
    # desired_caps['automationName'] = 'Uiautomator2'
    # 允许输入中文
    desired_caps['unicodeKeyboard'] = True
    desired_caps['resetKeyboard'] = True
    desired_caps['noReset'] = True
    # desired_caps["newCommandTimeout"] = 30
    # desired_caps['fullReset'] = 'false'
    # desired_caps['newCommandTimeout'] = 10
    # desired_caps['recreateChromeDriverSessions'] = True
    desired_caps['chromeOptions'] = {'androidProcess': 'com.tencent.mm:tools'}
    # 手机驱动对象
    driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)
    return driver

Basic/get_data.py
这是获取测试数据的方法


import os
import yaml


def getData(funcname, file):
    PATH = os.getcwd() + os.sep

    with open(PATH + 'Data/' + file + '.yaml', 'r', encoding="utf8") as f:
        data = yaml.load(f, Loader=yaml.FullLoader)

    # 1 先将我们获取到的所有数据都存放在一个变量当中
    tmpdata = data[funcname]

    # 2 所以此时我们需要使用循环走进它的内心。
    res_arr = list()
    for value in tmpdata.values():
        tmp_arr = list()
        for j in value.values():
            tmp_arr.append(j)

        res_arr.append(tmp_arr)

    return res_arr

Basic/Log.py
日志文件,不多介绍


# -*- coding: utf-8 -*-

"""
封装log方法

"""

import logging
import os
import time

LEVELS = {
    'debug': logging.DEBUG,
    'info': logging.INFO,
    'warning': logging.WARNING,
    'error': logging.ERROR,
    'critical': logging.CRITICAL
}

logger = logging.getLogger()
level = 'default'


def create_file(filename):
    path = filename[0:filename.rfind('/')]
    if not os.path.isdir(path):
        os.makedirs(path)
    if not os.path.isfile(filename):
        fd = open(filename, mode='w', encoding='utf-8')
        fd.close()
    else:
        pass


def set_handler(levels):
    if levels == 'error':
        logger.addHandler(MyLog.err_handler)
    logger.addHandler(MyLog.handler)


def remove_handler(levels):
    if levels == 'error':
        logger.removeHandler(MyLog.err_handler)
    logger.removeHandler(MyLog.handler)


def get_current_time():
    return time.strftime(MyLog.date, time.localtime(time.time()))


class MyLog:
    path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    log_file = path+'/Log/log.log'
    err_file = path+'/Log/err.log'
    logger.setLevel(LEVELS.get(level, logging.NOTSET))
    create_file(log_file)
    create_file(err_file)
    date = '%Y-%m-%d %H:%M:%S'

    handler = logging.FileHandler(log_file, encoding='utf-8')
    err_handler = logging.FileHandler(err_file, encoding='utf-8')

    @staticmethod
    def debug(log_meg):
        set_handler('debug')
        logger.debug("[DEBUG " + get_current_time() + "]" + log_meg)
        remove_handler('debug')

    @staticmethod
    def info(log_meg):
        set_handler('info')
        logger.info("[INFO " + get_current_time() + "]" + log_meg)
        remove_handler('info')

    @staticmethod
    def warning(log_meg):
        set_handler('warning')
        logger.warning("[WARNING " + get_current_time() + "]" + log_meg)
        remove_handler('warning')

    @staticmethod
    def error(log_meg):
        set_handler('error')
        logger.error("[ERROR " + get_current_time() + "]" + log_meg)
        remove_handler('error')

    @staticmethod
    def critical(log_meg):
        set_handler('critical')
        logger.error("[CRITICAL " + get_current_time() + "]" + log_meg)
        remove_handler('critical')


if __name__ == "__main__":
    MyLog.debug("This is debug message")
    MyLog.info("This is info message")
    MyLog.warning("This is warning message")
    MyLog.error("This is error")
    MyLog.critical("This is critical message")



Basic/Shell.py
执行shell语句方法


# -*- coding: utf-8 -*-
# @Time    : 2018/8/1 下午2:54
# @Author  : WangJuan
# @File    : Shell.py

"""
封装执行shell语句方法

"""

import subprocess


class Shell:
    @staticmethod
    def invoke(cmd):
        output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
        o = output.decode("utf-8")
        return o

Page/page.py

class Page:
    def __init__(self, driver):
        self.driver = driver

    @property
    def initloginpage(self):
        return Login_Page(self.driver)


Test/test_login.py
登陆的测试用,我贴一条使用数据文件的用例


class Test_login:
    @pytest.mark.parametrize("args", getData("test_login_error", 'data_error_login'))
    def test_error_login(self, args):
        """错误登陆"""
        self.page.initloginpage.input_user(args[0])
        self.page.initloginpage.input_pwd(args[1])
        self.page.initloginpage.click_login()
        toast_status = self.page.initloginpage.is_toast_exist(args[2])
        if toast_status == False:
            self.page.initpatientpage.take_screenShot()
            assert False


pytest.ini
pytest配置文件,注释的是启动失败重试3次,因为appium会因为一些不可控的原因失败,所有正式运行脚本的时候需要加上这个


[pytest]
;addopts = -s --html=report/report.html --reruns 3
addopts = -s --html=report/report.html
testpaths = ./Test
python_files = test_*.py
python_classes = Test*
python_functions = test_add_prescription_list



requirements.txt
框架中需要的患教,直接pip install -r requirements.txt 安装就可以了,可能会失败,多试几次

```python
adbutils==0.3.4
allure-pytest==2.7.0
allure-python-commons==2.7.0
Appium-Python-Client==0.46
atomicwrites==1.3.0
attrs==19.1.0
certifi==2019.6.16
chardet==3.0.4
colorama==0.4.1
coverage==4.5.3
decorator==4.4.0
deprecation==2.0.6
docopt==0.6.2
enum34==1.1.6
facebook-wda==0.3.4
fire==0.1.3
humanize==0.5.1
idna==2.8
importlib-metadata==0.18
logzero==1.5.0
lxml==4.3.4
more-itertools==7.1.0
namedlist==1.7
packaging==19.0
Pillow==6.1.0
pluggy==0.12.0
progress==1.5
py==1.8.0
PyMySQL==0.9.3
pyparsing==2.4.0
pytest==5.0.0
pytest-cov==2.7.1
pytest-html==1.21.1
pytest-metadata==1.8.0
pytest-repeat==0.8.0
pytest-rerunfailures==7.0
PyYAML==5.1.1
requests==2.22.0
retry==0.9.2
selenium==3.141.0
six==1.12.0
tornado==6.0.3
uiautomator2==0.3.3
urllib3==1.25.3
wcwidth==0.1.7
weditor==0.2.3
whichcraft==0.6.0
zipp==0.5.1

到此这篇关于Appium+Python+pytest自动化测试框架的实战的文章就介绍到这了


最后

如果你想学习自动化测试,那么下面这套视频应该会帮到你很多

如何逼自己1个月学完自动化测试,学完即就业,小白也能信手拈来,拿走不谢,允许白嫖....

最后我这里给你们分享一下我所积累和整理的一些文档和学习资料,有需要直接领取就可以了!


以上内容,对于软件测试的朋友来说应该是最全面最完整的备战仓库了,为了更好地整理每个模块,我也参考了很多网上的优质博文和项目,力求不漏掉每一个知识点,很多朋友靠着这些内容进行复习,拿到了BATJ等大厂的offer,这个仓库也已经帮助了很多的软件测试的学习者,希望也能帮助到你。

​​

​​​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1129032.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【文章学习系列之模型】Koopa

本章内容 文章概况模型结构主要结构实验结果消融实验模型效率分解效果定性分解效果定量算子稳定性 总结 文章概况 《Koopa: Learning Non-stationary Time Series Dynamics with Koopman Predictors》是2023年发表于NeurIPS的一篇论文。考虑到时序预测中训练和推理数据之间甚至…

网工内推 | 网络工程师,大专以上、HCIA认证即可,最高14薪

01 湖南口味王集团 招聘岗位:网络工程师 职责描述: 1、负责园区内电脑日常维护; 2、负责园区内办公周边设备的日常维护,如打印机、投影仪等; 3、负责园区内电话日常维护; 4、负责园区内信息资产管理&#…

vcomp100.dll丢失的解决方法,一键修复vcomp100.dll丢失问题

在计算机使用过程中,我们经常会遇到一些错误提示,其中之一就是“vcomp100.dll丢失”。这个错误通常会导致某些程序无法正常运行,给用户带来困扰。为了解决这个问题,我整理了以下五个解决方法,希望能对大家有所帮助。 一…

Python-----for循环基本语法及其应用---对序列进行遍历循环

for循环基本语法 for循环结构主要用于(序列 :包括 字符串、列表、元组、集合以及字典)类型数据的遍历(循环)操作。 遍历(Traversal),是指沿着某条搜索路线,依次对树(或图&#…

《深入理解java虚拟机 第三版》学习笔记三

第 8 章 虚拟机字节码执行引擎 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 8.1 概述 执行引擎是 Java 虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都…

Python3 + Appium + 安卓模拟器实现APP自动化测试并生成测试报告

这篇文章主要介绍了Python3 Appium 安卓模拟器实现APP自动化测试并生成测试报告,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 正文 一、安装Python3 直接登录Python官网https://www.python.org/&…

C++数据结构X篇_21_插入排序(稳定的排序)

文章目录 1. 插入排序原理2. 算法图解3. 核心代码:4. 插入排序整体代码实现 1. 插入排序原理 插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相…

Unity 自定义小地图

最近工作做了个小地图,再此记录下思路。 1、准备所需素材 显示为地图(我们取顶视图)。创建一个Cube,缩放到可以把实际地图包住。实际地图的尺寸和偏移量 。我这里长宽都是25,偏移量(1,0&…

MySQL进阶(数据库引擎)——MyISAM和InnoDB引擎的区别

1.是否支持行级锁 MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。 (1)MySQL大致可以归纳为以下3种锁: 表级锁:开销小,加锁快;不会出现死锁;锁的粒度大…

C# FileInfo类的使用方法及常用操作(附代码示例)

在C#编程中,处理文件操作是一项常见而重要的任务。为了更好地管理和操作文件,C#提供了一个强大且灵活的FileInfo类。本文将深入探讨C# FileInfo类的使用方法,并为您提供一些实用的代码示例。 目录 一、什么是FileInfo类?二、使用F…

CLion使用SSH远程连接Linux服务器

最近要一直用实验室的服务器写Linux下的C代码, 本来一直用VScode(SSH)连接服务器, 但是我以前还是用JetBrains的IDE用的多, 毕竟他家的IDE代码提示和功能在某些细节上更加丰富。所以这次我使用了Clion里的远程连接(同样也是SSH工具)连接上了我的服务器, 实现了和VScode上同样的…

NOIP2023模拟1联测22 黑暗料理

NOIP2023模拟1联测22 黑暗料理 题目大意 自己看 思路 两个数相加能够产生质数的情况就是:11 或者 偶数质数 那么 1 1 1 不能保留超过一个 建一个图,原点连向所有奇数点,所有偶数点连向汇点,奇数点和偶数点的和为奇数的就相连 …

分布式事务 学习

分布式事务 关系型数据库事务(本地事务) 原子性:构成事务的所有操作,要么都执行完成,要么都不执行/一致性:在事务执行前后,数据库的一致性约束没有被破坏。隔离性:并发的两个事务的…

『第一章』命运的齿轮开始转动:雨燕(Swift)诞生!

在本篇博文中,您将学到如下内容: 1. 破茧成“燕”2. 持续进化!3. Swift 5.0:ABI 稳定性4. Swift 5.1:模块稳定性和库进化5. Swift 5.9 来了6. 登高望远:Swift 6.0总结 雨燕翻新幕,风鹃绕旧枝 金鹊徒为滞,雨…

程序员的新去处?国内新能源公司大汇总!

近几年来,传统互联网企业哀鸿遍野,而新能源车企却在悄然崛起:HC逐年增加,薪资逐渐起飞,年终分红也让人眼红…… 聪明的程序员们已经把目光瞄准了新时代新能源车企,今天就带大家横向对比一下国内比较火热的…

2.7.C++项目:网络版五子棋对战之session模块的设计

文章目录 零、前置一、意义二、功能三、管理四、框架五、完整代码 零、前置 在WEB开发中,HTTP协议是⼀种无状态短链接的协议,这就导致⼀个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登…

24李永乐模拟6套卷中的一道定积分定义+真题改编题

大概率是武忠祥老师出的一道题,同时考察了数形结合,定积分换元,无穷级数表示,广义积分中值定理,定积分定义的一道真题改编好题。 第二问用第一问结论,区间再现一步秒杀,颇有真题风范。

找不到msvcp100.dll解决教程

在计算机使用过程中,我们经常会遇到一些错误提示,其中之一就是“msvcp100.dll丢失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题,本文将介绍四个修复msvcp100.dll丢失的方法,帮助读者快速恢复计算机的正常运…

【c++】运算符重载实例

重载自增自减运算符 Intger num(2); num; num;对自增运算符的重载要区分前置和后置。在重载之前需要思考一个问题,num是返回一个临时变量还是num对象的本体。 为了解决这个问题可以考虑实现一个Inc_()函数和_Inc()函数分别模仿后置和前置的行为 Integer Inc_(){i…

Calibre拾遗:FDI (Foreign Database Interface)系统简介

Calibre是强大的GDS处理工具,包括查看,验证,分析等操作,操作由浅入深,除过手动编辑GDS的不是很灵活外,其他各种命令和操作策略,都是远(遥)远(遥)走…