使用Android 提供的默认控件所开发出来的APP,如TextView、imageview、listview等。
优点:android自带SDK组件开发,与android契合度高,速度快,性能好。
缺点:开发成本高,上线时间不固定,影响敏捷交付,新版本更新需要App Store下载。
即使用Android原生控件,也包含特殊组件WebView。WebView提供了渲染HTML的功能,因此可以展示出一套新的渲染体系。混合型开发先使用原生app做一层包裹,如启动界面,主界面,进入后会通过webview的控件去加载网页。因此后续开发中就可以使用各种H5的开发方式去进行开发。
目前中国大部分APP的开发模式都是混合型的。例如微信、支付宝,原生界面是比较核心的界面,但是有大量细小功能,如果全部放在移动端中会非常庞大,因此这些功能会使用webview来加载一个远程网址,从而实现更多功能的装载,这样也有利于实时更新,如远程变化后,本地渲染就会相应的发生变化。
优点:开发成本中,性能中等,基于原生APP开发衍生而来,开发和更新不需要上线可在后台进行热启动。
主要方式是使用H5去开发,用户使用手机浏览器去访问。经过定制开发,专门为手机浏览器进行设计的。
优点:开发成本最低,性能差,可随时进行部署使用
例如图中,通过Android Studio查看开发方式,某些界面开发方式使用Activity,里面会使用类似EditText、TextView这种由Android默认提供的一套组件库,这种就是native原生开发。
native原生开发组件特征:通过UI Automator Viewer 可以查看到元素的class属性,原生组件的class属性名称中有android.widget。
webview组件特征:通过UI Automator Viewer查看class属性,可以看出class的的名称android.webkit.webview
通过Android提供的webview组件来加载html文件,可以是本地网页,也可以是远程网页。
UIautomationviewer会强制的把H5的控件的一些属性翻译成原生控件的属性。如果是文本翻译成textview;如果是图片则翻译成image之类的;如果是超链接这种特殊控件,且原生组件中没有对应类型的,则统一翻译成android.view.View.
图示中组件名称为com.uc.webview.export.WebView,不是Android默认的android.webkit.webview说明有团队在默认的webview的基础之上做了二次开发。
识别webview的小Tip:
- 正常都会有保留WebView的名称,就算做了二次开发一般也会保留此名称。
- 页面组件中名称会存在View这种,说明有部分非原生组件未被UI Automator View识别,并翻译成了View。
上图说明,手机浏览器也是原生app里面包括了webview组件,这个webview的组件去加载了远程的html。
uiautomatorviewer的分析是不完整的
浏览器的分析工具可以分析webview控件完整信息
设置成true,打开webview调试开关。
平台webview组件调试开关 | 应用webview组件调试开关 | 可调式 |
---|---|---|
as模拟器android6.0默认开启 | ✔️可调式 | |
as模拟器android7.0及以上默认关闭 | 关闭 | ❌不可调试 |
as模拟器android7.0及以上默认关闭 | 开启 | ✔️可调式 |
真机默认关闭 | 关闭 | ❌不可调试 |
真机默认关闭 | 开启 | ✔️可调式 |
微信小程序内核 某些老版本默认开启 | ✔️可调式 | |
微信小程序内核 默认关闭 | ❌不可调试 |
ps:因为这些方法不常用且有各种局限,所以使用不多,仅限于安全领域研究。使用最多的办法是通过后门开启调试开关。
webview页面必须要先打开,webview页面只有先打开的情况下,分析工具中的相关内容才会出现。
分析工具类型:
chrome://inspect
edge://inspect
firefox about:debugging
操作内容:使用手机浏览器,打开百度网站,进行页面元素分析。
注意:
- 浏览器是默认打开webview调试开关的。打开任意网站都能够被分析到
- 示例中使用的模拟器是MuMu。
- 真机中的调试方法和当前一直,确保被调试的手机app的webview调试开关已经开启。
第二步:使用 edge://inspect 进行分析
方式 | 技术栈 | 优点 | 缺点 |
---|---|---|---|
原生自动化 | uiautomator appium atx |
简单; 不依赖webview调试开关开启。 |
不易维护(对于H5的大部分属性,翻译后存在属性丢失的可能性,非常不利于维护) |
web自动化 | selenium chromedriver minitest |
易维护 | 1. 不适合混合开发(大部分APP都是混合型的,会导致流程割裂,出现原生是一部分,web是另外一部分的情况。如果大部分逻辑是在web里的,那可以使用此方法。); 2. 依赖webview调试开关开启。 |
混合自动化 | appium | 易维护,通用。 | 1. 技术复杂(因为要维持原生和web两套机制,因此技术比较复杂); 2. 依赖webview调试开关开启。 |
混合自动化技术原理示意图:
Appium本身用多个引擎管理自动化,对不同的自动化类型选择不同引擎去进行处理。
Appium自动化测试原理示意图:
论坛帖子:chromedriver下载地址与webview自动化关键代码
查看Andriod内置浏览器WebView版本:
No Chromedriver found that can automate Chrome ‘x.x.xxxx’. You could also try to enable automated chromedrivers download server feature. See appium/chromedriver.md at master · appium/appium · GitHub 96 for more details.
unknown error:Chrome version must be >=x.x.xxxx
因为APP内置的chrome webview的版本不一定与手机浏览器上的版本一致,因此chrome driver的版本在appium中是不能写死的,利用appium的chromedriver自动发现机制,让appium自动的去找适合的chromedriver。
appium chromedriver 自动发现机制中常用的appium capability参数参数:
chromedriverExecutableDir
: 指定 chromedriver 可执行文件集合的目录
chromedriverChromeMappingFile
: 允许显式指定版本对应关系
showChromedriverLog
: 让appium 日志展示 chromedriver 的日志方便排查appium capability参数设置 ——python:
def setup(self):
desired_caps = {
"platformName":"Android",
"deviceName":"emulator-5554",
"appPackage": "com.example.android.apis",
"appActivity":".ApiDemos",
"chromedriverExecutableDir":"E:\chromedriver_webview_test",
"chromedriverChromeMappingFile":"E:\chromedriver_webview_test\mapping.json",
"showChromedriverLog":True
}
self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)
# 使用隐式等待
self.driver.implicitly_wait(10)
appium capability参数设置 ——java:
//简单方案(不推荐):把chromedriver版本写死
caps.setCapability("chromedriverExcutable","E:\chromedriver_webview_test\chromedriver_2.42");
//完善的自动发现机制
caps.setCapability("chromedriverExecutableDir","E:\chromedriver_webview_test");
caps.setCapability("chromedriverChromeMappingFile","E:\chromedriver_webview_test\mapping.json");
caps.setCapability("showChromedriverLog",True);
appium context上下文机制:
怎么在原生和webview之间进行切换? 当前原生APP名为native,代表当前属于原生自动化环境下,当通过api切换到webview后,会有一个“WEBVIEW_XXXX”格式的进程名,说明当前处于webview自动化环境下。利用不同的名称来识别是做原生测试还是webview测试。
Api说明:
contexts
,第一个是原生 NATIVE,剩下的为 webviewcurrent_context
switch_to.context('WEBVIEW_XXXX')
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.wait import WebDriverWait
"""
模拟器:avd模拟器
测试测试:模拟器再带的ApiDemos
"""
class TestWebView:
def setup_class(self):
desired_caps = {}
desired_caps['platformName'] = 'Android'
desired_caps['platformVersion'] = '6.0'
desired_caps['deviceName'] = 'emulator-5554'
desired_caps['appPackage'] = 'com.example.android.apis'
desired_caps['appActivity'] = '.ApiDemos'
desired_caps['noReset'] = 'true'
#python appium client 2.x 会默认使用w3c,因此此处使用chromeOptions参数吧w3c
desired_caps['chromeOptions']={'w3c':False}
#关键部分:设置chromedriver所在路径
desired_caps['chromedriverExecutableDir']='E:\chromedriver_webview_test'
#设置显式指定chromedriver和webview版本对应关系
#desired_caps['chromedriverChromeMappingFile']='E:\chromedriver_webview_test\mapping.json'
#开启chromedriverlog
desired_caps['showChromedriverLog']=True
self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)
# 使用隐式等待
self.driver.implicitly_wait(10)
def teardown(self):
self.driver.quit()
def test_webview(self):
self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,"new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text(\"Views\").instance(0))").click()
self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,"new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text(\"WebView\").instance(0))").click()
#打印上下文情况
#['NATIVE_APP', 'WEBVIEW_com.example.android.apis']
print(self.driver.contexts)
#webview组件加载慢的时候不一定会及时出现webview上下文,最好显示等待
WebDriverWait(self.driver,5).until(lambda driver:len(self.driver.contexts)>1)
#最好显示指定,在多进程同时有webview的时候,最后一个context不一定是当前app的webview
self.driver.switch_to.context('WEBVIEW_com.example.android.apis')
#局部导入By
from selenium.webdriver.common.by import By
self.driver.find_element(By.LINK_TEXT,'Hello World! - 1').click()
#查看当前所在context
#WEBVIEW_com.example.android.apis
print(self.driver.current_context)