赞
踩
Menu:企业微信移动app测试实战(2)、(3)
参考链接 uiautomator 定位: https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html 8 XPath 用法: https://www.w3.org/TR/xpath-functions/#func-matches https://www.w3.org/TR/xpath-functions/ 元素定位 测试步骤三要素: 定位、交互、断言 定位 Id 定位(优先级最高) XPath 定位(速度慢,定位灵活) Accessibility ID 定位(content-desc) Uiautomator 定位(速度快,语法复杂) xpath w3c https://www.w3.org/TR/xpath-functions/#func-matches https://www.w3.org/TR/xpath-functions/ xpath表达式常用用法: not 、contains、ends_with、starts_with and 、or XPath 常用方法 绝对定位: 不推荐 相对定位: //* //*[contains(@resource-id, ‘login’)] (重点) //*[@text=‘登录’] (重点) //*[contains(@resource-id, ‘login’) and contains(@text, ‘登录’)] (重点) //*[contains(@text, ‘登录’) or contains(@class, ‘EditText’)] (了解) //[ends-with(@text,‘号’) ] | //[starts-with(@text,‘姓名’) ] 两个定位的集合列表 (了解) //*[@clickable=“true"]//android.widget.TextView[string-length(@text)>0 and string-length(@text)<20] (了解) //[contains(@text, ‘看点’)]/ancestor:://*[contains(@class, ‘EditText’)] (轴) (了解) #XPATH定位下一级目录直接/*[@text="定位文本元素"] ,如果定位元素是在孙子节点使用//*[@text="定位文本元素"] 原生定位: Uiautomator 定位 写法:’new UiSelector().text(“text")’ 滚动查找: new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text(“查找的文本”).instance(0)); toast: XPath 定位 显式等待,动态的等待元素出现
案例 【联系人用例】:
1.使用pytest
2.使用xpath定位、使用原生定位滚动查找
3.使用参数化@pytest.mark.parametrize
4.验证 成功Toast #该案例如果用toast直接验证会报错,该案例必须使用XPath显式等待方式去完成toast验证
#datas/addcontact.yml文件内容
-
- "霍格name2"
- "男"
- "13700000002"
-
- "霍格name3"
- "女"
- "13700000003"
#datas/delcontact.yml文件内容
-
"霍格name4"
-
"霍格name5"
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' 联系人用例 ''' from time import sleep import pytest import yaml from appium import webdriver from appium.webdriver.common.mobileby import MobileBy from selenium.webdriver.support.wait import WebDriverWait with open('datas/addcontact.yml') as f: addcontactdatas = yaml.safe_load(f) with open('datas/delcontact.yml') as f: delcontactdatas = yaml.safe_load(f) class TestContact: #setup和teardown改成类方法时就不用每次重新去启动APP,从而节省时间 #改成类方法后,类不会再去实例化driver,节省时间 def setup_class(self): caps = {} caps["platformName"] = "android" caps["deviceName"] = "emulator-5554" caps["appPackage"] = "com.tencent.wework" caps["appActivity"] = ".launch.LaunchSplashActivity" caps["noReset"] = "true" caps["noReset"] = "true" caps['skipServerInstallation'] = 'true' # 跳过 uiautomator2 server的安装 caps['skipDeviceInitialization'] = 'true' # 跳过设备初始化 # caps['dontStopAppOnReset'] = 'true' # 启动之前不停止app caps['settings[waitForIdleTimeout]'] = 0 # 与server 建立连接,初始化一个driver 创建session,返回一个sessionid self.driver = webdriver.Remote("http://localhost:4723/wd/hub", caps) self.driver.implicitly_wait(15) #setup和teardown改成类方法时就不用每次重新去启动APP,从而节省时间 #改成类方法后,类不会再去实例化driver,节省时间 def teardown_class(self): self.driver.quit() @pytest.mark.parametrize('name,gender,phonenum', addcontactdatas) def test_addcontact(self, name, gender, phonenum): ''' 添加联系人用例设计 1、打开应用 2、点击通讯录 3、点击添加成员 4、手动输入添加 5、输入【用户名】,姓别,手机号 6、点击保存 7、验证添加成功 ''' # name = "霍格name1" # gender = "女" # phonenum = "13700000001" self.driver.find_element(MobileBy.XPATH, "//android.widget.TextView[@text='通讯录']").click() self.driver.find_element(MobileBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector()' '.scrollable(true).instance(0))' '.scrollIntoView(new UiSelector()' '.text("添加成员").instance(0));').click() self.driver.find_element(MobileBy.XPATH, "//android.widget.TextView[@text='手动输入添加']").click() # 设置姓名 self.driver.find_element(MobileBy.XPATH, "//*[contains(@text, '姓名')]/../*[@class='android.widget.EditText']").send_keys(name) # 设置性别 self.driver.find_element(MobileBy.XPATH, "//*[contains(@text, '性别')]/..//*[@text='男']").click() if gender == '男': self.driver.find_element(MobileBy.XPATH, "//*[@text='男']").click() else: self.driver.find_element(MobileBy.XPATH, "//*[@text='女']").click() # 设置手机号 self.driver.find_element(MobileBy.XPATH, "//*[@text='手机号']").send_keys(phonenum) # 点击保存 self.driver.find_element(MobileBy.ID, "com.tencent.wework:id/gq7").click() # 验证成功 Toast # self.driver.find_element(MobileBy.XPATH, "//*[@class='android.widget.Toast']").text #该案例如果用toast直接验证会报错,该案例必须使用XPath显式等待方式去完成toast验证 element = WebDriverWait(self.driver, 10).until( lambda x: x.find_element(MobileBy.XPATH, "//*[@class='android.widget.Toast']")) result = element.text assert '成功' in result #setup和teardown改成类方法时就不用每次重新去启动APP,从而节省时间 #改成类方法后,类不会再去实例化driver,节省时间 #此时需要使用back()方法返回到上一个页面,不然找不到 【通讯录】,就不能完成下一次 添加联系人 self.driver.back() @pytest.mark.parametrize('name', delcontactdatas) def test_delcontact(self, name): ''' 删除联系人用例设计: 1.打开应用 2.点击通讯录 3.找到要删除的联系人 4.进入联系人页面 5.点击右上角三个点进入个人信息页面,编辑成员 6.删除联系人 7.确认删除 8.验证删除成功 ''' # name = "霍格沃兹name3" self.driver.find_element(MobileBy.XPATH, "//android.widget.TextView[@text='通讯录']").click() # 点击 搜索框 self.driver.find_element(MobileBy.ID, "com.tencent.wework:id/gq_").click() # 输入 联系人姓名 self.driver.find_element(MobileBy.ID, "com.tencent.wework:id/ffq").send_keys(name) sleep(3) # 获取联系人列表 elelist = self.driver.find_elements(MobileBy.XPATH, f"//*[@text='{name}']") # 判断 搜索出来的列表长度 #搜索的时候,通过text属性进行定位,搜索框有该元素,至少会存在一个,所以此时列表长度判断<2 if len(elelist) < 2: print("没有这个联系人") #此处添加return,完成元素列表长度判断后,直接return回到完成判断后的场景 return # 存在 联系人,点击第一个 elelist[1].click() self.driver.find_element(MobileBy.ID, "com.tencent.wework:id/gq0").click() # 点击 编辑成员 self.driver.find_element(MobileBy.XPATH, "//*[@text='编辑成员']").click() # 滚动查找 删除成员 并点击 self.driver.find_element(MobileBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector()' '.scrollable(true).instance(0))' '.scrollIntoView(new UiSelector()' '.text("删除成员").instance(0));').click() # 确定删除 self.driver.find_element(MobileBy.XPATH, "//*[@text='确定']").click() sleep(2) # 验证删除成功 elelist_after = self.driver.find_elements(MobileBy.XPATH, f"//*[@text='{name}']") assert len(elelist) - len(elelist_after) == 1
Menu:企业微信移动app测试实战(4)、(5)
PO改造
PO 封装 PO的方法和字段的意义 方法意义 用公共方法代表UI所提供的服务 方法应该返回其他的PageObject或者返回用于断言的数据 同样的行为不同的结果可以建模为不同的方法 不要在方法内加断言 字段意义 不要暴露页面内部的元素给外部 不需要建模UI内的所有元素 改造流程 改造 1、搭建PO 模式的架构, 将业务逻辑写出来,后面就不需要改动,除非业务逻辑发生变化 #企业微信移动app测试实战4 1h05min 改造 2、填充业务逻辑的实现代码,前端业务流程不变,只变后台逻辑 改造 3、BasePage封装,初始化driver 改造 4、app.py页面, 复用driver ,判断driver是否为None,如果为None 则创建一个Driver, 否则 复用原来的driver self.driver.launch_app() 改造 5、封装 find(), find_and_click(), find_and_sendkeys(), webdriver_wait(), find_byscroll() 改造 6、添加日志便于定位问题
BasePage页面的封装: 初始化方法 find方法 find_and_click方法 handle_exception方法 #异常处理 加入良好日志方便定位: #日志添加 使用标准log取代print logging.baseConfig(level=logging.DEBUG) 在具体的action中加入log方法方便跟踪 App页面的封装: 启动应用 关闭应用 重启应用 进入首页 数据驱动的应用: 测试数据的数据驱动 PO定义的数据驱动 测试步骤的数据驱动 断言数据驱动 #不要全部框架数据驱动,会丢失代码比较重要的重构、建模、开放的生态能力
多个页面之间相互导入时产生循环导入的问题(most likely due to a circular import),解决方案:
使用局部导入,注释掉循环导入的名称,然后通过alt+enter键选择'import xxx locally'局部导入的方式
案例:实现PO改造
改造流程
改造 1、搭建PO 模式的架构, 将业务逻辑写出来,后面就不需要改动,除非业务逻辑发生变化 #企业微信移动app测试实战4 1h05min
改造 2、填充业务逻辑的实现代码,前端业务流程不变,只变后台逻辑
改造 3、BasePage封装,初始化driver
改造 4、app.py页面, 复用driver ,判断driver是否为None,如果为None 则创建一个Driver, 否则 复用原来的driver self.driver.launch_app()
改造 5、封装 find(), find_and_click(), find_and_sendkeys(), webdriver_wait(), find_byscroll()
改造 6、添加日志便于定位问题
Pycharm项目说明:
1.datas文件夹存放数据驱动文件
2.page文件夹完成page页面元素定位及页面业务操作
3.testcases文件夹 测试用例的业务链调用
#datas/addcontact.yml文件内容
-
- "霍格name2"
- "男"
- "13700000002"
-
- "霍格name3"
- "女"
- "13700000003"
#page/basepage.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- import logging from appium.webdriver.common.mobileby import MobileBy from appium.webdriver.webdriver import WebDriver from selenium.webdriver.support.wait import WebDriverWait """ #封装滚动查找元素可以return 元素 ,封装click()和send_keys()就不可以啦,他们是方法不是元素,所以不能return BasePage : 存放一些基本的方法,比如:初始化 driver , find查找元素, find_and_click方法 handle_exception方法 #异常处理 1.查找元素弹框、点击元素出现弹框、输入元素出现弹框(弹框指的是异常弹框,例如升级、消息弹框等),那么就需要对这些异常弹框进行处理 2.使用try except 或者是 加装饰器处理这些异常情况 3.然后继续操作我们的元素 find_by_scroll方法 #滚动查找 加入良好日志方便定位: #日志添加 #【企业微信移动app测试实战5】 增加日志,日志一般是在基类里面添加 30min使用python自带logging记录日志 方案一: 1.使用标准log取代print 使用python自带logging记录日志 2.logging.baseConfig(level=logging.DEBUG) #logging.info 小写info 传入日志的相关message #logging.INFO 大写INFO设置日志级别 3.在具体的action中加入log方法方便跟踪 logging.info(f'find: {locator}') #logging.basicConfig(level=logging.INFO)配置失效,日志不打印的情况处理 #参考链接: https://blog.csdn.net/weixin_39773337/article/details/109138035 #在调用logging.basicConfig前打印了root处理器的handles是有两个的,但是进一步去追踪是哪个地方先行调用了basicConfig root_logger = logging.getLogger() for h in root_logger.handlers[:]: root_logger.removeHandler(h) logging.basicConfig(level=logging.INFO) 方案二: #企业微信移动app测试实战5 36min40s 使用pytest.ini收集日志 1.创建一个pytest.ini,在执行的过程中收集测试结果,注意需要在pycharm里面安装allure-pytest,不然会报错 #pytest.ini文件内容 [pytest] addopts = --alluredir ../result #存放结果路径 2.因为你pytest.ini里面的那一行中文注释导致的,写英文注释的话是不会报错的 ini文件是不能随便修改的 """ class BasePage: #python自带手机日志logging level=logging.INFO 大写INFO设置日志级别,配置完成后会自动打印logging中的日志 # logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.INFO)配置失效 #在调用logging.basicConfig前打印了root处理器的handles是有两个的,但是进一步去追踪是哪个地方先行调用了basicConfig root_logger = logging.getLogger() for h in root_logger.handlers[:]: root_logger.removeHandler(h) logging.basicConfig(level=logging.INFO) #None值在BasePage页面进行定义 def __init__(self, driver: WebDriver = None): self.driver = driver def find(self, locator): #小写info 传入日志的相关message logging.info(f'find: {locator}') return self.driver.find_element(*locator) def find_and_click(self, locator): logging.info('click') self.find(locator).click() def find_and_sendkeys(self, locator, text): logging.info(f'sendkeys : {text}') self.find(locator).send_keys(text) def find_by_scroll(self, text): logging.info('find_by_scroll') #封装滚动查找元素可以return 元素 ,封装click()和send_keys()就不可以啦 return self.driver.find_element(MobileBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector()' '.scrollable(true).instance(0))' '.scrollIntoView(new UiSelector()' f'.text("{text}").instance(0));') def webdriver_wait(self, locator, timeout=10): logging.info(f'webdriver_wait: {locator}, timeout: {timeout}') element = WebDriverWait(self.driver, timeout).until( lambda x: x.find_element(*locator)) return element #返回上一页方法封装 def back(self, num=1): logging.info(f'back: {num}') for i in range(num): self.driver.back()
#page/app.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- ''' 存放 app 应用常用的一些方法:比如启动app, 关闭app, 停止 app, 进入首页 ''' from appium import webdriver from app.page.basepage import BasePage from app.page.mainpage import MainPage ''' App页面的封装: 启动应用 关闭应用 重启应用 进入首页 app.py页面, 复用driver ,判断driver是否为None,如果为None 则创建一个Driver, 否则 复用原来的driver self.driver.launch_app() app.py页面既然需要判断driver是否为None,就需要有一个None值,None值在BasePage页面进行定义。 driver的复用,改动app页面的同时还需要改动basepage,需要在basepage页面默认指定一个driver类型,给driver一个默认None值 ''' class App(BasePage): def start(self): ''' 启动app ''' if self.driver == None: #既然需要判断driver是否为None,就需要有一个None值,None值在BasePage页面进行定义 # 第一次调用start()方法的时候driver 为None caps = {} caps["platformName"] = "android" caps["deviceName"] = "emulator-5554" caps["appPackage"] = "com.tencent.wework" # caps["appActivity"] = ".launch.LaunchSplashActivity" caps["appActivity"] = ".launch.WwMainActivity" caps["noReset"] = "true" caps['skipServerInstallation'] = 'true' # 跳过 uiautomator2 server的安装 caps['skipDeviceInitialization'] = 'true' # 跳过设备初始化 # caps['dontStopAppOnReset'] = 'true' # 启动之前不停止app caps['settings[waitForIdleTimeout]'] = 0 # 与server 建立连接,初始化一个driver 创建session,返回一个sessionid self.driver = webdriver.Remote("http://localhost:4723/wd/hub", caps) else: # launch_app() 这个方法不需要传入任何参数, 会自动启动起来DesireCapa里面定义的activity # start_activity(packagename, activityname) 可以启动其它的应用的页面 #如果driver存在,直接启动DesireCapa里面定义的activity self.driver.launch_app() self.driver.implicitly_wait(20) return self def restart(self): ''' 重启app ''' self.driver.close() self.driver.launch_app() #return self 使用return self后表示的意思是调用完当前页面的该方法后仍然可以接着调用当前页面的其他方法 return self def stop(self): ''' 停止 app ''' self.driver.quit() def goto_main(self): ''' 进入首页 ''' #当 当前页面方法的目的是进入到其他页面,需要传递一个driver过去。 上一个页面传递给下一个页面driver return MainPage(self.driver)
#page/mainpage.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- ''' 主页面 ''' from appium.webdriver.common.mobileby import MobileBy from app.page.basepage import BasePage from app.page.contactlistpage import ContactListPage class MainPage(BasePage): # def __init__(self, driver): # self.driver = driver contactlist = (MobileBy.XPATH, "//android.widget.TextView[@text='通讯录']") def goto_contactlist(self): ''' 进入到通讯录 ''' # self.driver.find_element(MobileBy.XPATH, # "//android.widget.TextView[@text='通讯录']").click() self.find_and_click(self.contactlist) return ContactListPage(self.driver) def goto_workbench(self): pass
#page/contactlistpage.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- ''' 通讯录列表页 ''' from appium.webdriver.common.mobileby import MobileBy from app.page.addmemberpage import AddMemberPage from app.page.basepage import BasePage class ContactListPage(BasePage): # def __init__(self, driver): # self.driver = driver addmember_text = "添加成员" def add_contact(self): # self.driver.find_element(MobileBy.ANDROID_UIAUTOMATOR, # 'new UiScrollable(new UiSelector()' # '.scrollable(true).instance(0))' # '.scrollIntoView(new UiSelector()' # '.text("添加成员").instance(0));').click() self.find_by_scroll(self.addmember_text).click() return AddMemberPage(self.driver) def search_contact(self): pass
#page/addmemberpage.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- ''' 添加成员页 ''' # from app.page.contactaddpage import ContactAddPage from appium.webdriver.common.mobileby import MobileBy from selenium.webdriver.support.wait import WebDriverWait from app.page.basepage import BasePage class AddMemberPage(BasePage): # def __init__(self, driver): # self.driver = driver add_manual_element = (MobileBy.XPATH, "//android.widget.TextView[@text='手动输入添加']") toast_ele = (MobileBy.XPATH, "//*[@class='android.widget.Toast']") def add_menual(self): ''' 手动输入添加 ''' from app.page.contactaddpage import ContactAddPage # self.driver.find_element(MobileBy.XPATH, # "//android.widget.TextView[@text='手动输入添加']").click() self.find_and_click(self.add_manual_element) return ContactAddPage(self.driver) def get_toast(self): # text = '成功' # element = WebDriverWait(self.driver, 10).until( # lambda x: x.find_element(MobileBy.XPATH, "//*[@class='android.widget.Toast']")) element = self.webdriver_wait(self.toast_ele) result = element.text return result
#page/contactaddpage.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- # from app.page.addmemberpage import AddMemberPage from appium.webdriver.common.mobileby import MobileBy from app.page.basepage import BasePage class ContactAddPage(BasePage): # def __init__(self, driver): # self.driver = driver name_element = (MobileBy.XPATH, "//*[contains(@text, '姓名')]/../*[@class='android.widget.EditText']") gender_element = (MobileBy.XPATH, "//*[contains(@text, '性别')]/..//*[@text='男']") male_ele = (MobileBy.XPATH, "//*[@text='男']") female_ele = (MobileBy.XPATH, "//*[@text='女']") phonenum_ele = (MobileBy.XPATH, "//*[@text='手机号']") save_ele = (MobileBy.ID, "com.tencent.wework:id/ad2") save_txt = "保存" def set_name(self, name): # 设置姓名 # self.driver.find_element(MobileBy.XPATH, # "//*[contains(@text, '姓名')]/../*[@class='android.widget.EditText']").send_keys(name) self.find_and_sendkeys(self.name_element, name) return self def set_gender(self, gender): # 设置性别 # self.driver.find_element(MobileBy.XPATH, # "//*[contains(@text, '性别')]/..//*[@text='男']").click() self.find_and_click(self.gender_element) if gender == '男': # self.driver.find_element(MobileBy.XPATH, "//*[@text='男']").click() self.find_and_click(self.male_ele) else: self.find_and_click(self.female_ele) # self.driver.find_element(MobileBy.XPATH, "//*[@text='女']").click() return self def set_phonnum(self, phonenum): # 设置手机号 # self.driver.find_element(MobileBy.XPATH, "//*[@text='手机号']").send_keys(phonenum) self.find_and_sendkeys(self.phonenum_ele, phonenum) return self def click_save(self): from app.page.addmemberpage import AddMemberPage # 点击保存 # self.driver.find_element(MobileBy.ID, "com.tencent.wework:id/gq7").click() #可以通过滑动到元素的位置,点击文本 self.find_by_scroll(self.save_txt).click() #self.find_by_scroll(self.save_txt) #也可以通过滑动到文件的位置,通过xpath定位文本元素来实现点击文本 # self.find_and_click(self.save_ele) #循环调用问题使用局部导入的解决方案 #保存完成员验证保存成功后又一次跳入 添加成员页AddMemberPage 页面 return AddMemberPage(self.driver)
#调用 #testcases/test_contact.py文件内容 #!/usr/bin/env python # -*- coding: utf-8 -*- import pytest import yaml ''' 测试策略:上线前使用自动化遍历工具过一遍,后续会介绍 【企业微信移动app测试实战5】 41min 自动化遍历工具简单介绍: 在上线之前,让你的所有的自动化用例全都去跑一遍,不要让页面出现空白、崩溃 Monkey、AppCrawler ''' from app.page.app import App #解决中文编码问题 encoding='utf-8' #路径 ../datas/addcontact.yml ../表示上一层 ./datas/addcontact.yml ./表示同一级文件路径 with open('../datas/addcontact.yml',encoding='utf-8') as f: addcontactdatas = yaml.safe_load(f) with open('../datas/delcontact.yml',encoding='utf-8') as f: delcontactdatas = yaml.safe_load(f) class TestContact: def setup_class(self): self.app = App() def teardown_class(self): self.app.stop() #如果一条用例执行失败,为了后面不受影响可以使用setup和teardown方法,setup进入主页,teardown方法多回退几次到首页 def setup(self): self.main = self.app.start().goto_main() def teardown(self): self.app.back(5) @pytest.mark.parametrize('name,gender,phonenum', addcontactdatas) def test_addcontact(self, name, gender, phonenum): ''' 添加联系人 ''' # name = "霍格name2" # gender = "女" # phonenum = "13700000002" mypage = self.main.goto_contactlist(). \ add_contact().add_menual(). \ set_name(name).set_gender(gender).set_phonnum(phonenum).click_save() text = mypage.get_toast() # mypage.add_menual() assert '成功' in text self.app.back() #比较文件路径区别 ./test.yml 表示当前同一级文件路径 ../test.yml表示上一级文件路径 # def test_file(self): # with open('./test.yml', encoding='utf-8') as f: # testdatas = yaml.safe_load(f) # print(testdatas)
#testcases/pytest.ini文件内容 为配置文件 #后续会继续解读这块内容
#1.创建一个pytest.ini,在执行的过程中收集测试结果,注意需要在pycharm里面安装allure-pytest,不然会报错
#2.因为你pytest.ini里面的那一行中文注释导致的,写英文注释的话是不会报错的 ini文件是不能随便修改的 pytest.ini文件里面都不能写中文注释哦
[pytest]
addopts = --alluredir ../result #存放结果路径
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。