UI自动化测试:小程序+web浏览器联动测试思路(Python)

一、背景介绍

  • 由于系统项目作业流程是移动端(小程序/APP) + 后台web端联合使,业务场景为:客户在小程序(或APP)发起视频来电呼叫,工作人员在web页面收到视频来电并接听,最终实现双方视频/语音/拍照等功能。主要需满足以下要求:
  • 1、针对此项目业务特点:需实现跨平台(移动+web端)联动测试执行;
  • 2、需真机测试:因引用到手机麦克风、摄像头来实现视频通话和拍照功能,另外也确保更接近真实使用场景;故必须要求真机跑流程;
  • 3、需要生成测试报告:对于流程每个环节需要截图保存,另外移动端和web端的截图需要整合到同个报告内;

二、构思设计及主要功能点

1)、终端方面:

  • 移动端:APP可使用appium 框架,小程序可使用mimium框架(以下讲解将以小程序为例);为满足真实场景,最终运行都是真机方式下进行测试。
  • web端:使用selenium框架实现,引用谷歌浏览器测试;
  • 辅助工具:手机真机、adb支持(安卓调试桥)、微信开发者工具、小程序项目包

2)、联动方面:

  • 两端并行运行:使用threading.Thread起两个线程,确保小程序和web端口在并行下执行;
  • 两端依赖/同步性:每个环节各自定义一个状态,即依赖的关系使用对方的环节状态判断,来满足依赖的同步性;

3)、测试结果方面:

  • 测试报告:引用小程序方面的mimium 框架生成测试报告,而针对于web端的截图将合到mimium中合并输出;

4)、简单画个联动流程图:

流程图.png

二、逻辑思路方面:

第一步:启动两个线程,分别执行小程序和web浏览器
  • 1、小程序(create_auto_miniapp):引用mimium框架来触发执行微信开发者工具,进而跑对应小程序项目包,并联动真机调试测试;
  • 2、web端(create_auto_web):引用selenium来触发浏览器的自动执行;
  • 3、注意事项:此块文件和测试case命名需要满足unitest的命名规则(即mimium是在unitest上跑case的);
class TestAutoZds(minium.MiniTest):

    @classmethod
    def setUpClass(cls):
        """
        web_state: 0初始化、10打开登录页面、20在工作台页面、30来电待接听页面、40视频中页面、50视频结束,提交状态页面、22工作台页面(已视频结束后)、11退出页面(登录页面)、99程序结束
        miniapp_state: 0初始化、10打开登录页面、20在案件列表页面、30呼叫坐席页面、40视频中页面、22工作台页面(视频结束后)、11退出页面(登录页面)、99程序结束
        """
        cls.synchro_state = {'miniapp_state': '0', 'web_state': '0'}
        cls.test_result = []  # 初始化测试结果
        cls.test_data = DataOperation().test_data('call_video.yml')  # 获取测试数据
        cls.case_data, cls.config = cls.test_data['case'], cls.test_data['config']
    def test_call_video_001(self): 
        self.case_code = 'case_001' #  执行的用例编号
        thread_miniapp = threading.Thread(target=self.create_auto_miniapp) # 小程序线程1
        thread_web = threading.Thread(target=self.create_auto_web) # web端浏览器线程2
        logger.info("启动线程")
        thread_miniapp.start()
        thread_web.start()
        logger.info("等待线程")
        thread_miniapp.join()
        thread_web.join()
        # 判断最终测试是否通过
        if self.test_result:
            self.assertNotIn(False, self.test_result)
        else:
            self.assertTrue(False)
