一、背景介绍
- 由于系统项目作业流程是移动端(小程序/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)、简单画个联动流程图:
二、逻辑思路方面:
第一步:启动两个线程,分别执行小程序和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,已踩过坑请注意绕行)
- 2、谷歌浏览器驱动:下载对应版本驱动后,存放在python的Scripts目录中(where python查询)
3、微信开发者工具安全设置中的服务端口需要打开启用
4、微信开发者工具调式基础库建议不低于3.3.3版本;
四、其他说明:
- 1、minitest执行命令样例:minitest -m test.test_video -c config.json -g
2、测试报告样例展示:
- 3、minium使用说明,可参考微信官方文档:https://minitest.weixin.qq.com/#/minium/Python/readme