嗯,似乎好久没上来写文章了。接近两个月没怎么动笔了,惭愧惭愧! 想想这两个月也没干什么,主要是为了生计。一个公司好不好,最主要是要能生存,有现金流。最近自己最大的体会,就是内卷太厉害,各行各业,各种卷。 OK,废话少说。今天来聊一聊如何搭建一个自动化框架。 老生常谈的话题。 我们写代码的时候,为了方便维护,管理以及扩展啥的,需要搭建一个框架。 那么这个框架该如何搭建呢? 以mobile UI为例,看看需要搭建到什么程度。 首先需要支持不同的平台。Android,IOS都能支持。最好是一套代码共用。 然后模拟器,真机也能够支持。 还能够支持多机同步,异步的跑case. 同步跑可以测兼容性,异步跑可以节省运行的时间。 支持数据驱动; 支持CICD. 看起来似乎复杂,其实也不难。
下面来列一列一些小技巧,这里主要是以appium和pytest为主。 不同路径的调用,当用同一套代码来实现自动化的时候,可以用同一个文件,通过用platfrom这个参数来实现来实现。 也可以同一个文件名,函数名,不同的模块来时间,有点像name space。 Importlib模块与import都可以通过过字符串来导入另外一个模块。 Importlib是python的一个库,通过导入importlib,调用import_module()方法,传入用户想要获取的模块对应的路径字符串,即可获取一个,模块module,module可以调用这个test模块下的所有属性和方法。 import是python的一个内置方法,直接调用import()即可获取一个模块.
定位元素的方法
from selenium.webdriver.common.by import By
import selenium.common
from selenium.webdriver.support.wait import WebDriverWait
import selenium.webdriver.support.expected_conditions as EC
class MobileBy(By):
IOS_PREDICATE = '-ios predicate string'
IOS_UIAUTOMATION = '-ios uiautomation'
IOS_CLASS_CHAIN = '-ios class chain'
ANDROID_UIAUTOMATOR = '-android uiautomator'
ANDROID_VIEWTAG = '-android viewtag'
ANDROID_DATA_MATCHER = '-android datamatcher'
ANDROID_VIEW_MATCHER = '-android viewmatcher'
WINDOWS_UI_AUTOMATION = '-windows uiautomation'
ACCESSIBILITY_ID = 'accessibility id'
IMAGE = '-image'
CUSTOM = '-custom'
class BasicPage(object):
def __init__(self, driver):
self.driver = driver
def find_element(self, *loc):
try:
# 元素可见时,返回查找到的元素;以下入参为元组的元素,需要加*
# WebDriverWait(self.driver, TIME_OUT_S).until()
WebDriverWait(self.driver, WAIT_LONG_TIME_S).until(EC.presence_of_element_located(loc))
return self.driver.find_element(*loc)
except selenium.common.exceptions.NoSuchElementException:
logging.warning('Can not find element: %s' % loc[1])
raise
def find_elements(self, *loc):
try:
# WebDriverWait(self.driver, TIME_OUT_S).until(lambda driver: driver.find_elements(*loc))
WebDriverWait(self.driver, WAIT_LONG_TIME_S).until(EC.visibility_of_element_located(loc))
return self.driver.find_elements(*loc)
except selenium.common.exceptions.NoSuchElementException:
logging.warning('Can not find element: %s' % loc[1])
raise
我们可以用find_element这种形式。
一些手势的处理:
def long_press(self, *loc):
action1 = TouchAction(self.driver)
element = self.find_element(*loc)
action1.long_press(el=element, duration=DURATION_MS).wait(WAIT_TIME_MS).perform()
def press(self, *loc):
action1 = TouchAction(self.driver)
element = self.find_element(*loc)
action1.press(element).perform()
def swipe(self, oritation="up", ratio=0.75, duration=DURATION_MS):
x = self.driver.get_window_size()["width"]
y = self.driver.get_window_size()["height"]
def down():
start_y = 0.2
stop_y = ratio
x1 = int(x * 0.5)
y1 = int(y * start_y)
x2 = int(x * 0.5)
y2 = int(y * stop_y)
self.driver.swipe(x1, y1, x2, y2, duration)
def up():
stop_y = 0.2
start_y = ratio
x1 = int(x * 0.5)
y1 = int(y * start_y)
x2 = int(x * 0.5)
y2 = int(y * stop_y)
self.driver.swipe(x1, y1, x2, y2, duration)
def left():
start_x = 0.2
stop_x = ratio
x1 = int(x * start_x)
y1 = int(y * 0.5)
x2 = int(x * stop_x)
y2 = int(y * 0.5)
self.driver.swipe(x1, y1, x2, y2, duration)
def right():
stop_x = 0.25
start_x = ratio
x1 = int(x * start_x)
y1 = int(y * 0.5)
x2 = int(x * stop_x)
y2 = int(y * 0.5)
self.driver.swipe(x1, y1, x2, y2, duration)
actions = {
"down": down,
"up": up,
"left": left,
"right": right
}[oritation]
actions()
def scroll(self, start_element, stop_element, duration=DURATION_MS):
self.driver.scroll(start_element, stop_element, duration)
元素状态的判断:
def get_text(self, *loc):
try:
words = self.find_element(*loc).text
return words
except:
logging.warning('Can not find element: %s' % loc[1])
return ""
def is_element_exist_in_page(self, text):
if text in self.driver.page_source:
return True
else:
return False
def is_element_exist(self, *loc):
try:
WebDriverWait(self.driver, WAIT_SHORT_TIME_S, 0.5).until(EC.presence_of_element_located(loc))
return True
except:
return False
def is_element_enable(self, *loc):
if self.find_element(*loc).is_enabled() == True:
return True
else:
return False
判断元素是否可用 is_displayed/is_enabled/is_selected 对弹出框的处理:
def is_toast_exist(self, text, poll_frequency=0.1):
'''is toast exist, return True or False
:Agrs:
- driver - 传driver
- text - 页面上看到的文本内容
- timeout - 最大超时时间,默认30s
- poll_frequency - 间隔查询时间,默认0.5s查询一次
:Usage:
is_toast_exist(driver, "看到的内容")
'''
try:
toast_loc = (MobileBy.XPATH, ".//*[contains(@text,'%s')]" % (text))
WebDriverWait(self.driver, TIME_OUT_S, poll_frequency).until(EC.presence_of_element_located(toast_loc))
return True
except:
return False
def get_toast_text(self, timeout=20, poll_frequency=0.1):
'''
定位toast元素,获取text属性
:param driver: driver实例对象
:param timeout: 元素定位超时时间
:param poll_frequency: 查询频率
:return: toast文本内容
'''
toast_loc = (By.XPATH, '//*[@class="android.widget.Toast"]')
try:
toast = WebDriverWait(self.driver, timeout, poll_frequency).until(EC.presence_of_element_located(toast_loc))
toast_text = toast.get_attribute('text')
return toast_text
except Exception as e:
return e
def accept_dialouge(self):
'''
if dialouge has allow
'''
self.driver.switch_to.alert.accept()
def accept_pop_up(self, *loc):
'''
if dialouge has OK or accept, Continue, can be locate easy
'''
try:
time.sleep(WAIT_LITTLE_TIME_S)
loc = self.find_element(*loc)
if loc:
loc.click()
else:
pass
except:
pass
def accept_permission_popup(self, text):
'''
if dialouge has accept or other, hard to be located but need to use text
'''
for i in range(MAX_TRY_TIMES):
loc = (MobileBy.XPATH, "//*[@text='{}']".format(text))
try:
e = WebDriverWait(self.driver, WAIT_SHORT_TIME_S, 0.5).until(EC.presence_of_element_located(loc))
e.click()
except:
pass
一些基本操作:
def relaunch_app(self):
self.driver.background_app(WAIT_LITTLE_TIME_S)
def set_orientation(self, orientation="LANDSCAPE"):
if self.get_current_orientation() != "LANDSCAPE" and orientation == "LANDSCAPE":
self.driver.orientation = "LANDSCAPE"
if self.get_current_orientation() != "PORTRAIT" and orientation == "PORTRAIT":
self.driver.orientation = "PORTRAIT"
def set_random_orientation(self):
if self.get_current_orientation() == "LANDSCAPE":
self.driver.orientation = "PORTRAIT"
if self.get_current_orientation() == "PORTRAIT":
self.driver.orientation = "LANDSCAPE"
def get_current_orientation(self):
orientation = self.driver.orientation
return orientation
def current_activity(self): # this function only support for Android
activity = self.driver.current_activity
return activity
def wait_activity(self, activity, timeout, interval=1):
"""
Wait for an activity: block until target activity presents
or time out.
This is an Android-only method.
:Agrs:
- activity - target activity
- timeout - max wait time, in seconds
- interval - sleep interval between retries, in seconds
"""
try:
WebDriverWait(self, timeout, interval).until(self.current_activity == activity)
return True
except:
return False
def current_context(self):
context = self.driver.current_context
return context
def all_contexts(self):
contexts = self.driver.contexts
return contexts
def switch_to_webview(self):
webview = self.all_contexts()[1]
self.driver.switch_to.context(webview)
def switch_to_native(self):
self.driver.switch_to.context('NATIVE_APP')
自己定义的一些个方法:
def navigate(self, locate="home"):
home_loc = {
"android": (MobileBy.ID, "main_tab_id_home"),
"ios": (MobileBy.IOS_PREDICATE, 'label == "Home"')
}[plantform]
lesson_loc = {
"android": (MobileBy.ID, "main_tab_id_course"),
"ios": (MobileBy.IOS_PREDICATE, 'label == "Lesson"')
}[plantform]
class_loc = {
"android": (MobileBy.ID, "main_tab_id_book"),
"ios": (MobileBy.IOS_PREDICATE, 'label == "Class"')
}[plantform]
me_loc = {
"android": (MobileBy.ID, "main_tab_id_attend"),
"ios": (MobileBy.IOS_PREDICATE, 'label == "Me"')
}[plantform]
change_course_loc = {
"android": (MobileBy.ID, "changeCourse"),
"ios": (MobileBy.IOS_PREDICATE, 'name == "Change course" AND value == "Change course"')
}[plantform]
setting_dot_loc = {
"android": (MobileBy.ID, "settings_icon_expanded"),
"ios": (MobileBy.IOS_PREDICATE, 'label == "extraMenu"')
}[plantform]
def go_to_home_page():
self.find_element(*home_loc).click()
time.sleep(WAIT_LITTLE_TIME_S)
def go_to_lesson_page():
self.find_element(*lesson_loc).click()
time.sleep(WAIT_SHORT_TIME_S)
def go_to_class_page():
self.find_element(*class_loc).click()
time.sleep(WAIT_SHORT_TIME_S)
def go_to_me_page():
self.find_element(*me_loc).click()
time.sleep(WAIT_SHORT_TIME_S)
def go_to_change_course_page():
go_to_lesson_page()
self.find_element(*change_course_loc).click()
time.sleep(WAIT_SHORT_TIME_S)
go_to = {
"home": go_to_home_page,
"lesson": go_to_lesson_page,
"class": go_to_class_page,
"me": go_to_me_page,
"changecourse": go_to_change_course_page
# "settings": go_to_setting_page(),
# "account": go_to_account_page(),
}[locate]
go_to()
conftest.py里面定义一下:
import pytest
import importlib
lesson_action=importlib.import_module("actions.{}.lessonpage".format(plantform))
course_action=importlib.import_module("actions.{}.coursepage".format(plantform))
activity_action=importlib.import_module("actions.{}.activitypage".format(plantform))
driver = None
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
'''
获取每个用例状态的钩子函数
:param item:
:param call:
:return:
'''
# 获取钩子方法的调用结果
outcome = yield
rep = outcome.get_result()
# 仅仅获取用例call 执行结果是失败的情况, 不包含 setup/teardown
if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f:
# let's also access a fixture for the fun of it
if "tmpdir" in item.fixturenames:
extra = " (%s)" % item.funcargs["tmpdir"]
else:
extra = ""
f.write(rep.nodeid + extra + "\n")
# 添加allure报告截图
if hasattr(driver, "get_screenshot_as_png"):
with allure.step('添加失败截图...'):
allure.attach(driver.get_screenshot_as_png(), "失败截图", allure.attachment_type.PNG)
@pytest.fixture(scope="module")
def driver():
global drivers
desired_caps = {}
if plantform == "android":
yaml_path = os.path.join(config_path, "android.yaml")
device_name, plat_version = device_info().get_singal_device_info
desired_caps = get_yaml_data(yaml_path)["capability"]
print(desired_caps)
desired_caps['platformVersion'] = plat_version
desired_caps['deviceName'] = device_name
print(desired_caps)
if plantform == "ios":
yaml_path = os.path.join(config_path, "ios.yaml")
if device_type == "real":
udid = iosdevice().Get_UUID
device_name=iosdevice().Get_Device_Name
device_version=iosdevice().Get_Device_information
desired_caps = get_yaml_data(yaml_path)["real_caps"]
print(desired_caps)
desired_caps['platformVersion'] = device_version
desired_caps['deviceName'] = device_name
desired_caps['udid'] = udid
print(desired_caps)
if device_type == "simulator":
desired_caps = get_yaml_data(yaml_path)["simulator_caps"]
print(desired_caps)
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
yield driver
其他的一些操作,辅助操作,写写好,就一个比较完备的框架就能实现了。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有