07 APP 自动化- appium+pytest+allure框架封装

文章目录

      • 一、PO
      • 二、代码简单实现
        • 项目框架预览:
        • base_page.py
        • dir_config.py
        • get_data.py
        • logger.py
        • start_session.py
        • config.yaml
        • key_code.yaml
        • launch_page_loc.py
        • login_page_loc.py
        • launch_page.py
        • login_page.py
        • test_login.py
        • pytest.ini
        • run.py
      • APP 自动化代码总和

一、PO

PO 分为四层 :base 层、pageobjects 层、testcases 层、testdata 层

  • 第一层 base 层:抽取每个共同的属性及行为进行封装,定义到 basepage 类中
  • 第二层 pageobjects 层: 每个页面定义为一个类,类=属性+页面操作方法
    • 属性:页面元素的定位语句 (By.xx,"‘定位语句’)====> pagelocations
    • 页面操作方法 pageobjects 层
    • web 页面可以展示很多内容 每个页面定位一个页面类 创建 xxxpage.py
    • app 页面展示内容少点 每个页面定位一个页面类 创建 xxxpage.py
  • 第三层:testcases 层 项目覆盖那些流程 unittest/pytest
    • 用例1 用例2 ,用例n… 每个用例创建一个.py文件?并不是
    • 冒烟用例
    • 基于功能模块进行分类管理用例
      • 用例1:
        • 1、操作步骤:里面的操作是属于哪些页面=调用这页面类里面的方法
          实例化页面类.方法()
        • 2、断言 验证测试点是否与预期一致

pytest 和 allure 环境安装参考:用例管理框架pytest之fixtrue,conftest.py,allure报告以及logo定制

二、代码简单实现

项目框架预览:

base_page.py
import os.path
import time
from  appium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from AppFrame.common.logger import FrameLogger as Log
from AppFrame.common import dir_config as dir
import AppFrame.common.get_data as dataclass BasePage:"""BasePage:定义每个页面的相同属性及方法相同属性?获取浏览器驱动对象(数据)相同方法?元素定位、点击、输入...等等操作日志错误截图测试报告"""def __init__(self,driver: webdriver.Remote):self.driver = driver# 等待元素可见def wait_ele_visibility(self,page_name,loc,timeout=15,poll_fre=0.5):try:WebDriverWait(self.driver,timeout,poll_fre).until(EC.visibility_of_element_located(loc))# WebElement对象Log().get_logger().info(f"在[{page_name}]页面,找到元素:{loc}可见")except:Log().get_logger().error(f"在[{page_name}]页面,未找到元素:{loc}可见!!!")raise# 等待元素存在def wait_ele_presence(self,page_name,loc,timeout=15,poll_fre=0.5):try:WebDriverWait(self.driver,timeout,poll_fre).until(EC.presence_of_element_located(loc))Log().get_logger().info(f"在[{page_name}]页面,找到元素:{loc}存在")except:Log().get_logger().error(f"在[{page_name}]页面,未找到元素:{loc}存在!!!")raisedef locator(self,page_name,loc):"""元素定位"""# 参数loc,里面包含两个参数# loc = (By.LINK_TEXT,'登录')# *loc,是把外面括号去掉,变成2个参数print(page_name,loc)try:el = self.driver.find_element(*loc)  # WebElement 对象Log().get_logger().info(f"在[{page_name}]页面,定位到元素:{loc}")except:Log().get_logger().info(f"在[{page_name}]页面,未定位到元素:{loc}!!!")# 失败截图self.save_screenshot(page_name)raisereturn eldef input(self,page_name,loc,value):"""输入"""try:self.wait_ele_visibility(page_name, loc)self.locator(page_name,loc).send_keys(value)Log().get_logger().info(f"在{page_name}页面,元素{loc}输入:{value}!")except:Log().get_logger().error(f"在{page_name}页面,元素{loc}输入失败!")# 失败截图self.save_screenshot(page_name)raisedef click(self,page_name,loc):"""点击"""try:self.wait_ele_visibility(page_name, loc)self.locator(page_name,loc).click()Log().get_logger().info(f"在{page_name}页面,元素{loc}点击成功!")except:Log().get_logger().error(f"在{page_name}页面,元素{loc}点击失败!")# 失败截图self.save_screenshot(page_name)raisedef move_element(self,page_name,loc):"""鼠标键盘操作"""try:self.wait_ele_visibility(page_name, loc)ActionChains(self.driver).move_to_element(self.locator(page_name,loc)).click().perform()Log().get_logger().info(f"在{page_name}页面,鼠标移动到元素:{loc}")except:Log().get_logger().error(f"在{page_name}页面,鼠标移动到元素{loc}失败!")# 失败截图self.save_screenshot(page_name)raisedef save_screenshot(self,img_name):file_name = os.path.join(dir.screenshots_dir, img_name+'.png')self.driver.save_screenshot(file_name)Log().get_logger().info(f"失败截图,截取当前网页,存储的路径:{file_name}")def sleep(self,s):time.sleep(s)def switch_new_win(self):"""切换到最新打开的窗口"""wins = self.driver.window_handlesprint(wins)# 切换最后打开的窗口self.driver.switch_to.window(wins[1])def switch_iframe(self,page_name,loc):"""切换到对应的 iframe"""try:self.wait_ele_visibility(page_name, loc)self.driver.switch_to.frame(self.locator(page_name,loc))Log().get_logger().info(f"在{page_name}页面,切换到 iframe 元素{loc}成功!")except:Log().get_logger().error(f"在{page_name}页面,切换到 iframe 元素{loc}失败!")# 失败截图self.save_screenshot(page_name)raise# appbasepage:# 滑屏操作  上下左右滑def swipe(self,direction,duration=1000):""":param direction:up down left right:param duration::return:"""# 获取整个app屏幕的大小size = self.driver.get_window_size()x = size["width"]y = size["height"]# 左滑if direction.lower() == "left":self.driver.swipe(start_x=x * 0.9, end_x=x * 0.2, start_y=y * 0.9, end_y=y * 0.9, duration=1000)# 右滑if direction.lower() == "right":self.driver.swipe(start_x=x * 0.2, end_x=x * 0.9, start_y=y * 0.9, end_y=y * 0.9, duration=1000)# 上拉if direction.lower() == "up":self.driver.swipe(start_x=x * 0.5, end_x=x * 0.5, start_y=y * 0.2, end_y=y * 0.9, duration=1000)# 下拉if direction.lower() == "down":self.driver.swipe(start_x=x * 0.5, end_x=x * 0.5, start_y=y * 0.9, end_y=y * 0.2, duration=1000)def press_keys(self,page_name,loc,keys:list):"""模拟键盘输入"""try:self.wait_ele_visibility(page_name, loc)self.locator(page_name,loc).click()real_value = ""for key in keys:key_code = 0meta_state = Noneflag = Nonefor k,v in key.items():if k == "key":real_value += vkey_code = data.get_yaml_data("key_code.yaml",v)if k == "flag":flag = vif k == "meta":meta_state = vself.driver.press_keycode(int(key_code), metastate=meta_state, flags=flag)Log().get_logger().info(f"在{page_name}页面,元素{loc}输入:{real_value}!")except:Log().get_logger().error(f"在{page_name}页面,元素{loc}输入失败!")# 失败截图self.save_screenshot(page_name)raise# app 自动化测试 appium
# app 自动化测试+web 自动测试整合 公共类 basepage web自动化 webbasepage  app自动化 appbasepage
# 日志+错误截图+测试报告输出
dir_config.py
import os# 根路径
base_dir = os.path.dirname( os.path.dirname(os.path.abspath(__file__)))
# 用例路径
testcases_dir = os.path.join(base_dir, 'testcases')
# 数据路径
testdata_dir = os.path.join(base_dir, 'testdata')
# 测试报告路径
reports_dir = os.path.join(base_dir, 'outputs/reports')
# 日志路径
logs_dir = os.path.join(base_dir, 'outputs/logs')
# 失败截图
screenshots_dir = os.path.join(base_dir, 'outputs/screenshots')
# 配置路径
config_dir = os.path.join(base_dir, 'config')
get_data.py
import yaml
import os
from AppFrame.common import dir_config as Dir
import jsonpathdef get_yaml_data(file_name, key=None):file_path = os.path.join(Dir.config_dir, file_name)with open(file_path, encoding="utf-8") as yaml_file:data = yaml.load(yaml_file, Loader=yaml.FullLoader)  # 字典类型if key is not None:data = jsonpath.jsonpath(data, f"$..{key}")return data[0]return data
logger.py
from AppFrame.common import dir_config as Dir
import logging
import os
import timeclass FrameLogger:def get_logger(self):# 创建日志器logger = logging.getLogger("logger")# 日志输出当前级别及以上级别的信息,默认日志输出最低级别是warningif not logger.handlers:logger.setLevel(logging.INFO)# 创建控制台处理器----》输出控制台SH = logging.StreamHandler()# 创建文件处理器----》输出文件log_path = os.path.join(Dir.logs_dir, f"log_{time.strftime('%Y%m%d%H%M%S', time.localtime())}.txt")FH = logging.FileHandler(log_path,mode="w",encoding="utf-8")# 日志包含哪些内容    时间  文件  日志级别 :事件描述/问题描述formatter = logging.Formatter(fmt="[%(asctime)s] [%(filename)s] %(levelname)s :%(message)s",datefmt='%Y/%m/%d %H:%M:%S')logger.addHandler(SH)logger.addHandler(FH)SH.setFormatter(formatter)FH.setFormatter(formatter)return logger
start_session.py
import AppFrame.common.get_data as data
from appium.options.android import UiAutomator2Options
from appium import webdriverdef start_appium_session(port=4723, **kwargs):""" 启动会话 """# 获取启动配置参数desired_caps = data.get_yaml_data("config.yaml")# 增加其他启动参数for k, v in kwargs.items():desired_caps[k] = v# 发送命令给 appium serverdriver = webdriver.Remote(f'http://127.0.0.1:{port}',options=UiAutomator2Options().load_capabilities(desired_caps))return driver
config.yaml
'platformName': 'Android'  # 指定操作系统
'platformVersion': '12'  # 指定操作系统版本
'automationName': 'Uiautomator2'  # 默认框架
'deviceName': '127.0.0.1:62001'  # 指定设备名称
'appPackage': 'com.tal.kaoyan'  # 被操作的应用程序包名
'appActivity': 'com.tal.kaoyan.ui.activity.SplashActivity'  # 启动页面
'noReset': 'true'  # true--不重置  false--重置
'app': 'F:\Pycharm\AppAuto\Class\kaoyan_v4.5.3.apk'  # apk文件所在路径
key_code.yaml
#  """
#  KeyEvent 常量官方文档:https://developer.android.com/reference/android/view/KeyEvent#constants
#  按键功能	     KeyCode值	  key值         说明
#
#  数字键0-9	     7 到 16	  0-9           对应数字 0 到 9
#  字母键a-z	     29 到 54	  a-z           对应字母 a 到 z(在 Android 中,大写字母的 KeyCode 与小写相同,但需通过 Meta 键(模拟 Shift) 触发)
#  空格 	         62	          blank         输入空格
#  回车	         66	          enter         确认 / 换行
#  删除	         67	          delete        删除字符(退格键)
#  @             77           @             特殊字符@
#
#  """"blank": "62"
"enter": "66"
"delete": "67"
"@": "77"
"," : "55"
"." : "56"
"~" : "126"
"*" : "15"
"0": "7"
"1": "8"
"2": "9"
"3": "10"
"4": "11"
"5": "12"
"6": "13"
"7": "14"
"8": "15"
"9": "16"
"a": "29"
"b": "30"
"c": "31"
"d": "32"
"e": "33"
"f": "34"
"g": "35"
"h": "36"
"i": "37"
"j": "38"
"k": "39"
"l": "40"
"m": "41"
"n": "42"
"o": "43"
"p": "44"
"q": "45"
"r": "46"
"s": "47"
"t": "48"
"u": "49"
"v": "50"
"w": "51"
"x": "52"
"y": "53"
"z": "54"
launch_page_loc.py
from appium.webdriver.common.appiumby import AppiumBy
# 该页面所有的定位
user_protocol_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/tip_commit')
confirm_permission_loc= (AppiumBy.ID, 'com.tal.kaoyan:id/tv_ok')
login_page_loc.py
from appium.webdriver.common.appiumby import AppiumBy
# 手机号验证码登录/注册页面
phone_number_loc = (AppiumBy.ANDROID_UIAUTOMATOR,'new UiSelector().resourceId("com.tal.kaoyan:id/kylogin_phone_input_phonelayout")')
protocol_phone_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginTreatyCheckboxPhone')
get_code_loc = (AppiumBy.ID, "com.tal.kaoyan:id/kylogin_phone_input_codeget")
code_input_loc = (AppiumBy.ANDROID_UIAUTOMATOR,'new UiSelector().resourceId("com.tal.kaoyan:id/kylogin_phone_input_code")')
login_code_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginCodeLoginBtn')
password_login_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginRegistorcodeAndPassword')# 密码登录页面
account_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginEmailEdittext')
password_loc= (AppiumBy.ID, 'com.tal.kaoyan:id/rtlLoginLayout')
protocol_password_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginTreatyCheckboxPassword')
login_password_loc = (AppiumBy.ID, 'com.tal.kaoyan:id/loginLoginBtn')
launch_page.py
from AppFrame.base.base_page import BasePage
import AppFrame.pagelocators.launch_page_loc as locsclass LaunchPage(BasePage):# 启动各弹窗按钮操作def launch_close_prompt(self):# -------------首次启动后弹窗处理----------------try:# 等待用户协议弹窗元素出现(最多5秒)self.click("启动页面",locs.user_protocol_loc)except Exception:pass  # 未出现弹窗,继续执行后续代码try:# 等待权限弹窗元素出现(最多5秒)self.click("启动页面",locs.confirm_permission_loc)except Exception:pass  # 未出现弹窗,继续执行后续代码
login_page.py
from time import sleepfrom AppFrame.base.base_page import BasePage
import AppFrame.pagelocators.login_page_loc as locs
class LoginPage(BasePage):# 密码登录操作def password_login(self,account,password):# 点击密码登录按钮self.click("登录",locs.password_login_loc)# 输入账户self.input("login",locs.account_loc,account)# 输入密码self.press_keys("登录",locs.password_loc,password)# 同意协议self.click("登录",locs.protocol_password_loc)# 点击登录self.click("登录",locs.login_password_loc)# 手机号验证码登录操作def code_login(self,phone_number,password):# 输入手机号self.press_keys("登录", locs.phone_number_loc, phone_number)# 同意协议self.click("登录", locs.protocol_phone_loc)# 点击获取验证码self.click("登录", locs.get_code_loc)# 等待用户输入code_value = input("请输入手机验证码")self.input("登录",locs.code_input_loc,code_value)# 点击登录self.click("登录", locs.login_code_loc)sleep(2)
test_login.py
# -*- coding: utf-8 -*-
from AppFrame.pageobjects.launch_page import LaunchPage
from AppFrame.pageobjects.login_page import LoginPage
import AppFrame.common.start_session  as start
import allure
import pytest@allure.epic("项目名称:考研帮-app自动化测试")
@allure.feature("模块名称:登录模块")
class TestLogin:@allure.story("用例名称:正确账户密码登录成功")@allure.severity(allure.severity_level.NORMAL)@pytest.mark.user@pytest.mark.smokedef test_login_success(self):driver = start.start_appium_session(noReset = 'false')launch_page = LaunchPage(driver)launch_page.launch_close_prompt()login_page = LoginPage(driver)account = "17719847692"password = [{"key":"h"},{"key":"c"},{"key":"@"},{"key":"1"},{"key":"2"},{"key":"3"},{"key":"4"},{"key":"5"},{"key":"6"}]login_page.password_login(account,password)
pytest.ini
[pytest]
addopts = -vs -m 'smoke' --alluredir=outputs/logs --clean-alluredir
testpaths =  testcases/
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers = smoke: smoke testcasesuser: user testcases
run.py
import os
import time
import pytestif __name__ == "__main__":pytest.main()time.sleep(1)os.system("allure generate outputs/logs ‐o outputs/reports --clean")

APP 自动化代码总和

AppAuto.zip

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

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

相关文章

用户体验升级:表单失焦调用接口验证,错误信息即时可视化

现代前端应用中,表单交互是用户体验的重要组成部分。而表单验证作为其中的核心环节,不仅需要前端的即时反馈,还需要与后端接口联动进行数据合法性校验。本文将详细介绍如何在Vue3中实现表单输入与接口验证的无缝联动,并优雅地展示…

Vue 插槽(Slot)用法详解

插槽(Slot)是Vue中一种强大的内容分发机制,它允许你在组件中定义可替换的内容区域,为组件提供了更高的灵活性和可复用性。本文将全面介绍Vue插槽的各种用法。 1. 基本插槽 基本插槽是最简单的插槽形式,它允许父组件向子组件插入内容。 子组…

C++ 标准模板库(STL)详解文档

C 标准模板库(STL)详解文档 1 前言2 常用容器2.1 内容总览2.2 向量 vector2.2.1 概述2.2.2 常用方法2.2.3 适用场景2.2.4 注意事项 2.3 栈 stack2.3.1 概述2.3.2 常用方法2.3.3 注意事项 2.4 队列 queue2.4.1 概述2.4.2 常用方法2.4.3 注意事项 2.5 优先…

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…

Redis(02)Win系统如何将Redis配置为开机自启的服务

一、引言 Redis 是一款高性能的键值对存储数据库,在众多项目中被广泛应用。在 Windows 环境下,为了让 Redis 能更稳定、便捷地运行,将其设置为系统服务并实现自动启动是很有必要的。这样一来,系统开机时 Redis 可自动加载&#xf…

apex新版貌似移除了amp从源码安装方式装的话会在from apex import amp时报错

问题: 安装完apex结果 from apex import amp会报错 解决方法: # apex git clone https://github.com/NVIDIA/apex cd apex # https://github.com/modelscope/ms-swift/issues/4176 git checkout e13873debc4699d39c6861074b9a3b2a02327f92 pip insta…

掌握 HTTP 请求:理解 cURL GET 语法

cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…

【AI学习】三、AI算法中的向量

在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…

leetcode题解450:删除BST中的结点!调整二叉树的结构最难!

一、题目内容 题目要求删除二叉搜索树(BST)中值为 key 的节点,并保证删除后二叉搜索树的性质不变。返回删除节点后的二叉搜索树的根节点的引用。一般来说,删除节点可分为两个步骤:首先找到需要删除的节点;如…

让 Kubernetes (K8s) 集群 使用 GPU

要让 Kubernetes (K8s) 集群 使用 GPU,并且节点是 KVM 虚拟化 出来的,需要确保以下几点: KVM 虚拟机透传 GPU(PCIe Passthrough) 宿主机和 K8s 节点正确安装 NVIDIA 驱动 K8s 集群安装 nvidia-device-plugin Pod 配…

Android第十七次面试总结(Java数据结构)

一、Java 集合体系核心架构与高频考点 1. 集合体系架构图 Java集合框架 ├─ Collection(单列集合) │ ├─ List(有序、可重复) │ │ ├─ ArrayList(动态数组,随机访问快) │ │ ├─…

Linux 删除登录痕迹

本文介绍相对彻底的删除 Linux 的登录痕迹,操作前确保已经可以拿到能提权ROOT令牌的系统管理权限。 当然,仍可以先查阅以下文章。 Linux 删除用户终端命令行操作记录-CSDN博客 1、清楚当前会话记录 history -c # 清空当前终端内存中的历史命令 2、永…

Lighttpd 配置选项介绍

根据提供的 Lighttpd 配置选项文档(https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ConfigurationOptions ),以下是所有配置选项的详细解释、作用及适用场景,按模块分组说明: 以下是对 Lighttpd 配置选项 …

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…

Python 训练营打卡 Day 40-训练和测试的规范写法

一.单通道图片的规范写法 以之前的MNIST数据集为例 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader , Dataset # DataLoader 是 PyTorch 中用于加载数据的工具 from torchvision import datasets, transforms # t…

Java 枚举(Enum)的使用说明

在 Java 中,枚举(Enum)是一种特殊的数据类型,用于定义一组固定的命名常量。它比传统的常量(如 public static final)更安全、更灵活,且支持面向对象特性。以下是枚举的详细用法: 1. …

Docker部署Nginx-UI

使用如下命令拉取运行nginx-ui软件 docker run -dit \ --namenginx-ui \ --restartalways \ -e TZAsia/Shanghai \ -v /mnt/user/appdata/nginx:/etc/nginx \ -v /mnt/user/appdata/nginx-ui:/etc/nginx-ui \ -v /var/run/docker.sock:/var/run/docker.sock \ -…

OkHttp 3.0源码解析:从设计理念到核心实现

本文通过深入分析OkHttp 3.0源码,揭示其高效HTTP客户端的实现奥秘,包含核心设计理念、关键组件解析、完整工作流程及实用技巧。 一、引言:为什么选择OkHttp? 在Android和Java生态中,OkHttp已成为HTTP客户端的标准选择…

洛谷P12170 [蓝桥杯 2025 省 Python B] 攻击次数

题目传送门 思路 首先定义一个数 n n n ,初值为 2025 2025 2025,从第一回合开始,三个英雄持续攻击,攻击方式为: 第一个英雄: 每回合攻击 5 5 5