前言

由于公司UI自动化框架底层用的是Uiautomator2,所以我就用Uiautomator2搭了一套UI自动化框架,并运用某软件做了一个实战,思路其实和之前写的Appnium一样的

  • ps:这里其实主要讲的是一个思路,就是你在你的框架里面具体需要使用到哪些包的一个思路,关于具体的代码大家要自行修改
  • Appnium-企业微信UI自动化框架搭建:

一、Uiautomator2介绍


二、整体框架介绍

base包:封装基础类	

case包:封装测试用例类

datas包:参数化数据(excel 或者 yml...)

logs包:封装日志

page包:封装所有的页面

sms包:发送邮箱

report包:存放测试报告

utils包:工具包,例如读取参数化数据

基于python的UI自动化用例 python ui自动化框架_基于python的UI自动化用例


三、框架搭建

1、Base包:

1.1、先创建一个BasePage.py

  • 为什么要单独封装一个BasePage呢? 如果说以后我们不用uiautomator2这个框架了,我们只需要更改BasePage即可,不会影响到其他类的代码,从某种程度上来说也就是进行解耦
  • 另外,这个类也可以封装自己写的公用的方法,例如:重复性很高的代码,这些方法不论在哪个app里都能用的话,我们就单独拧出来封装成一个方法,例如下我写的:关闭弹窗/重新传值等
"""
    基类,大概率不变的东西放在基类
    例如,框架本身的东西,或者自己封装的公用方法
"""
from datetime import time
from time import sleep

import uiautomator2 as u2

device = 'JJN6R18305005632'


