【Appium】 课程全笔记(录播+直播课)链接:
Appium基础3(手势操作和uiautomator查找元素)
企业微信增加和删除联系人的PO改装
传统测试用例的问题
- 无法适应UI变化,UI变化会导致大量的case需要修改
- 大量的样板代码drvier、find、click
- 无法清晰表达业务用例场景
PageObject模式原则
- 方法意义:
- 用公共方法代表UI所提供的服务
- 方法应该返回其他的PageObject或者返回用于断言的数据
- 同样的行为不同的结果可以建模为不同的方法
- 不要在方法内加断言
- 字段意义:
- 不要暴露页面内部的额元素给外部
- 不需要建模UI内的所有元素
PO模式封装的主要组成元素
- page对象:完成对页面的封装
- driver对象:完成对web、android、ios、接口的驱动
- 测试用例:调用page对象实现业务并断言
- 数据封装:配置文件和数据驱动
- utiles:其他功能封装,改进原生框架不足
python不支持循环导入的例子
代码1
from page.base_page import BasePage
from page.member_invite_page import MemberInvitePage
#通讯录页面
class AddressListPage(BasePage):
def click_addmember(self):
#进入手动输入添加页面
return MemberInvitePage(self._driver)
代码2
from page.address_list_page import AddressListPage
from page.base_page import BasePage
from page.contact_add_page import ContactAddPage
class MemberInvitePage(BasePage):
def click_manual_add(self):
return ContactAddPage(self._driver)
def click_back(self):
return AddressListPage(self._driver)
问题点
- 比如AddressListPage要引用MemberInvitePage,首先AddressListPage就会初始化全部需要import的文件
,然后等跳转到MemberInvitePage,发现MemberInvitePage有AddressListPage要引用,这样就循环导入了,MemberInvitePage需要初始化AddressListPage,导致循环引用
解决方法
- import的时候import local即可
from page.base_page import BasePage
# from page.member_invite_page import MemberInvitePage
#通讯录页面
class AddressListPage(BasePage):
def click_addmember(self):
#进入手动输入添加页面
#import的时候import local即可
from page.member_invite_page import MemberInvitePage
return MemberInvitePage(self._driver)
技术特点
- 一个函数 return self,是可以让对象可以循环使用当前的函数
class ContactAddPage(BasePage):
def input_name(self):
#用来衔接方法,可以做到a.input_name().input_gender().input_address()的效果
return self
def input_gender(self):
return self
def input_address(self):
return self
def input_phone(self):
return self
def click_save(self):
return MemberInvitePage(self._driver)
a=ContactAddPage()
a.click_save().click_manual_add().input_name().input_phone().input_gender().input_name()
- 每跳转一个页面,就新建一个page,要注意python无法循环导入的问题,import时选择local加入即可
- 这里只是只有一个添加联系人的用例
- 没有封装显示等待、find_elements、滚动的方法
- 参数化的用例用一个data文件夹的yaml文件去管理
- 步骤用steps文件夹里面的yaml文件去管理,通常是一个页面文件对应一个yaml文件,包含by,locator和action,但如果一个页面有多个元素,到底怎么封装,顺序怎么搞,还不清楚
- find_elementd的封装包含了不知名弹窗的处理、错误次数的判定
- 日志通过logging.basicConfig(level=logging.INFO)来定义,方便我们去打印出日志,而且不喜欢pytest的-s -v参数,红色字体显示更加明显
logging.basicConfig(level=logging.INFO)
def find(self,locator,value):
logging.info(locator)
logging.info(value)
项目结构
data 存放用例参数化的文件夹
- contact_add.yml
- 这里定义了两个联系人的参数,最终会添加两个联系人
-
- "tong6"
- "有毛病"
- "女"
- "13172661112"
-
- "tong7"
- "有毛病"
- "男"
- "13172661113"
steps 存放数据用例步骤的文件夹
- main_steps.yml
- 存放主页上需要操作的步骤,因为只有一个点击,就放了一个
- 这个格式最终的数据是[{}]的方式
-
by: XPATH
locator: //*[@resource-id="com.tencent.wework:id/dyx" and @text="通讯录"]
action: click
testcase 用例的文件夹
- test_add_contact.py 增加联系人的用例文件
import pytest
import yaml
from page.app import App
#测试添加联系人的用例的类
class TestAddTcontact():
#初始化app,并且到主页的位置
def setup(self):
self.main=App().strat().main()
#参数化添加数据,用yaml数据驱动的方法
@pytest.mark.parametrize("name,address,gender,input_phone",yaml.safe_load(open(r"../data/contact_add.yml",encoding="UTF-8")))
def test_add_contact(self,name,address,gender,input_phone):
#进入到通讯录,点击增加联系人,点击手动添加联系人,输入名字,输入地址、输入性别、输入电话、点击保存、确认添加成功的toast,点击back返回到通讯录页面
self.main.goto_addresslist().click_addmember().\
click_manual_add().input_name(name).input_address(address).\
input_gender(gender).input_phone(input_phone).\
click_save().verify_toast().click_back()
page 页面的
- 由于页面比较多,先理一下页面的顺序,并按照外层到内层列出文件
- main.py 主页 -》address_list_page.py 通讯录 -》 member_invite_page.py 添加联系人的方式 -》 contact_add_page.py 添加联系人的页面
- main.py 主页
from appium.webdriver.common.mobileby import MobileBy as By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from page.address_list_page import AddressListPage
from test_qiyeweixin.page.base_page import BasePage
#进入的主页面
class Main(BasePage):
def goto_message(self):
pass
# 进入通讯录的页面
def goto_addresslist(self):
#进入通讯页面
WebDriverWait(self._driver, 10).until(expected_conditions.visibility_of_element_located(
(By.XPATH, '//*[@resource-id="com.tencent.wework:id/dyx" and @text="通讯录"]')))
# self._driver.find_element(By.XPATH, '//*[@resource-id="com.tencent.wework:id/dyx" and @text="通讯录"]').click()
#封装了步骤的方法,直接调用步骤的数据即可,这里是点击进入通讯录的按钮
self.step("../steps/main_steps.yml")
#进入通讯录的页面的page
return AddressListPage(self._driver)
def goto_workbench(self):
pass
def goto_profile(self):
pass
- address_list_page.py 通讯录
from page.base_page import BasePage
# from page.member_invite_page import MemberInvitePage
from appium.webdriver.common.mobileby import MobileBy as By
#通讯录页面
class AddressListPage(BasePage):
def click_addmember(self):
#进入手动输入添加页面
from page.member_invite_page import MemberInvitePage
#下滑直到找到添加成员
self._driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().'
'scrollable(true).instance(0)).'
'scrollIntoView(new UiSelector().textContains("添加成员").'
'instance(0));').click()
# 点击通讯录的元素
self.find(By.XPATH, "//*[@text='手动输入添加']").click()
# 进入到邀请联系人的页面
return MemberInvitePage(self._driver)
- member_invite_page.py 添加联系人的方式
from page.address_list_page import AddressListPage
from page.base_page import BasePage
# from page.contact_add_page import ContactAddPage
from appium.webdriver.common.mobileby import MobileBy as By
#请联系人的页面
class MemberInvitePage(BasePage):
# 点击进入邀请联系人的方法
def click_manual_add(self):
#通过local导入,防止python循环导入的问题
from page.contact_add_page import ContactAddPage
# 滚动寻找添加成员的元素,怕联系人太多,所以用这个万能的滚动公式
self._driver.find_element_by_android_uiautomator('new UiScrollable(new UiSelector().'
'scrollable(true).instance(0)).'
'scrollIntoView(new UiSelector().textContains("添加成员").'
'instance(0));').click()
# 进入到添加联系人的页面
return ContactAddPage(self._driver)
# 返回上一级菜单
def click_back(self):
#点击返回的按钮,回到通讯录的页面
self.find(By.XPATH,"//*[@resource-id='com.tencent.wework:id/h9e']").click()
#返回通讯录页面
return AddressListPage(self._driver)
def verify_toast(self):
#保存之后页面会有toast,捕捉到添加成功的toast,用asset去判断即可
#但是没什么返回值,怎么确认呢?这个老师没有优化
self.find(By.XPATH,"//*[@class='android.widget.Toast']")
return self
- contact_add_page.py 添加联系人的页面
from page.base_page import BasePage
from page.member_invite_page import MemberInvitePage
from appium.webdriver.common.mobileby import MobileBy as By
#添加联系人的详细内容的页面
class ContactAddPage(BasePage):
#添加联系人
def input_name(self,name):
# 添加联系人的名字
self.find(By.XPATH, '//*[@resource-id="com.tencent.wework:id/b5t"]//*[@text="必填"]').send_keys(
name)
return self
#添加姓名
def input_gender(self,gender):
# 选择性别是男的还是女的,如果性别是男的,就默认不选就好了,因为默认的性别就是男的
if gender == "女":
# 如果性别是女生,就先点击一下选择联系人的控件
self.find(By.XPATH, f'//*[@text="男"]').click()
# 第一个元素是男,第二个元素是女的,选择第二个元素女生,点击即可
gender_girl = self._driver.find_elements(By.XPATH, '//*[@resource-id="com.tencent.wework:id/bce"]')[1]
gender_girl.click()
return self
#添加地址
def input_address(self,address):
elements = self._driver.find_elements(By.XPATH, '//*[@text="选填"]')
# 第二个元素就是添加地址了
elements[1].click()
# 点击之后进入另一个页面,然后输入地址
self.find(By.XPATH, "//*[@resource-id='com.tencent.wework:id/he']").send_keys(address)
# 点击保存,保存地址
self.find(By.XPATH, "//*[@resource-id='com.tencent.wework:id/h9w']").click()
return self
#添加手机号码
def input_phone(self,phone):
# 输入手机号码
self.find(By.XPATH, '//*[@text="手机号"]').send_keys(phone)
return self
#点击保存联系人
def click_save(self):
# 点击保存联系人的按钮
self.find(By.XPATH, '//*[@text="保存"]').click()
return MemberInvitePage(self._driver)
- app.py 负责初始化app,加载app之后,就进入到主页的文件
from appium import webdriver
from page.base_page import BasePage
from page.main import Main
#这是app的类,负责启动,关闭,重启等操作
class App(BasePage):
#启动app
def strat(self):
self.desire_cap = {
"platformName": "android",
"platformVersion": "6.0",
"deviceName": "127.0.0.1:7555",
"noReset": "true",
"appPackage": "com.tencent.wework",
"appActivity": ".launch.WwMainActivity",
"autoGrantPermissions": "true"
}
self._driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", self.desire_cap)
self._driver.implicitly_wait(5)
return self
def stop(self):
pass
def restart(self):
return self
#返回main的主页方法
def main(self):
#返回主页
return Main(self._driver)
- base_page
- 封装的复用driver、find_element、和步骤数据驱动的方法
import logging
from typing import List
import yaml
from appium.webdriver.common.mobileby import MobileBy as By, MobileBy
from appium.webdriver.webdriver import WebDriver
class BasePage():
#定义了一个log的日志,等级是INFO
logging.basicConfig(level=logging.INFO)
#定义了一个
_black_list=[
(By.XPATH,"//*[@text='允许']")
]
#定义查找黑名单的初始尝试次数
_error_time=0
#定义了查找黑名单的最多尝试测试
_error_max_time=3
#定义步骤驱动中,send的value的值
_param = {}
#定义一个初始值为None的driver,方便复用driver
def __init__(self,driver:WebDriver=None):
self._driver=driver
#封装find_element的函数,具备查找弹框的处理机制
def find(self,locator,value):
#通过log日志打印出要传的locator和value
logging.info(locator)
logging.info(value)
# if isinstance(locator,tuple):
# return self._driver.find_element(*locator)
# else:
# return self._driver.find_element(locator,value)
# 把上面的变成一个三元的表达式
try:
#这里是一个三元的传值,里面的locator可以写成(By.ID,"id")或者By.ID,"id"两者都行
return self._driver.find_element(*locator) if isinstance(locator,tuple) else self._driver.find_element(locator,value)
#把异常定义为e
except Exception as e:
#当尝试的次数大于最大的尝试次数,就报错,不让这个try死循环
if self._error_time > self._error_max_time:
raise e
#当尝试一次,尝试查找弹窗次数+1
self._error_time+=1
#循环遍历黑名单的locator
for ele in self._black_list:
#查找黑名单的元素
black_elements=self._driver.find_elements(*ele)
#当元素的值大于1,表示找到了黑名单弹窗的元素
if len(black_elements) > 0:
#就点击一下,让弹窗消失
black_elements[0].click()
#由于弹窗消失了,所以可以找到元素了,调用自己
self.find(locator,value)
#找不到元素就抛出异常
raise e
#封账步骤数据的方法,传一个steps的yaml文件
def step(self,file):
#打开yaml文件
with open(file,encoding="UTF-8") as yaml_file:
#为了编写的方法,编译器不知道yaml处理后就是一个列表外层的字典类型
#用List[dict]定义steps的数据类型,方便pycharm编译器,方便我们,让steps变成python的数据类型
steps:List[dict]=yaml.safe_load(yaml_file)
#方便编译器知道我们是一个元素,调用方法方便
element:WebDriver
#循环遍历整个steps的数据
for step in steps:
#日志文件打印出当前的step是怎么样的
logging.info(step)
#进一步解析step的key中的by。主要是定位用的,By
if 'by' in step.keys():
#定义一个变量,进一步解析by的value
myby=step['by']
#当by的value为ID,直接调用find,由于传ID,等于默认传By.ID,所以不需要写成MobileBy.ID
if myby == 'ID':
element=self.find(myby,step['locator'])
#当by的value为XPATH,直接调用find,传入locator的value的值step['locator']
#记住不要写成MobileBy.myby,是识别不出来的
if myby == 'XPATH':
element = self.find(MobileBy.XPATH, step['locator'])
#进一步解析step的key中的action,主要是click、send_keys等行为
if 'action' in step.keys():
#定义一个变量,进一步解析action的value
myaction=step['action']
#当行为的value是点击,就点击这个元素
if myaction == 'click':
element.click()
#当行为的value是send,就发送,当然这里没有去写
if myaction == 'send':
pass