从企业实战中学习Appium自动化测试(一)
一.WlrPage
连接体外程控器页面
class WlrPage(SRActivity):#界面的“门牌号”#面向对象继承——类属性覆盖Activity = '.modules.connectcontroller.ActConnectController'def __init__(self, sr_app: SR1620App):super().__init__(sr_app)#控件嵌套字典#借助父类提供的控件容器,存放自己定义的控件''' 而这就是父类构造方法中提供的基础资源,所以必须使用super()去完成初始化,不然没法调用super.update_locator去存放自己定义的控件'''self.update_locator({ '查找体外程控器按钮': {'by': SRBy.ID, 'value': 'id/tv_scan'},'体外程控器列表': {TYPE: SRScrollView, ITEM: '@体外程控器编号', 'by': SRBy.ID, 'value': 'id/rv_bt_list'},'体外程控器编号': {'by': SRBy.XPATH, 'value': '(.//android.widget.TextView[1])'},'体外程控器连接状态': {'by': SRBy.XPATH, 'value': '(.//android.widget.TextView[2])'},'已连接': {'by': SRBy.ID, 'value': 'id/tv_status'},'演示模式按钮': {'by': SRBy.ID, 'value': 'id/cb_yanshi'},'程控记录按钮': {'by': SRBy.ID, 'value': 'id/frame_record'},'配对按钮': {'by': SRBy.ID, 'value': 'id/frame_pair'},'设置按钮': {'by': SRBy.ID, 'value': 'id/frame_setting'},})# 点击“查找体外程控器”按钮def search_wlr(self):while self.controls['查找体外程控器按钮'].text == '查找体外程控器':time.sleep(1)self.controls['查找体外程控器按钮'].click()# 打开演示模式def search_wlr_demo(self):self.controls['演示模式按钮'].click()self.search_wlr()#连接体外程控器def conn_wlr(self, wlr: str):self.search_wlr()timeout = 60time0 = time.time()while time.time() - time0 < timeout:time.sleep(2)for sr_view in self.controls['体外程控器列表']:if sr_view.text == wlr:sr_view.click()try:sr_view.click()except:passreturnraise ControlNotFoundError(f"连接体外程控器: {wlr}失败,未找到对应体外程控器")#进入配对页面def enter_pair_page_with_conn(self):if self.controls['已连接'].exist():time.sleep(2)self.controls['配对按钮'].click()def enter_pair_page_without_conn(self, wlr):passdef get_wlr_list(self) -> list:wlr_list = []for item in self.controls['体外程控器列表']:wlr_list.append(item.text)return wlr_list
(一)代码讲解
这段代码讲解:
(二)知识点
1:Activity是什么?/面向对象 继承中的“类属性覆盖”方式
使用Activity属性标识界面
当前这个页面类(WlrPage)所对应的 Android 界面(体外程控器连接界面)
2.继承
为什么要调用super()方法?
答:子类的实现依赖于父类构造方法中所提供的基础资源,使用
super()
确保父类先完成初始化,避免子类因缺少依赖报错。
为什么子类不直接使用父类的构造方法还自己去定义呢?
答:父类的构造方法是 “通用基础款”,满足不了子类的 “个性化需求”
二.SRActivity
界面操作基类,封装了所有页面的共同需要的基础操作方法和定义了一些通用操作规则,其他类继承了这个类就可以进行页面基础操作了,不用每个页面都重复写
'''
界面操作基类,封装了页面一些基础的操作方法。其他类继承了这个类,就可以执行操作了
'''
class SRActivity(object):Activity = '' def __init__(self, ar_app: SRApp, wait_activity=True, **kwargs):self._locators = {}#"页面管家"和“应用管家”建立资源连接self._app = ar_appself._driver: WebDriver = ar_app.web_driver#从应用获取自动化驱动,供页面操作控件(点击、输入等)self._wait_activity = wait_activityif wait_activity:self._wait_timeout = kwargs.get('wait_timeout') # 从关键字参数中获取超时时间self._wait_interval = kwargs.get('wait_interval') # 从关键字参数中获取轮询间隔# 若未传入超时时间,默认设为30秒if not self._wait_timeout:self._wait_timeout = 30# 若未传入轮询间隔,默认设为1秒if not self._wait_interval:self._wait_interval = 1#调用WebDriver的wait_activity方法,传入三个参数#持续检查当前页面是否为self.Activity指定的页面,直到页面出现(返回True)或超时(返回False)rtn = self._driver.wait_activity(self.Activity, self._wait_timeout, self._wait_interval)if not rtn:raise RuntimeError(f'进入{self.__class__.__name__}页面失败')@property def controls(self):'''让 “找控件” 的操作 更直观、更好懂'''#返回当前页面实例return selfdef has_control_key(self, control_key):'''是否包含控件control_key:rtype: boolean'''return control_key in self._locators'''“隐藏技能”:只要你知道按钮的名字(比如 “登录按钮”),直接用 页面['按钮名'] 就能找到它。定义了这样的形式来找到控件'''def __getitem__(self, index):'''获取index指定控件:type index: string:param index: 控件索引,如'查找按钮''''if index not in self._locators.keys():raise NameError("%s没有名为'%s'的子控件!" % (type(self), index))params = self._locators[index].copy()if params[BY] == SRBy.ID:if not params[VALUE].startswith('android:id/'):params[VALUE] = f"{self._app.PackageName}:{params[VALUE]}"if ROOT in params:root = params[ROOT][1:]# if '_instance' in self._locators[root]:# params[ROOT] = self._locators[root]['_instance']# else:params[ROOT] = self[root]if ITEM in params:item_name = params.pop(ITEM)item_locator = self._locators[item_name[1:]]params['item_by'] = item_locator.get(BY)params['item_value'] = item_locator.get(VALUE)if '_instance' in params:params.pop('_instance')if TYPE in params:ViewType = params.pop(TYPE)instance = ViewType(web_driver=self._driver, **params)else:instance = SRView(web_driver=self._driver, **params)self._locators[index]['_instance'] = instancereturn instancedef update_locator(self,locators):'''更新控件定义'''self._locators.update(locators)def get_locator(self, control_key: str) -> ():by = self._locators.get(control_key)['by']value = self._locators.get(control_key)['value']if by == SRBy.ID:value = f"{self._app.PackageName}:{value}"return by, valuedef wait_for_exist(self, timeout=10, interval=0.5):'''等待窗口出现'''import retime0 = time.time()current_activity = ''if self.Activity == '':return Truepattern = re.compile(self.Activity)while time.time() - time0 < timeout:current_activity = self._driver.current_activity# self._driver.wait_activity()if current_activity == self.Activity:return Trueif current_activity and pattern.match(current_activity):return Truetime.sleep(interval)raise ControlNotFoundError('窗口:%s 未找到,当前窗口为:%s' % (self.__class__.Activity, current_activity))@propertydef rect(self):'''窗口区域'''rect = self._driver.get_window_rect()return rect['Left'], rect['Top'], rect['Width'], rect['Height']def close(self):'''关闭窗口'''self._driver.back()timeout = 3time0 = time.time()while time.time() - time0 < timeout:if self._driver.current_activity != self.Activity:returntime.sleep(0.5)def scroll_on_screen(self):'''在屏幕上从下往上滚动半屏todo 可以根据需要改为上下左右滑动'''size = self._driver.get_window_rect()start_x, end_x = size['width'] * 0.5start_y = size['height'] * 0.75end_y = size['height'] * 0.25self._driver.swipe(start_x, start_y, end_x, end_y, duration=1000)def get_app(self):return self._appdef wait_for_disappear(self, control_key: str, wait_timeout=30, wait_interval=0.5):'''等待控件消失,比如loading动画控件等:param control_key::param wait_timeout::param wait_interval::return:'''by, value = self.get_locator(control_key)time0 = time.time()while time.time() - time0 < wait_timeout:try:view = self._driver.find_element(by, value)if not view.is_enabled() or not view.is_displayed():returnexcept (NoSuchElementException, StaleElementReferenceException):returntime.sleep(wait_interval)raise TimeoutError(f'等待控件消失超时:控件[{control_key}]:[{value}]依然存在')def check_ui(self, text: str) -> bool:if not text:return Falseself.update_locator({f'{text}': {BY: SRBy.ANDROID_UIAUTOMATOR, VALUE: f'new UiSelector().text("{text}")'},})return self.controls[text].exist() and self.controls[text].is_display()
(一)知识点
知识点1:**kwargs是什么?
接收任意数量的关键字参数,并打包成一个字典
知识点2:元组和字典?
知识点3: @property装饰器/为什么使用?
把类里的 方法 伪装成 属性—— 调用时不用加
()
,像访问普通变量一样用。让技术实现更贴合业务思维
1.贴合业务语义:
访问控件集合中的某一个控件
2.保持接口稳定,便于扩展
3.简化代码
知识点4:__getitem__方法
三.SRApp
应用级的基类,负责管理整个 APP 的 “全局事务”——
比如启动 / 关闭 APP、控制驱动、处理全局弹窗、管理设备信息等,是自动化的 “根基”
可以把它理解成 “手机里的 APP 总控台”,所有和 APP 整体相关的操作,都由它负责
class SRApp(object):PackageName = ''def __init__(self, app_activity: str, device_serial: str = '', no_reset: bool = True, msgbox_confirm: bool = True):self.web_driver: WebDriver = Noneself._device_id = device_serialdevice_version = ADB.get_device_version(self._device_id)self.caps = {'platformName': 'android','platformVersion': device_version,'appium:deviceName': self._device_id,'appPackage': self.PackageName,'appActivity': app_activity,"appium:newCommandTimeout": 300000,'appium:automationName': 'UiAutomator2','autoGrantPermissions': True,'appium:noReset': no_reset,'appium:forceAppLaunch': True,'appium:shouldTerminateApp': True}self._msgbox_confirm_task = threading.Thread(target=self._check_and_exit, args=(), name='confirm_msgbox')self._exit_event = threading.Event()self._confirm_msgbox = msgbox_confirmself.__step_count = 0def start(self):logger.debug(self.caps)print(self.caps)options = UiAutomator2Options().load_capabilities(self.caps)try:self.web_driver = webdriver.Remote("http://localhost:4723", options=options)except Exception:time.sleep(2)print(traceback.format_exc())self.web_driver = webdriver.Remote("http://localhost:4723", options=options)if self._confirm_msgbox and not self._exit_event.is_set():self._msgbox_confirm_task.start()# 设置全局隐式等待3sself.web_driver.implicitly_wait(2)# # 启动时等待闪屏和权限弹窗def quit(self):self._exit_event.set()if self.web_driver:self.web_driver.quit()def get_driver(self):return self.web_driverdef send_back_key(self):self.web_driver.back()def stop_check_msgbox(self):self._exit_event.set()def app_is_running(self) -> bool:if not self.web_driver:return Falsetry:app_state = self.web_driver.query_app_state(self.PackageName)if app_state == ApplicationState.RUNNING_IN_FOREGROUND:return Trueexcept WebDriverException:return Truereturn Falsedef active_app(self):self.web_driver.activate_app(self.PackageName)def _check_and_exit(self):while not self._exit_event.is_set() \and threading.main_thread().is_alive() \and self.web_driver:self._check_permit()time.sleep(2)def _check_permit(self):text_pattern = '^(完成|去授权|仅在使用中允许|始终允许|同意|允许|稍后再说|拍摄|关闭|关闭应用|暂不|好|好的|确定|' \'确认|安装|下次再说|知道了|忽略|允(\\s){0,2}许|同(\\s){0,2}意|继续|下一步)$'try:el = self.web_driver.find_element(SRBy.ANDROID_UIAUTOMATOR, f'new UiSelector().textMatches("{text_pattern}")')print(f'click [{el.text}] by text')el.click()time.sleep(2)except Exception:passdef _check_ios_permit(self):text_pattern = ['无线局域网与蜂窝网络', '好', '相机', '麦克风']for text in text_pattern:try:self.web_driver.find_element(SRBy.NAME, text).click()print(f'click [{text}] by name')# time.sleep(2)except Exception:try:self.web_driver.find_element(SRBy.ACCESSIBILITY_ID, text).click()print(f'click [{text}] by accessibility id')# time.sleep(2)except Exception:passdef get_device_id(self) -> str:return self._device_iddef _screen_shot(self, case_id='', ipg='', step='', **kwargs):logger.debug(f'case_id:{case_id}, ipg:{ipg}, step:{step}, kwargs:{kwargs}')shoot_dir = get_result_dir()shoot_dir = os.path.join(shoot_dir, case_id)os.makedirs(shoot_dir, exist_ok=True)if ipg:shoot_dir = os.path.join(shoot_dir, ipg)os.makedirs(shoot_dir, exist_ok=True)shoot_file = f'{ self.__step_count}'if step:step = step.replace('\\', '').replace('/', '')shoot_file = f'{step}'if kwargs:for k, v in kwargs.items():if k == 'config' and isinstance(v, str):shoot_dir = os.path.join(shoot_dir, v)os.makedirs(shoot_dir, exist_ok=True)continueshoot_file = f'{shoot_file}_{k}_{v}'assert shoot_file != ''logger.debug(f'shoot_file:{shoot_file}')self.web_driver.get_screenshot_as_file(f'{shoot_dir}/{shoot_file}.png')return f'{shoot_dir}/{shoot_file}.png'def wait_for_activity(self, activity: str):self.web_driver.wait_activity(activity, 30, 1)def get_current_activity(self):return self.web_driver.current_activitydef wait_for_activity_change(self, current_activity: str, timeout=30, interval=0.5):time0 = time.time()while time.time() - time0 < timeout:if current_activity == self.get_current_activity():time.sleep(interval)else:returnraise TimeoutError(f'等待界面切换超时,当前界面为:{self.get_current_activity()}')
(一)知识点
知识点1:面向对象编程的核心思想
类与类之间通过“实例引用”实现协作,把不同的功能拆分到不同的类中,再通过引用把它们串起来,灵活方便维护
class SRActivity(object):def __init__(self, ar_app: SRApp, wait_activity=True, **kwargs):"页面管家"和"应用管家"建立资源连接self._app = ar_appself._driver: WebDriver = ar_app.web_driver
WebDriver:模拟用户在浏览器/移动应用中的操作