实验环境由Anaconda进行版本管理。
版本:conda 22.9.0
下载链接:https://www.anaconda.com/download/
实验中使用Python编写代码,利用Python自带的单元测试库Unittest进行测试。
版本:Python 3.10.10
本实验中的Python利用Anaconda进行下载和管理,如需单独下载可以访问
https://www.python.org/downloads/release/python-31010/
实验中采用雷电模拟器模拟安卓手机系统进行测试。
系统版本:Android 9
雷电模拟器版本:9.0.43
实验中使用Android SDK 辅助模拟器与Appium通信,控制App。
版本:Android-13.0(通过 Android Studio 安装)
下载链接:https://developer.android.google.cn/studio
实验中使用Appium控制模拟器和测试端口进行测试。
版本:1.22.3
通过以下命令分别安装 Appium、Appium-doctor和Appium-Python-Client:
npm install -g appium
npm install appium-doctor -g
pip install Appium-Python-Client
Appium 桌面版的下载链接为:https://github.com/appium/appiumdesktop/releases/tag/v1.3.1
Appium 是一个用 Node.js 写的、暴露 REST API 的 WEB 服务器。它接受来自客户端的连接,监听命令并在移动设备上执行,答复 HTTP 响应来描述执行结果。
通过Appium启动App,需要在Appium中指定设备相关信息和应用相关信息。需要获取的信息包括操作系统、设备名、设备系统版本号、App包名和App主活动。
我们使用雷电模拟器在设备内的 “设置” 界面查找相关设备信息,可以得到操作系统为Android系统,操作系统版本为Andriod 9。
再使用adb获取设备名,使用命令行命令adb devices获取到目前连接的所有设备,如下图的设备信息,雷电模拟器的设备名为emulator-5554 device。
此外,还需要获取我们所要运行的App对应的包名与主活动名。首先需要在模拟器中运行对应应用,进入应用主界面(这里以Bilibili作为示例),在模拟器所在目录下使用命令行命令 adb shell dumpsys window | findstr mCurrentFocus,得到两个参数
appPackage = "tv.danmaku.bili"
appActivity = "tv.danmaku.bili.MainActivityV2"
为使后续实验中能够在Python脚本内控制App的各个控件,需要首先获取各个控件的id等相关信息。本实验使用Appium桌面版连接模拟器获取相关信息。
首先打开Appium桌面版,在设置相关信息的位置按操作系统名(platformName),设备系统版本号(platformVersion),设备名(deviceName),App包名(appPackage),App主活动名(appActivity)进行设置后,开启连接,模拟器会自动启动设定好的App并进入到主界面。以下是用于设置参数的json代码:
{
"platformName": "Android",
"platformVersion": "9",
"deviceName": "emulator-5554 device",
"appPackage": "tv.danmaku.bili",
"appActivity": "tv.danmaku.bili.MainActivityV2"
}
进入Appium桌面版的界面后,通过点击界面上相应的元素即可获得客户端元素的ID、xpath、ClassName以及继承关系等信息。
在 Python 脚本中调用os库可以使用系统命令,因此我们在Python中使用如下代码:先将可能正在后台运行的server端清除,以避免不必要的错误;然后打开appium,让appium使用4723端口。
os.system('adb kill-server')
os.system('appium -a localhost -p 4723')
此外,由于os.system( )会将未完成的系统调用阻塞,所以我们需要引入多线程机制,将appium挂载在后台进程,而在另一个线程中进行模块测试。
我们将Bilibili的driver控制端和实验中需要用到的控制函数封装为BiliOperator类,通过以下代码对driver端进行参数设置,并启动Bilibili客户端。
self.desired_caps = {}
self.desired_caps["platformName"] = "Android"
self.desired_caps["platformVersion"] = "9"
self.desired_caps["deviceName"] = "emulator-5554 device"
self.desired_caps["appPackage"] = "tv.danmaku.bili"
self.desired_caps["appActivity"] = "tv.danmaku.bili.MainActivityV2"
self.desired_caps["unicodeKeyboard"] = True
self.desired_caps["resetKeyboard"] = True
self.desired_caps["noReset"] = True
self.desired_caps["newCommandTimeout"] = 100000
self.host = '127.0.0.1'
self.port = 4723
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', self.desired_caps)
我们将控制App的类封装好后,使用一个类继承unittest的TestCase类,在其中设置不同的测试调用相应的控制App的类的方法,从而利用unittest进行自动化测试。
类的继承代码为:
class TestCalculator(unittest.TestCase):
在测试中用到的部分断言:
self.assertNotEqual(len(titles), 0)
self.assertNotEqual(len(descs), 0)
并在测试开始后,使用下面代码,运行unittest:
unittest.main()
在实验中,我们准备了一些关键词(如China Daily、SpaceX等),并遍历这些关键词进行搜索。此外,我们还通过桌面版Appium的Inspector窗口,提前获取了实验中所需元素的ID、xpath等属性。我们首先通过driver.find_element选中搜索框,并且获取到搜索框的元素。通过向获取到的搜索框元素发送关键词本文,实现搜索框的输入,然后调用安卓键盘的回车键,开始搜索。进入搜索结果界面后,通过driver.find_elements获得所有ID含有title的元素,遍历这些元素并且获取它们的text属性,从而获得搜索到的所有视频的标题。
sbox = self.driver.find_element(By.ID, ('search_src_text'))
sbox.send_keys(keyword)
time.sleep(1)
self.driver.press_keycode(AndroidKey.ENTER)
time.sleep(3)
eles = self.driver.find_elements(By.ID, 'title')
for ele in eles:
titles.append(ele.text)
time.sleep(15)
由于driver端只进行发送信号的工作,并不会检查server端对App操作的完成情况,因此在每次driver的操作后,都需要让进程阻塞一段时间,使得Appium对于App界面的屏幕操作(如点击等)、键盘操作(如回车等)和抓取操作能够充分完成。
由以下代码实现点赞按钮的获取和点击:
self.driver.find_element(By.ID, ('frame_recommend')).click()
time.sleep(1)
我们获取评论界面所有ClassName为TextView的元素,然后对元素进行处理和筛选,得到相应视频的评论区中已经加载出的评论。抓取和处理评论的代码如下:
self.driver.find_element(By.ID, ('tab_sub_title')).click()
time.sleep(1)
desctext = self.driver.find_elements(By.CLASS_NAME, ('android.widget.TextView'))
foundcmt = False
for desc in desctext:
if not foundcmt:
if ":" in desc.text:
foundcmt = True
else:
continue
if len(desc.text) < 5:
continue
descs.append(desc.text)
time.sleep(3)
除了对Bilibili的视频相关功能进行测试,我们还对Bilibili客户端的购物功能进行了自动化测试。首先用driver.press_keycode(AndroidKey.BACK)模拟安卓机器的回退,回退至主界面后转移到Bilibili会员购界面。进入会员购界面后,通过点击搜索框并使用send_keys(keyword)发送搜索信息,进入商品的搜索结果界面(注意,在此处不能使用 driver.press_keycode(AndroidKey.ENTER),经测试Bilibili客户端在会员购界面不能相应回车键)。随后,我们任意制定了一个商品的序号,选中进入。最后,通过筛选元素的方式找到购物车按钮,将该商品添加至购物车。
会员购搜索、选择商品和添加购物车的测试代码如下:
print("Searching for " + keyword + "...")
sbox = self.driver.find_element(By.ID, ('search_edit'))
sbox.send_keys(keyword)
time.sleep(6)
self.driver.find_element(By.ID, ('mall_id_search_page_actionbar_commit')).click()
time.sleep(10)
self.driver.find_elements(By.CLASS_NAME, ('android.webkit.WebView'))
time.sleep(10)
eles = self.driver.find_elements(By.CLASS_NAME, ('android.widget.Image'))
eles[4].click()
time.sleep(8)
eles = self.driver.find_elements(By.CLASS_NAME, ('android.view.View'))
for ele in eles:
if u"购物车" in ele.text:
ele.click()
break
time.sleep(5)
self.driver.press_keycode(AndroidKey.BACK)
time.sleep(1)
self.driver.press_keycode(AndroidKey.BACK)
time.sleep(1)
self.quit_search()
self.quit_buy()
在使用Bilibili客户端的过程中,可能会随机弹出很多界面。我们的App自动化测试脚本具有对于这种异常功能的容错能力,比如对于进入界面时弹出的青少年模式选项的处理代码如下:
try:
iknow = self.driver.find_element_by_id('text2')
if iknow:
iknow = self.driver.find_element_by_id('button')
print('Adolescent protect found!')
iknow.click()
except:
print('Adolescent protect not found!')
通过在App启动时运行上述代码,使得自动化测试脚本不会因为选择青少年模式的弹窗而受到影响。
实验中,由于App本身功能复杂,我们在自动测试过程中遇到了一些问题,并采用了灵活高效的方式解决。
除了上述提到的青少年模式,Bilibili客户端在进入会员购页面是还会概率性展示一个广告页面,阻碍Appium找到所需的页面元素(存在遮挡)。我们观察到,该广告并没有退出按键,但该广告页面会自动在几秒后消失,因此选择在点击会员购按钮进入会员购分区后先等待数秒,再进行搜索操作,这样就能避免该广告页面带来的影响。
对于会员购,我们遇到的另一个问题是在进入搜索结果界面后,一直无法访问到界面元素导致退出。经过大量的实验,我们发现无论加载时间有多长、或是反复退出重进,都会有一个类名为WebView的List对象阻碍我们的脚本获取页面元素,因此我们使用self.driver.find_elements(By.CLASS_NAME, ('android.webkit.WebView'))
这一段代码简单地对 WebView 类对象进行一次访问,使得它自动退出。
此外,我们还注意到,由于搜索功能需要访问互联网(无论是搜索视频还是搜索会员购商品),需要不定的加载时间,我们本来计划使用try关键字忙等访问,但后来发现由于访问页面元素本身较慢,以及Python调用Appium的调用情况,这样会大幅拖慢程序运行,我们采用两种方式解决:1.部分页面加载所需时间固定,采用忙等的方式,直接阻塞进程等待页面加载;2.对于加载时间不定的页面,采取等待反馈的方式,代码如下,其中func为任意反馈函数(可以是Lambda表达式):
def wait(self, func, time=10):
res = WebDriverWait(self.driver, timeout=time).until(func)
return res