【Appium】企业微信练习2(PO--增加联系人)

【Appium】 课程全笔记(录播+直播课)链接:

Appium基础1(环境搭建和简介)

Appium基础2(元素定位和元素常用方法)

Appium基础3(手势操作和uiautomator查找元素)

Appium基础4(显式等待)

Appium基础5(toast和参数化)

Appium基础6(webview)

Appium_企业微信实战课1(非PO,增加和删除联系人)

Appium_企业微信实战课2(PO–增加联系人)


企业微信增加和删除联系人的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
2 Likes