第二步:线程1 --> 小程序执行模块:
    def create_auto_miniapp(self):
        self.app_case_data = self.case_data[self.case_code] if self.case_code else self.case_data # 初始数据
        logger.info("小程序-打开登录页")
        self.app.navigate_to(self.app_case_data['miniapp_data']['miniapp_url'])
        time.sleep(3)
        case_name = "小程序-点击连接视频"
        logger.info(case_name)
        if self.synchro_work('web_state', '20'): # 此处即引用到依赖的web端状态,来判断同步性
            current_time = datetime.datetime.now().time()
            self.page.get_element("view.caseList-contennt-right", text_contains='连接坐席').click()
            if current_time >= datetime.time(20, 0) or current_time < datetime.time(11, 0): 
                time.sleep(3)
                x, y = map(int, get_screen_size(745 / 1080, 1425 / 2400))
                logger.info(f'*非工作时间发起视频,点击确定按钮坐标为x={x},y={y}')
                self.native.click_coordinate(x=x, y=y)  # 此处依据手机屏幕尺寸来计算位置,并触发点击动作
            self.synchro_state['miniapp_state'] = '30'
            if self.synchro_work('web_state', '40', count_time=35):
                case_name = "小程序-视频中进行拍照"
                logger.info(case_name)
                self.page.get_element("image.image-list-photo").click()  # 小程序点击拍照
                if not self.assertPageContainsText('1/1', max_timeout=15):
                    logger.error('*异常:图片拍照上传失败,不符合1/1的文本内容,请二次确认...')
                    self.test_result.append(False)
                self.capture(case_name)
            else:
                self.capture('(测试失败)' + case_name)
                self.test_result.append(False)
                error_data = "*测试失败:小程序未等到web座席端视频中状态(40)"
                logger.error(error_data)
                raise AssertionError(error_data)
            if self.synchro_work('web_state', ['22', '11', '99']):
                self.synchro_state['miniapp_state'] = '99'
                time.sleep(2)
        else:
            self.test_result.append(False) 
            error_data = "*测试失败:小程序未等到web座席端登录成功状态(20)"
            logger.error(error_data)
            raise AssertionError(error_data)
        self.test_result.append(True) # 依据此变量来最终判断是否测试通过(只要列表有False元素下即不通过)
  • 重点说明1、依据类属性cls.synchro_state状态来判断,满足与web端的同步及依赖性;
  • 重点说明2、运行截图:可调用框架自带的capture方法,即可保存当前小程序状况的图片
第三步(与小程序并行):线程2 --> Web端浏览器执行模块
    def create_auto_web(self):
        self.web_case_data = self.case_data[self.case_code] if self.case_code else self.case_data # 初始数据
        logger.info('**打开谷歌浏览器')
        chrome_option = ChromeOption('case').options(self.config['chrome_config'])
        driver = webdriver.Chrome(options=chrome_option) # 此块options引用的自定义配置了浏览器的信息
        driver.implicitly_wait(15)  # 设置隐式等待
        try:
            logger.info('**打开web端登录页面')
            driver.get(self.config['web_host'])
            logger.info('**打开web端单点登录页面')
            driver.find_element(By.XPATH, '//img[@src="./assets/img/login_round.png"]').click()
            try:
                element = WebDriverWait(driver, 1).until(
                    EC.presence_of_element_located(
                        (By.XPATH, '//div[@class="login-box-tit"]'))
                )
                logger.info(f"**单点页面打开成功:{element}")
            except Exception as e:
                logger.debug('单点页面登录出现异常情况:' + str(e))
            driver.save_screenshot(self.miniapp_save_screenshot('单点登录页面'))
            driver.back() 
            logger.info('**输入账号&密码')
            placeholder = driver.find_element(By.XPATH, '//input[@placeholder="账号"]')
            placeholder.clear()
            placeholder.send_keys(self.web_case_data['web_data']['web_account']['account'])
            driver.find_element(By.XPATH, '//input[@placeholder="密码"]').send_keys(
                self.web_case_data['web_data']['web_account']['password'])
            logger.info('**点击登录')
            driver.save_screenshot(self.miniapp_save_screenshot('web坐席端-登录页面'))  # 保存图片
            if self.synchro_work('miniapp_state', ['20', '30']):
                driver.find_element(By.XPATH,
                                    '//button[@class="ant-btn ng-star-inserted ant-btn-primary ant-btn-lg"]').click()
            else:
                self.test_result.append(False)
                error_data = "**测试失败:web坐席端未等到小程序登录成功页面(案件列表页面)"
                logger.error(error_data)
                raise AssertionError(error_data)
            driver.save_screenshot(self.miniapp_save_screenshot('web坐席端-工作台页面'))  # 保存图片
            self.synchro_state['web_state'] = '20'
            logger.info('**等待,接听小程序呼叫的视频')
            if self.synchro_work('miniapp_state', '30', 30):
                answer_button = driver.find_element(By.XPATH, '//button[@class="btn btn-lg btn-danger"]')
                self.synchro_state['web_state'] = '30'
                answer_button.click()
                self.synchro_state['web_state'] = '40'
                driver.save_screenshot(self.miniapp_save_screenshot('web坐席端-视频中页面'))  # 保存图片
                logger.info('**web坐席端,点击结束视频通话')
                end_button = driver.find_element(By.XPATH, '//button[@class="btn btn-danger"]')
                end_button.click()
                self.synchro_state['web_state'] = '50'
                self.synchro_state['web_state'] = '11'
                logger.info("**web端坐席模块执行完毕")
                if self.synchro_work('miniapp_state', ['22', '11', '99']):
                    self.synchro_state['web_state'] = '99'
            else:
                self.test_result.append(False)
                error_data = "**测试失败:web坐席端未等到小程序呼叫来电"
                logger.error(error_data)
                raise AssertionError(error_data)
        except Exception as e:
            logger.error(f"发生错误:{e}")
            self.test_result.append(False)
            raise
        finally:
            self.test_result.append(True)
            driver.close()
  • 重点说明1、上述增加了浏览器基础配置(即options参数):此块是用于打开浏览器时,默认给了自定义的基础配置数据(是为了减少一些浏览器权限及提示而影响测试流程);
        #摘录部分配置
        option.add_argument('start-maximized') # 窗口最大化
        option.add_argument('--log_level=3')  # 取消控制台一些无用输出信息
        option.add_argument('--disable-gpu')
        option.add_argument('--ignore-certificate-errors') # 忽略证书错误,不需要手动点高级选项
        option.add_experimental_option('excludeSwitches',
                                       ['enable-automation', 'enable-logging'])  # 去除小黄条和取消控制台无用信息
        # 默认chrome取消弹出记住密码框,允许使用麦克风、摄像头、地图定位、通知权限(1允许2不允许)
        option.add_experimental_option('prefs',
                                       {'credentials_enable_service': False, 'profile.password_manager_enable': False,
                                        'profile.default_content_setting_values.media_stream_mic': 1,  # 麦克风
                                        'profile.default_content_setting_values.media_stream_camera': 1,  # 摄像头
                                        'profile.default_content_setting_values.geolocation': 1,  # 定位
                                        'profile.default_content_setting_values.notifications': 1})  # 通知
  • 重点说明2、web截图合并到报告的解决办法:是先引用小程序的方法截图后,再引用了selenium的save_screenshot截图方法来覆盖图片。即为了达到web端截图可合并到minium测试报告内呈现;
    def miniapp_save_screenshot(self, image_name):
        """借助小程序截图,实现web端图片存入测试报告中"""
        path_capture = self.capture(image_name)
        time.sleep(0.5)
        return path_capture

