AppUI自动化实战封装
1、新建一个项目
2、在项目中写两条用例:
启动雪球-进入我的页面进入登录-登录成功后获取登录名验证是否成功登录
import time from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction # 初始化app desired_caps = {} desired_caps['platformName'] = 'Android' # 'Android' or ’Ios' desired_caps['deviceName'] = 'emulator-5554' # 设备名称,可以自定义 # desired_caps['appPackage'] = 'com.iodkols.onekeylockscreen' # desired_caps['appActivity'] = '.OneLock' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' #desired_caps['noReset'] = 'True' driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() driver.implicitly_wait(10) time.sleep(8) #在首页进入【我的】页面 driver.find_element(By.XPATH, "//*[@text='我的']").click() #在【我的】页面点击【账号密码登录】进入登录页面 driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() #在登录页面输入账号 driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys('13262506790') #在登录页面输入密码 driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') #在登录页面点击【登录】 driver.find_element(By.XPATH, "//*[@text='登录']").click() # 对一键导入弹框进行判断处理 try: cancel_ele = driver.find_element(By.XPATH, "//*[@text='取消']").click() except: pass #登录后在我的页面获取登录昵称 name_ele = driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") print(name_text) assert '思源_smile' == name_text driver.quit()启动雪球-进行登录-在首页进入讨论帖页面-编辑并发布帖子-进入我的页面校验帖子是否成功生成
import time from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction # 初始化app desired_caps = {} desired_caps['platformName'] = 'Android' # 'Android' or ’Ios' desired_caps['deviceName'] = 'emulator-5554' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' #desired_caps['noReset'] = 'True' driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() driver.implicitly_wait(10) time.sleep(8) #在首页进入【我的】页面 driver.find_element(By.XPATH, "//*[@text='我的']").click() #在【我的】页面点击【账号密码登录】进入登录页面 driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() #在登录页面输入账号 driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys('13262506790') #在登录页面输入密码 driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') #在登录页面点击【登录】 driver.find_element(By.XPATH, "//*[@text='登录']").click() #登录后进入雪球首页 driver.find_element(By.XPATH, "//*[@text='雪球']").click() #在首页点击【编辑】按钮 driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/post_status']").click() #点击【发讨论】按钮进入发布讨论页面 driver.find_element(By.XPATH, "//*[@text='发讨论']").click() #在发布讨论页面输入框中输入讨论内容 title_text = "随着气候变暖,今年会不会有一个寒冷的冬天?" driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/status_edit']").send_keys(title_text) #点击【发布】返回首页 driver.find_element(By.XPATH, "//*[@text='发布']").click() #进入【我的】页面 driver.find_element(By.XPATH, "//*[@text='我的']").click() #在【我的】页面点击【讨论】进入讨论页面 driver.find_element(By.XPATH, "//*[@text='讨论']").click() #在讨论页面获取讨论标题元素 eles = driver.find_elements(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/statusText']") #对标题元素获取text属性存入列表 title_list = [ele.get_attribute("text") for ele in eles] # 写个函数对结果进行断言 def assert_title(): for title in title_list: if title == title_text: return True else: return False assert assert_title() driver.quit()处理用例中公共的部分app初始化,对公共部分单独封装实现模块化及分层复用
3、对初始化app代码单独封装
新建py_page包-包下新建app模块,定义一个App类,类下定义一个start()实例方法对driver进行初始化并返回创建的driver
import time from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction class App: def start(self): # 初始化app desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['deviceName'] = 'emulator-5554' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' self.driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() self.driver.implicitly_wait(10) return self.driver在测试用例中app初始化通过from py_page.app import App及driver = App().start()来代替,即实现所有用例共用app中的初始化代码;
4、比较多个用例存在公共页面元素交互部分,把这部分元素的定位与交互单独封装,以页面为模块归类,类中方法为在该页面执行的元素交互;
from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class LoginPage(App): def __init__(self): self.driver = self.start() def account_login(self): # 在登录页面进行账号密码登录 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() self.driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys( '13262506790') self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') self.driver.find_element(By.XPATH, "//*[@text='登录']").click()from appium.webdriver.common.mobileby import MobileBy as By class MainPage(App): def __init__(self): self.driver = self.start() def goto_myPage(self): # 在首页进入【我的】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click()from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MyPage(App): def __init__(self): self.driver = self.start() def goto_login(self): # 在我的页面进入【登录】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() def is_cancel(self): # 对一键导入弹框进行判断处理 try: self.driver.find_element(By.XPATH, "//*[@text='取消']").click() except: pass def get_login_name(self): # 登录后在我的页面获取登录昵称 name_ele = self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") return name_text5、在修改原来测试用例,采用调用py_page中各方法来实现测试业务的流转执行
from py_page.main_page import MainPage class TestLogin: def test_login_pos(self): name_text = MainPage().goto_myPage().goto_login().account_login().is_cancel().get_login_name() print(name_text) assert name_text == '思源_smile'似乎存在什么问题?
优化上述py_page保证链式调用
from appium.webdriver.common.mobileby import MobileBy as By class MainPage(App): def __init__(self): self.driver = self.start() def goto_myPage(self): # 在首页进入【我的】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() from py_page.my_page import MyPage return MyPage()from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MyPage(App): def __init__(self): self.driver = self.start() def goto_login(self): # 在我的页面进入【登录】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() return LoginPage() def is_cancel(self): # 对一键导入弹框进行判断处理 try: self.driver.find_element(By.XPATH, "//*[@text='取消']").click() except: pass return self def get_login_name(self): # 登录后在我的页面获取登录昵称 name_ele = self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") return name_textfrom py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class LoginPage(App): def __init__(self): self.driver = self.start() def account_login(self): # 在登录页面进行账号密码登录 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() self.driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys( '13262506790') self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') self.driver.find_element(By.XPATH, "//*[@text='登录']").click() from py_page.my_page import MyPage return MyPage()上述代码似乎还存在问题,在测试用例的链式调用中,涉及页面跳转的每次调用页面类都会调用app初始化一个driver,而不是传递原来的driver;
初步解决方案,每个页面类在构造方法中对driver进行判断,并在涉及页面跳转的方法中return 页面类(self.driver),即把当前页面的driver作为参数传入要跳转的页面,需要引入单例设计
from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MainPage(App): def __init__(self,driver=None): if driver == None: self.driver = self.start() else: self.driver = driver def goto_myPage(self): # 在首页进入【我的】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() from py_page.my_page import MyPage return MyPage(self.driver)from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MyPage(App): def __init__(self,driver=None): if driver == None: self.driver = self.start() else: self.driver = driver def goto_login(self): # 在我的页面进入【登录】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() from py_page.login_page import LoginPage return LoginPage(self.driver) def is_cancel(self): # 对一键导入弹框进行判断处理 try: self.driver.find_element(By.XPATH, "//*[@text='取消']").click() except: pass return self def get_login_name(self): # 登录后在我的页面获取登录昵称 name_ele = self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") return name_textfrom py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class LoginPage(App): def __init__(self,driver=None): if driver==None: self.driver = self.start() else: self.driver=driver def account_login(self): # 在登录页面进行账号密码登录 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() self.driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys( '13262506790') self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') self.driver.find_element(By.XPATH, "//*[@text='登录']").click() from py_page.my_page import MyPage return MyPage(self.driver)再次执行测试用例,即可实现完整的元素定位交互以及页面跳转
6、继续优化py_page中的代码,把页面类中的构造方法封装到app中,减少代码冗余
app模块的初步实现
from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction class App: def __init__(self,driver=None): self.driver = driver def start(self): # 初始化app if self.driver==None: desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['deviceName'] = 'emulator-5554' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' #desired_caps['noReset'] = 'True' self.driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() self.driver.implicitly_wait(10) return self.driverfrom py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MainPage(App): def goto_myPage(self): # 在首页进入【我的】页面 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() from py_page.my_page import MyPage return MyPage(self.driver)from py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class MyPage(App): def goto_login(self): # 在我的页面进入【登录】页面 self.driver.find_element(By.XPATH, "//*[@text='账号密码登录']").click() from py_page.login_page import LoginPage return LoginPage(self.driver) def is_cancel(self): # 对一键导入弹框进行判断处理 try: self.driver.find_element(By.XPATH, "//*[@text='取消']").click() except: pass return self def get_login_name(self): # 登录后在我的页面获取登录昵称 name_ele = self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") return name_textfrom py_page.app import App from appium.webdriver.common.mobileby import MobileBy as By class LoginPage(App): def account_login(self): # 在登录页面进行账号密码登录 self.driver.find_element(By.XPATH, "//*[@text='我的']").click() self.driver.find_element(By.XPATH, "//*[@text='帐号密码登录']").click() self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']").send_keys( '13262506790') self.driver.find_element(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']").send_keys('1111111a') self.driver.find_element(By.XPATH, "//*[@text='登录']").click() from py_page.my_page import MyPage return MyPage(self.driver)from py_page.main_page import MainPage class TestLogin: def test_login_pos(self): name_text = MainPage().goto_myPage().goto_login().account_login().is_cancel().get_login_name() print(name_text) assert name_text == '思源_smile'基于上面代码修改app后原测试用例会报错,想一想这是为什么?
因为测试用例中调用MainPage()时由于继承了app,会调用app中的构造方法初始化一个driver=None,但goto_myPage()中有self.driver.find_element(By.XPATH,"//*[@text='我的']").click(),然而这个driver是一个None自然没有find_element这个方法
修改方案如下
from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction class App: def __init__(self,driver=None): self.driver = driver def start(self): # 初始化app if self.driver==None: desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['deviceName'] = 'emulator-5554' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' #desired_caps['noReset'] = 'True' self.driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() self.driver.implicitly_wait(10) return self def main(self): from py_page.main_page import MainPage return MainPage(self.driver)测试用例修改如下:
from py_page.main_page import MainPage from py_page.app import App class TestLogin: def test_login_pos(self): name_text = App().start().main().goto_myPage().goto_login().account_login().is_cancel().get_login_name() print(name_text) assert name_text == '思源_smile'7、继续优化py_page,使元素定位及交互从页面类中分离,减少页面类中的代码量,方便维护与管理
(1)在py_page中新建base_page.py封装页面基类,实现对页面元素的定位及交互,页面元素的查找常用有两种形式:查找单个元素&&查找多个元素集,即封装find && finds方法,每个查找元素的形式包含id, xpath, class ,ACCESSIBILITY_ID四种方式故作如下封装,由于BasePage中find&&finds方法需要用到driver,故BasePage初始化一个driver来实现调用self.driver
from appium.webdriver.common.mobileby import MobileBy as By class BasePage: def __init__(self, driver: webdriver = None): self.driver = driver def find(self, by, locator): by_locator = None if by == 'id' or by == 'ID': by_locator = (By.ID, locator) elif by == 'xpath' or by == 'XPATH': by_locator = (By.XPATH, locator) elif by == 'class' or by == 'CLASS': by_locator = (By.CLASS_NAME, locator) elif by == 'ACCESSIBILITY_ID' or by == 'accessibility_id': by_locator = (By.ACCESSIBILITY_ID, locator) ele = self.driver.find_element(*(by_locator)) return ele def finds(self, by, locator): by_locator = None if by == 'id' or by == 'ID': by_locator = (By.ID, locator) elif by == 'xpath' or by == 'XPATH': by_locator = (By.XPATH, locator) elif by == 'class' or by == 'CLASS': by_locator = (By.CLASS_NAME, locator) elif by == 'ACCESSIBILITY_ID' or by == 'accessibility_id': by_locator = (By.ACCESSIBILITY_ID, locator) eles = self.driver.find_elements(*(by_locator)) return eles def find_and_click(self, by, locator): ele = self.find(by, locator) ele.click() def finds_and_click(self, by, locator, index): eles = self.finds(by, locator) eles[index].click() def finds_and_send(self, by, locator, index, text): eles = self.finds(by, locator) eles[index].send_keys(text) def find_and_send(self, by, locator, text): self.find(by, locator).send_keys(text) def find_and_clear(self, by, locator): self.find(by, locator).clear() @staticmethod def get_yaml_eles(yaml_path): with open(yaml_path, "r", encoding="UTF-8") as f: datas = yaml.safe_load(f) return datas修改app.py,使App继承BasePage, 因此调用这个App()时会调用BasePage中的构造方法,初始化一个self.driver==None
from appium import webdriver from appium.webdriver.common.mobileby import MobileBy as By from appium.webdriver.common.touch_action import TouchAction from py_page.base_page import BasePage class App(BasePage): def start(self): # 初始化app if self.driver==None: desired_caps = {} desired_caps['platformName'] = 'Android' # 'Android' or ’Ios' desired_caps['deviceName'] = 'emulator-5554' desired_caps['appPackage'] = 'com.xueqiu.android' desired_caps['appActivity'] = 'view.WelcomeActivityAlias' #desired_caps['noReset'] = 'True' self.driver = webdriver.Remote('<http://localhost:4723/wd/hub>', desired_caps) # 类似于driver = webdriver.Chrome() self.driver.implicitly_wait(10) return self def main(self): from py_page.main_page import MainPage return MainPage(self.driver) def stop(self): self.driver.quit()py_page中其他页面类继承页面基类BasePage,使得页面类能够调用页面基类中元素定位及交互方法,后续在测试中需要用到页面元素交互的方法都可以封装在BasePage中,供页面类直接调用;
基于BasePage的封装,优化py_page中页面类的代码实现:
from appium.webdriver.common.mobileby import MobileBy as By from py_page.base_page import BasePage class MainPage(BasePage): def goto_myPage(self): # 在首页进入【我的】页面 self.find_and_click(By.XPATH, "//*[@text='我的']") from py_page.my_page import MyPage return MyPage(self.driver)from appium.webdriver.common.mobileby import MobileBy as By from py_page.base_page import BasePage class MyPage(BasePage): def goto_login(self): # 在我的页面进入【登录】页面 self.find_and_click(By.XPATH, "//*[@text='帐号密码登录']") from py_page.login_page import LoginPage return LoginPage(self.driver) def is_cancel(self): # 对一键导入弹框进行判断处理 try: self.find_and_click(By.XPATH, "//*[@text='取消']") except: pass return self def get_login_name(self): # 登录后在我的页面获取登录昵称 name_ele = self.find(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/name']") name_text = name_ele.get_attribute("text") return name_textfrom appium.webdriver.common.mobileby import MobileBy as By from py_page.base_page import BasePage class LoginPage(BasePage): def account_login(self): # 在登录页面进行账号密码登录 self.find_and_send(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']",'13262506790') self.find_and_send(By.XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']",'1111111a') self.find_and_click(By.XPATH, "//*[@text='登录']") from py_page.my_page import MyPage return MyPage(self.driver)(2)把py_page页面类中的页面元素集中管理,从代码中分离,当前端页面元素及交互变更时,我们减少对页面类中代码的修改,从数据载体中集中统一维护;
新建yaml_page,yaml_page文件夹下对应每一个页面类创建对应的yamlPage
login_page.yaml
account_login: login_account: [XPATH, "//*[@resource-id='com.xueqiu.android:id/login_account']"] login_password: [XPATH, "//*[@resource-id='com.xueqiu.android:id/login_password']"] login_button: [XPATH, "//*[@text='登录']"]main_page.yaml
goto_myPage: my: [XPATH, "//*[@text='我的']"]my_page.yaml
goto_login: account_login: [XPATH, "//*[@text='帐号密码登录']"] is_cancel: cancel: [XPATH, "//*[@text='取消']"] get_login_name: login_name: [XPATH, "//*[@resource-id='com.xueqiu.android:id/name']"]修改对应py_page中页面类中的代码
mian_page.py
from py_page.base_page import BasePage class MainPage(BasePage): yaml_path = './yaml_page/main_page.yaml' yaml_datas = BasePage.get_yaml_eles(yaml_path) def goto_myPage(self): # 在首页进入【我的】页面 datas = self.yaml_datas['goto_myPage'] self.find_and_click(*datas['my']) from py_page.my_page import MyPage return MyPage(self.driver)(3)继续优化py_page中页面类的代码,下面login_page.py中的account_login方法包含多个步骤,可用行为驱动的方式优化
login_page.py
from py_page.base_page import BasePage class LoginPage(BasePage): yaml_path = './yaml_page/login_page.yaml' yaml_datas = BasePage.get_yaml_eles(yaml_path) def account_login(self): # 在登录页面进行账号密码登录 datas = self.yaml_datas['account_login'] self.find_and_send(*datas['login_account'],'13262506790') self.find_and_send(*datas['login_password'],'1111111a') self.find_and_click(*datas['login_button']) from py_page.my_page import MyPage return MyPage(self.driver)