class BasePage:
    def __init__(self):
        self.d = u2.connect(device)

    def back(self, num):
        # 返回
        for i in range(num):
            self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/iv_back']").click()

    def clear_input_x_by_resourceId(self, location, value, x):
        """
            清空已填写的选项,重新传值
            :param location: 元素定位字符串
            :param value: 重新传的值
            :param x :索引
        """
        self.d(resourceId=location)[x].click()  # 定位到元素
        self.d(focused=True).clear_text()
        self.d.send_keys(value)

    def clear_input_by_resourceId(self, location, value):
        """
            清空已填写的选项,重新传值
            :param location: 元素定位字符串
            :param value: 重新传的值
        """
        self.d(resourceId=location).click()  # 定位到元素
        self.d(focused=True).clear_text()
        self.d.send_keys(value)
        return self

    def close_popup(self, text, locationType, location):
        """
            关闭弹窗
            :param text:字符串,弹窗包含的text文本
            :param locationType:定位方式 xpath or resourceId
            :param location:定位元素字符串,例:"//*[@text='登录']"
        """
        element = self.d(text=text)
        while True:
            if element:
                element.click()
                continue
            else:
                print("权限校验弹窗不存在")
                break
        if locationType == "xpath":
            self.d.xpath(location).click()
        elif locationType == "resourceId":
            self.d(resourceId=location).click()

    def swipe_until_element_found(self, param, wait_after_found=0.0, **kwargs):
        """
        检查元素是否存在,若不存在则进行上滑,滑动后再次检查,直到滑动到页面底部
        若找到元素则返回,否则滑动到页面底部后,仍未找到元素,则抛出异常,提示找不到元素
        :param param: xpath字符串 或 元素对象
        :param wait_after_found: 找到元素后,原地等待时间
        :param kwargs:
        :return:
        """
        element = self.d.xpath(param) if isinstance(param, str) else param
        param = param if isinstance(param, str) else param.selector
        while True:
            try:
                assert element.exists
                if wait_after_found:
                    print("Element found,sleep {} seconds".format(wait_after_found))
                sleep(wait_after_found)
                return element
            except AssertionError:
                print("Element 【 {} 】 Not found, Continue to swipe up...".format(param))
                # 获取滑动前页面下半部分的所有元素
                page_content = self.d.dump_hierarchy()[(len(self.d.dump_hierarchy()) // 2):]
                # self.up(**kwargs)
                self.d.swipe_ext("up")
                sleep(0.5)
                # 获取滑动后页面下半部分的所有元素,并与上一次滑动前的页面元素对比,页面元素没有变化时跳出循环
                if self.d.dump_hierarchy()[(len(self.d.dump_hierarchy()) // 2):] == page_content:
                    break
        if not element.exists:
            raise AssertionError("Element 【 {} 】 located failed in this page".format(param))

    def swipe_for_click(self, param, wait_after_click=0.0, **kwargs):
        """
        判断UI元素是否存在, 不存在则持续向上滑动到底部,直到UI元素在页面内出现,再进行点击
        :param param: xpath字符串 或 元素对象
        :param wait_after_click: 点击后等待时间
        :return:
        """
        element = self.swipe_until_element_found(param, **kwargs)
        element.click()
        if wait_after_click:
            print("Element found and click,then sleep {} seconds".format(wait_after_click))
        sleep(wait_after_click)

    def up(self, scale=0.9, times=1, duration=1.0, **kwargs):
        """
        上滑操作
        :param scale: 滑动单位,默认0.9个单位
        :param times: 滑动次数,默认1次
        :param duration: 滑动时间,默认1.0秒
        :return:
        """
        for i in range(times):
            self.d.swipe_ext("up", scale, duration=duration, **kwargs)

    def down(self, scale=0.9, times=1, duration=1.0, **kwargs):
        """
        下滑操作
        :param scale: 滑动单位,默认0.9个单位
        :param times: 滑动次数,默认1次
        :param duration: 滑动时间,默认1.0秒
        :return:
        """
        for i in range(times):
            self.d.swipe_ext("down", scale, duration=duration, **kwargs)

    def left(self, scale=0.9, times=1, duration=1.0, **kwargs):
        """
        左滑操作
        :param scale: 滑动单位,默认0.9个单位
        :param times: 滑动次数,默认1次
        :param duration: 滑动时间,默认1.0秒
        :return:
        """
        for i in range(times):
            self.d.swipe_ext("left", scale, duration=duration, **kwargs)

    def right(self, scale=0.9, times=1, duration=1.0, **kwargs):
        """
        右滑操作
        :param scale: 滑动单位,默认0.9个单位
        :param times: 滑动次数,默认1次
        :param duration: 滑动时间,默认1.0秒
        :return:
        """
        for i in range(times):
            self.d.swipe_ext("right", scale, duration=duration, **kwargs)

    def wait_until_element_found(self, param, timeout=30.0, retry_interval=2, wait_after_found=0.0):
        """
        定位元素,如果不存在就间隔若干秒后重试,直到元素定位成功或超时
        :param param: xpath字符串 或 元素对象
        :param timeout: 超时, 默认30秒
        :param retry_interval: 间隔时间, 默认2秒
        :param wait_after_found: 找到元素后,原地等待时间
        :return:
        """
        element = self.d.xpath(param) if isinstance(param, str) else param
        max_time = time() + timeout
        while True:
            try:
                assert element.exists
                if wait_after_found:
                    print("Element found,then sleep {} seconds".format(wait_after_found))
                sleep(wait_after_found)
                return element
            except AssertionError:
                param = param if isinstance(param, str) else param.selector
                print("Element 【 {} 】 Not found, Retry...".format(param))
                if time() > max_time > 0:
                    raise AssertionError("Element 【 {} 】 located failed after {} timeout".format(param, timeout))
                sleep(retry_interval)

    def wait_for_click(self, param, wait_after_click=0.0, **kwargs):
        """
        判断UI元素是否存在, 不存在则等待UI元素在一定时间内出现,再进行点击
        :param param: xpath字符串 或 元素对象
        :param wait_after_click: 点击后等待时间
        :return:
        """
        element = self.wait_until_element_found(param, **kwargs)
        element.click()
        if wait_after_click:
            print("Element found and click,then sleep {} seconds".format(wait_after_click))
        sleep(wait_after_click)

    def repeat_click(self, param, times, wait_after_repeat_click=0.0):
        """
        重复多次点击UI元素
        :param param: xpath字符串 或 元素对象
        :param times: 点击次数
        :param wait_after_repeat_click: 重复点击后等待时间,默认为0.0
        :return:
        """
        element = self.wait_until_element_found(param)
        for i in range(times):
            element.click()
        if wait_after_repeat_click:
            print("Element click,then sleep {} seconds".format(wait_after_repeat_click))
        sleep(wait_after_repeat_click)

1.2、创建一个LoverPadApp.py,继承BasePage

  • 由于以后可能又去测试其他的App,所以我们这边就单独再创建一个LoverPadApp的类继承BasePage,为了区分App
"""
    具体APP相关的操作
"""
from time import sleep

from mikasa_script.pad_ui_auto.base.BasePage import BasePage


class LoverPadApp(BasePage):
    def start(self):
        """
            启动App
        """
        if self.d == None:
            self.d.app_start('具体APP')
        return self.d

    def restart(self):
        """
            重启App
        """
        pass

    def clear(self):
        """
            清除app缓存
        """
        self.d.app_clear('具体APP')

    def stop(self):
        """
            关闭App
        """
        self.d.app_stop('具体APP')

    def login_goto_main(self, workerId, password):
        """
            员工登录进首页
        """
        from mikasa_script.pad_ui_auto.page.MainPage import MainPage
        if self.d != None:
            return MainPage(self.d)
        # 入口
        location = 'com.zhenai.loveguider:id/edittext'
        self.clear_input_x_by_resourceId(location, workerId, 0)
        self.clear_input_x_by_resourceId(location, password, 1)
        sleep(1)
        self.d.xpath("//*[@text='立即登录']").click()
        sleep(2)
        return MainPage(self.d)


if __name__ == '__main__':
    LoverPadApp().clear()

2、Page包:

  • 这里封装企业微信所有要测试的页面
  • 思路:一个页面封装为一个类,页面上的每个功能封装为一个方法
  • 另外元素的定位方法,大家视情况而定:
    1)如果APP很多id是动态生成的话,就建议使用:Xpath定位,因为id可能不是唯一的,是动态生成的,换一台设备后,原代码可能就会因为找不到id而无法运行。
    为了提高代码设计的兼容性,我们尽量考虑到以后前端元素变更又或者测试设备更换的情况
    2)如果Id是唯一的,就建议用id查找,因为Xpath定位比id查找慢

2.1、创建首页,MainPage.py

"""
    首页
"""

from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberLoginPage import MemberLoginPage


class MainPage(LoverPadApp):
    # python 类型提示
    def __init__(self, d):
        # 拿到上个页面的driver
        self.d = d

    def click_member_login(self):
        """
            点击会员登录按钮
        """
        if self.d != None:
            return MemberLoginPage(self.d)
        # 如果有弹窗,点击始终允许
        self.close_popup("始终允许", None, None)
        location = "//*[@text='登录']"
        self.d.xpath(location).click()
        self.close_popup("始终允许", None, None)
        return MemberLoginPage(self.d)

2.2、创建会员登录页面,MemberLoginPage.py

"""
    会员登录页
"""
from time import sleep


from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberMainPage import MemberMainPage


class MemberLoginPage(LoverPadApp):
    # python 类型提示
    def __init__(self, d):
        # 拿到上个页面的driver
        self.d = d

    def member_login(self, memberId):
        """
            会员到店登录
        """
        # if self.d is not None:
        #     return MemberMainPage(self.d)
        self.d.xpath("//*[@text='请输入会员手机号或ID']").click()
        self.d.send_keys(memberId)
        self.d.xpath("//*[@text='请选择接待包间']").click()
        sleep(1)
        self.d.xpath("//*[@text='测试包间']").click()
        sleep(2)
        self.d(text="进入包间").click()
        sleep(2)
        return MemberMainPage(self.d)

2.3、创建会员页面,MemberMainPage.py

"""
    会员界面
"""
from time import sleep

from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.CommitLetterPage import CommitLetterPage
from mikasa_script.pad_ui_auto.page.EmoCounselingPage import EmoCounselingPage
from mikasa_script.pad_ui_auto.page.EmoTestPage import EmoTestPage
from mikasa_script.pad_ui_auto.page.HighVipPage import HighVipPage
from mikasa_script.pad_ui_auto.page.IdCardPage import IdCardPage
from mikasa_script.pad_ui_auto.page.ImgEvaluationPage import ImgEvaluationPage
from mikasa_script.pad_ui_auto.page.MemberInfoPage import MemberInfoPage
from mikasa_script.pad_ui_auto.page.MyOrderPage import MyOrderPage


class MemberMainPage(LoverPadApp):

    def __init__(self, d):
        # 拿到上个页面的driver
        self.d = d

    def check_room_exist(self):
        """
            校验包间名字是否存在
        """
        element = self.d(text="包间:测试包间")
        if element:
            return True
        else:
            return False

    def click_info_table_button(self):
        """
            点击信息表按钮
        """
        # if self.d is not None:
        #     return MemberInfoPage(self.d)
        self.d.xpath("//*[@text='信息表']").click()
        sleep(2)
        return MemberInfoPage(self.d)

    def click_emo_test_button(self):
        """
            点击情感测评按钮
        """
        self.d.xpath("//*[@text='情感测评']").click()
        sleep(2)
        return EmoTestPage(self.d)

    def click_img_evaluation_button(self):
        """
            点击形象评估按钮
        """
        self.d.xpath("//*[@text='形象评估']").click()
        sleep(2)
        return ImgEvaluationPage(self.d)

    def click_id_card_button(self):
        """
            点击身份证件按钮
        """
        self.d.xpath("//*[@text='身份证件']").click()
        sleep(2)
        return IdCardPage(self.d)

    def click_commit_letter_button(self):
        """
            点击承诺函按钮
        """
        self.d.xpath("//*[@text='承诺函']").click()
        sleep(2)
        return CommitLetterPage(self.d)

    def click_my_order_button(self):
        """
            点击我的订单按钮
        """
        self.d.xpath("//*[@text='我的订单']").click()
        sleep(2)
        return MyOrderPage(self.d)

    def click_high_vip_button(self):
        """
            点击高端VIP服务按钮
        """
        self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/service_rv']/android.widget.FrameLayout[1]").click()
        sleep(2)
        return HighVipPage(self.d)

    def click_emo_counseling_button(self):
        """
            点击情感咨询服务按钮
        """
        self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/service_rv']/android.widget.FrameLayout[2]").click()
        sleep(2)
        return EmoCounselingPage(self.d)

2.4、创建会员信息表页面,MemberInfoPage.py

"""
    会员信息表界面
"""
from time import sleep

from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp



class MemberInfoPage(LoverPadApp):
    def __init__(self, d):
        # 拿到上个页面的driver
        self.d = d

    def check_info_view(self):
        """
            校验信息填写后,是否是信息资料预览界面
        """
        self.d.xpath("//*[@text='信息表']").click()
        sleep(2)
        element = self.d(text="择偶标准")
        if element:
            return False
        else:
            return True

    def input_member_info(self):
        """
            填写会员信息
        """
        self.close_popup("我已知晓", None, None)
        sleep(0.5)
        if self.d != None:
            #  页面下滑,移动到有修改信息按钮,如果有则点击修改
            update_button = "//*[@resource-id='com.zhenai.loveguider:id/change']"
            self.swipe_for_click(update_button, wait_after_click=0.0)
            sleep(2)
            self.close_popup("我已知晓", None, None)

        next_step = "//*[@resource-id='com.zhenai.loveguider:id/btn_next']"
        self.swipe_for_click(next_step, wait_after_click=0.0)
        sleep(2)
        self.d.xpath("//*[@resource-id='com.zhenai.loveguider:id/bottom_btn_next']").click()  # 下一步
        sleep(2)
        # 填表人确认
        self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/commitment_agree1"]').click()  # 我承诺
        self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/bottom_btn_next"]').click()  # 提交并预览
        self.close_popup("继续保存", None, None)
        # self.d.xpath('//*[@resource-id="com.zhenai.loveguider:id/tv_left"]').click()  # 不了,继续保存
        MemberInfoPage.back(self, 1)
        return MemberInfoPage(self.d)


3、logs包:

3.1、创建log_utils.py

"""
    该类为日志封装类
"""
import logging
import os

# 实例化logger对象
from mikasa_script.mikasa_auto.workwx_api_autotest.utils.utils import Utils

logger = logging.getLogger(__name__)

# 定义日志文件路径
log_path = Utils.get_log_path()

# 判断路径是否存在,不存在就创建
if not os.path.exists(log_path):
    os.mkdir(log_path)

# 绑定log的handler
file_handler = logging.FileHandler(filename=f"{log_path}/api_object.log", encoding="utf-8")
# 输出的formatter
formatter = logging.Formatter(
    '[%(asctime).19s] %(process)d%(levelname).1s %(filename)s:%(lineno)d:%(funcName)s: %(message)s]')
# 日志格式与句柄的绑定
file_handler.setFormatter(formatter)
# 控制台句柄定义
steam_handler = logging.StreamHandler()
# 日志格式与句柄的绑定
steam_handler.setFormatter(formatter)
# 与logging进行绑定
logger.addHandler(file_handler)
logger.addHandler(steam_handler)
# 设置展示/写入文件的日志的级别
logger.setLevel(logging.INFO)


4、utils包:

4.1、创建utils.py

"""
    该类为工具类
    包括读取yaml文件/读取log日志文件路径
"""
import os

import jsonpath
import yaml


class Utils:
    @classmethod
    def get_yaml_data(self, yaml_file):
        """
        封装提取yaml文件的方法
        :param yaml_file:  传入文件的路径
        :return:
        """
        with open(f"{Utils.get_data_path()}/{yaml_file}", encoding="utf-8") as f:
            datas = yaml.safe_load(f)
        return datas

    @classmethod
    def get_data_path(self):
        """
        获取data文件路径
        :return:
        """
        path = os.sep.join([os.path.dirname(os.path.abspath(__file__)), "../data"])
        print("path", path)
        return path

    @classmethod
    def get_log_path(cls):
        """
        获取日志文件路径
        :return:
        """
        path = os.sep.join([os.path.dirname(os.path.abspath(__file__)), "../logs"])
        print("path", path)
        return path

    @classmethod
    def jsonpath_utils(cls, obj, expt):
        """
        json处理工具
        :param obj:
        :param expt:
        :return:
        """
        return jsonpath.jsonpath(obj, expt)


if __name__ == '__main__':
    path = Utils.get_data_path()
    print(Utils.get_yaml_data("/corp_data.yaml"))


5、data包:

5.1、创建data.yaml

  • 维护你的配置之类的东西 ,然后用utils里面的get_yaml_data方法读取对应的数据
workInfo:
  workerId: 123456
  pad_pwd: 123456
memberInfo:
  memberId: 123456
  crm_pwd: 123456
  phone: 123456
  verifyCode: 123456


6、sms包:

6.1、新建email.py

import io
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

import os

# from HttpRunnerManager.settings import 
    subject = "自动化测试报告"

    with io.open(html_report_path, 'r', encoding='utf-8') as stream:
        send_file = stream.read()

    att = MIMEText(send_file, "base64", "utf-8")
    att["Content-Type"] = "application/octet-stream"
    att["Content-Disposition"] = "attachment;filename = TestReports.html"

    body = MIMEText("附件为定时任务生成的测试报告,请查收,谢谢!", _subtype='html', _charset='gb2312')

    msg = MIMEMultipart('related')
    msg['Subject'] = subject
    msg['from'] = EMAIL_SEND_USERNAME
    msg['to'] = receiver
    msg.attach(att)
    msg.attach(body)

    smtp = smtplib.SMTP()
    smtp.connect(smtp_server)
    smtp.starttls()
    smtp.login(EMAIL_SEND_USERNAME, EMAIL_SEND_PASSWORD)
    smtp.sendmail(EMAIL_SEND_USERNAME, receiver.split(','), msg.as_string())
    smtp.quit()


if __name__ == 'nt\\zhenai-testcases\\ui\\ZhenaiPad\\android\\登录注册\\results\\report.html')


7、case包:

# 1、验证allure环境是否配置成功
allure --version

# 生成测试报告,执行allure命令:  
pytest test_lover_pad.py --alluredir=./result --clean-alluredir #生成result文件

allure serve ./result  #生成报告

7.1、test_lover_pad.py

from mikasa_script.pad_ui_auto.base.LoverPadApp import LoverPadApp
from mikasa_script.pad_ui_auto.page.MemberMainPage import MemberMainPage
from mikasa_script.pad_ui_auto.utils.crm_api import *

workerId = "xx"
pad_pwd = "xx"
memberId = "xx"
crm_pwd = "xx"
phone = "xx"
verifyCode = "xx"

@allure.feature('test_app')
class TestLoverPad:
    def setup_class(self):
        # 用例开始之前,先确认用户进行到店
        sid = get_crm_sid(crm_pwd, phone, workerId, verifyCode)
        if not check_member_enter_shop(memberId, workerId, sid):
            # 若会员不存在到店记录,则进行到店操作
            sure_to_shop(workerId, memberId, sid)

    def setup(self):
        # 每个用例开始之前,启动恋爱宝
        self.app = LoverPadApp()
        pass

    def teardown(self):
        # 每个用例执行完成后,就默认返回到首页
        # self.app.clear()
        pass
        # self.app.back()

    def teardown_class(self):
        # 所有用例都结束后,则关闭企业微信
        # self.app.stop()
        pass

    # 用例一、会员登录
    @allure.story('test_member_login')
    def test_member_login(self):
        result = LoverPadApp().start().login_goto_main(workerId, pad_pwd).click_member_login().member_login(
            memberId).check_room_exist()
        # 断言包间名称是否存在
        assert True == result

    # 用例二、测试信息表界面
    @allure.story('test_info_table')
    def test_info_table(self):
        result = MemberMainPage(LoverPadApp().start()).click_info_table_button().input_member_info().check_info_view()
        assert True == result

    # 用例三、测试情感测评界面
    @allure.story('test_emo_test')
    def test_emo_test(self):
        MemberMainPage(LoverPadApp().start()).click_emo_test_button().write_emo_test()