文章目录
一、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、断言 验证测试点是否与预期一致
- 1、操作步骤:里面的操作是属于哪些页面=调用这页面类里面的方法
- 用例1:
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 data
class 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}存在!!!")
raise
def 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)
raise
return el
def 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)
raise
def 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)
raise
def 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)
raise
def 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_handles
print(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 = 0
meta_state = None
flag = None
for k,v in key.items():
if k == "key":
real_value += v
key_code = data.get_yaml_data("key_code.yaml",v)
if k == "flag":
flag = v
if k == "meta":
meta_state = v
self.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 jsonpath
def 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 time
class FrameLogger:
def get_logger(self):
# 创建日志器
logger = logging.getLogger("logger")
# 日志输出当前级别及以上级别的信息,默认日志输出最低级别是warning
if 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 webdriver
def 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 server
driver = 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 locs
class 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 sleep
from 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.smoke
def 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 testcases
user: user testcases
run.py
import os
import time
import pytest
if __name__ == "__main__":
pytest.main()
time.sleep(1)
os.system("allure generate outputs/logs ‐o outputs/reports --clean")