重点说明3:两端同步/依赖性控制,依据循环的计时器来满足同步依赖性,超时即自动退出;

    def synchro_work(self, system_type, waiting_state, count_time=80):
        count = 0
        while count < count_time:
            logger.info(f'&当前系统状态:{self.synchro_state}')
            if isinstance(waiting_state, str):
                if str(self.synchro_state[system_type]) == str(waiting_state):
                    logger.info(f'&同步完成:{system_type}等待状态{waiting_state}')
                    return True
            elif isinstance(waiting_state, list):
                for item in waiting_state:
                    if str(self.synchro_state[system_type]) == str(item):
                        return True
            count += 1
            time.sleep(1)
            logger.info(f'&同步等待:{system_type}->{waiting_state},第{count}秒')
        return False

三、配置项方面说明:

  • 1、case案例的测试数据配置,引用的yaml文件管理(即确保测试数据与代码隔离,且可以拓展增加case_002、003等测试场景)
local_var:
  remarks: 发起视频流程
  assert_case: &assert_case XXXXXX0001
  history_case: &history_case XXXX0002
case_001:
  case_name: 发起视频流程
  miniapp_data:
    miniapp_url: "../../test/login/login?phone=18800000000"
    assert_case: *assert_case
    history_case: *history_case
  web_data:
    web_account: {"name": "test","account": "test","password": "test#123"}
  remarks: 无
  • 2、小程序的开发者工具配置(此块使用的minium执行命令时,即“ -c config.json ”读取使用)
{
  "project_path": "C:/Users/test/Desktop/test20240201/client-miniapp-test-v1.1.6",  # 小程序程序包位置
  "dev_tool_path": "D:/微信web开发者工具/cli.bat", #微信开发者工具启动位置
  "debug_mode": "debug",
  "auto_authorize": true,
  "enable_app_log": false,
  "platform": "Android", # 是否需要真机调试,如不需要可删除此字段
  "device_desire": {
    "serial": "hefayp4hztroif6d"  # 真机的设备名,可adb devices查看
  }
}
  • 3、注意事项
  • 1、移动端执行需要adb支持,可自行下载platform-tools(注意版本需不低于1.0.36,已踩过坑请注意绕行)


    1、adb注意点
  • 2、谷歌浏览器驱动:下载对应版本驱动后,存放在python的Scripts目录中(where python查询)
  • 3、微信开发者工具安全设置中的服务端口需要打开启用


    2、开发者工具设置注意点
  • 4、微信开发者工具调式基础库建议不低于3.3.3版本;


    3、小程序测试基础库版本注意点2

四、其他说明:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容