作者自从 2020 年中有段时间发了几篇文章之后,一直忙于工作,没有机会发文,如今赋闲在家,得以发表一些测试心得。
作者在过去一年多内,在某大型游戏公司负责游戏 SDK 业务的质量保障工作,在客户端自动化测试方面有一定的探索和实践。
相信这个问题的答案,大家心里都有答案。有人说是:发现 bug,验证功能;有人说是:降低回归测试成本;有人说是:提高测试能力;不一而足。
对于 SDK 业务而言,无论是登录还是支付,稳定性要求都非常高,不允许出现失误。软件的变动(包括客户端和服务端)是否会影响功能,如果能在客户端层面进行自动化的确认,不论对质量,还是效率,都有很重要的意义。
除了测试能力对开发服务开放以外,我们还需要对运维服务。
自 2020 年以来,世界经济都在下行,各家公司整体都在降本增效,以应对经济下行的压力。与此同时,微服务和多中间件架构已经成为了各家公司的标配,这些中间件和服务器平时实际使用率并不高,存在优化的空间。公司倾向于缩减硬件成本,运维也有 KPI 要求。对于测试人员的影响就是:服务端和中间件的硬件变动需要一定的回归测试来验证。在我们团队中,这种情况不是一次两次,而是在整个降本周期内一直存在。
任何一个决策,都应该考虑可行性,客户端自动化测试也不例外。
根据测试金字塔理论,客户端自动化测试因为以下几个问题,导致其性价比不如集成测试。
因为业务独特的情况,这几个问题均可得到一定程度的削弱,可行性反而较高:
客户端自动化测试不等于兼容性测试,不需要测试各种机型,各种屏幕的设备。那是兼容性测试的工作,自动化测试的核心还是降低测试执行成本,确认质量符合要求。
任何测试都会存在干扰因素,不可能 100% 稳定:硬件设备断电,网络不稳定,进程卡死,服务端测试环境故障,服务端和客户端配置错误,测试框架出 Bug,甚至真机设备上的系统弹窗都会导致测试用例失败。误报确实是衡量客户端测试有效性的重要因素,但不应该成为衡量的唯一因素。
我们应该秉持的态度是:一方面在误报出现时,尽快由测试人员介入确认该误报;另一方面不断完善测试框架,尽可能消除这些干扰因素。
团队成员对 Python 和 Airtest 比较熟悉,并且之前有成熟的测试项目。Pytest 有丰富的测试插件,开发效率较高。此外可以自己编写钩子,本测试框架在配置测试环境、生成测试报告、发送测试邮件等环节多次用到了钩子。为了快速出成果,故选用了 Pytest + Airtest。
此外,为了紧跟前沿测试,使用了行为驱动开发的思想。测试驱动开发的思想,想必大家都有所耳闻。行为驱动开发,其实是前者的一种升级。应用在测试领域,就是行为驱动测试,它定义了每个测试步骤,并赋予了这些测试步骤一定的代码,使其能够实现这个测试步骤。如果只做到这些,那还不够诱人,行为驱动测试允许测试步骤的复用。具体示例请参考本文:4.1 测试脚本
Behavior-driven development is an extension of test-driven development, a development process that makes use of a simple DSL. These DSLs convert structured natural language statements into executable tests. The result is a closer relationship to acceptance criteria for a given function and the tests used to validate that functionality. As such it is a natural extension of TDD testing in general.
正如前文所说,可以创建客户端模拟器用于客户端自动化测试,可以降低硬件成本。对于这一设想的可行性,作者花了较多时间做了深度调研,打通了模拟器客户端自动化测试的各个环节。
对于安卓设备有:
# 切换到Android sdk的根目录
cd /Users/<username>/Library/Android/sdk
# 创建模拟器
./tools/bin/avdmanager create avd -n <emulator_name> -k "system-images;android-22;google_apis;arm64-v8a"
# 重命名模拟器
./tools/bin/avdmanager move avd -n <emulator_name> <new_emulator_name>
# 删除模拟器
./tools/bin/avdmanager delete avd -n <emulator_name>
# 启动模拟器
./emulator/emulator -avd Pixel_5_API_22 & bg
# 关闭模拟器
kill `ps aux | grep Android/sdk/emulator/qemu | xargs | awk -F ' ' 'END{print $2}'`
对于 iOS 设备:
# 创建模拟器,机型iPhone-13-Pro-Max,系统版本iOS-15-2
xcrun simctl create my_iphone_pro_max com.apple.CoreSimulator.SimDeviceType.iPhone-13-Pro-Max com.apple.CoreSimulator.SimRuntime.iOS-15-2
# 删除模拟器
xcrun simctl delete my_iphone_pro_max
# 启动模拟器
xcrun simctl boot my_iphone_pro_max
# 关闭模拟器
xcrun simctl shutdown my_iphone_pro_max
# 检查设备的运行状态
xcrun simctl bootstatus my_iphone_pro_max
# 找到正在启动的模拟器设备
xcrun simctl list | grep Booted
# 给设备起一个别名
xcrun simctl 7CD4F140-0BDC-4AE9-8FA0-960D33055044 rename my_iphone_pro_max
simctl
提供了更为丰富的 api,可以实现更多的功能。
使用 Python 代码封装上述的命令,即是整个框架的底层工具包。
连接模拟器的教程,网易的 Airtest 文档朱玉在前,我就不做赘述,请参考:
# Tiffany.py
from poco.drivers.ios import iosPoco
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from airtest.core.api import auto_setup, connect_device
from cases.utils import config_parser
class Tiffany(object):
"""
底层工具箱
"""
def __init__(self, app_name: str) -> None:
self.app_name = app_name
def make_android_poco(self) -> None:
auto_setup(__file__)
connect_device(uri=config_parser.get("DEVICE", "EMULATOR"))
self.poco = AndroidUiautomationPoco(
pre_action_wait_for_appearance=3,
action_interval=0.5,
poll_interval=0.4
)
def make_ios_poco(self) -> None:
auto_setup(__file__, devices=[config_parser.get("DEVICE", "IPHONE_URI"), ]
connect_device(config_parser.get("DEVICE", "IPHONE_URI"))
self.poco = iosPoco(
pre_action_wait_for_appearance=4,
action_interval=0.1,
poll_interval=0.2
)
对于 Android 设备,有祖传的 adb 命令来操作:
# 列出连接的设备
./platform-tools/adb devices
# 列出app
./platform-tools/adb -s Pixel_5_API_22 shell pm list packages
# 安装测试应用
./platform-tools/adb -s Pixel_5_API_22 install <path_to_apk>.apk
# 启动pocoservice
./platform-tools/adb shell am start -n com.netease.open.pocoservice/com.netease.open.pocoservice.TestActivity
# 启动测试应用
./platform-tools/adb -s Pixel_5_API_22 shell am start com.example.sdk.demo.activity.SplashActivity
# 卸载测试应用
./platform-tools/adb -s Pixel_5_API_22 uninstall com.example.sdk.demo
对于 iOS 设备,有xcrun simctl
命令来操作:
# 安装应用到指定的设备
xcrun simctl install my_iphone_pro_max resources/packages/IOSDemoMainland.app
# 卸载指定应用,根据bundle_id
xcrun simctl uninstall my_iphone_pro_max com.netease.boltrend.sdk.demo
# 列出已安装的应用
xcrun simctl listapps my_iphone_pro_max
# 查看app信息
xcrun simctl appinfo my_iphone_pro_max com.apple.mobilesafari
# 启动app
xcrun simctl launch my_iphone_pro_max com.apple.mobilesafari
# 关闭app
xcrun simctl terminate my_iphone_pro_max com.apple.mobilesafari
# 打开url
xcrun simctl openurl my_iphone_pro_max https://cn.bing.com/
# Sure, you can use the global macOS shortcut to capture a screenshot of a simulator (⇧⌘4), but if you intend to use those for your App Store listing, you’re much better off using the io subcommand:
$ xcrun simctl io booted screenshot app-screenshot.png
# You can also use this subcommand to capture a video as you interact with your app from the simulator (or don’t, in the case of automated UI tests):
$ xcrun simctl io booted recordVideo app-preview.mp4
此外,tidevice 也是好用的工具。详细用法请参考:阿里 Python 自动化工具 tidevice 使用指南
总体设计架构如下:
下面重点讲讲测试脚本编写:
针对一个邮箱登录的测试场景,可以用 gherkin 语言定义下面的测试步骤:
# mail_login_test.feature
Feature: mail_login_test
testcases about mail login
Scenario: mail login with correct password
Given start app and initialize settings
And open the main board
When I admit agreements on login board
And I go to the mail login board
And I input correct password and login
Then assert login success
每个测试步骤,都有对应的
# mail_login_test.py
from pytest_bdd import given, then, when
@given('start app and initialize settings')
def start_app_and_initialize_settings() -> None:
"""
启动app,初始化设置
"""
pass
@given('open the main board')
def open_the_main_board() -> None:
"""
打开登陆面板
"""
pass
@when("I admit agreements on login board")
def admit_agreements_on_login_board() -> None:
"""
同意用户协议
"""
pass
@when("I go to the mail login board")
def to_mail_login_board() -> None:
"""
到邮箱账号登录面板
"""
pass
@when("I input correct password and login")
def input_correct_password_and_mail_login() -> None:
"""
邮箱登录,输入正确的密码
"""
pass
@then('assert login success')
def login_success() -> None:
"""
断言登陆成功
"""
pass
@scenario('mail_login.feature', 'mail login with correct password')
def test_mail_login_with_correct_password() -> None:
"""
邮箱登录测试用例
"""
pass
可复用的步骤可以放到conftest.py
中,比如:start app and initialize settings
,open the main board
,I admit agreements on login board
,代码示例如下:
# conftest.py
def initialize_environment(apk_name: str) -> None:
"""
初始化测试环境
:param apk_name: apk包名
:return: None
"""
pass
对于支付宝支付的测试用例,需要登陆操作,显然可以复用邮箱登录的步骤。
# alipay_test.feature
Feature: alipay_test
testcases about alipay
Scenario: 支付宝支付,编辑商品信息,拉起支付窗口,验证支付是否调起
Given start app and initialize settings
And open the main board
When I admit agreements on login board
And I go to the mail login board
And I input correct password and login
Then assert login success
When I prepare the order information with CNY
And I place an order of alipay
Then alipay cashier desk will show up
由于复用了邮箱登录的步骤,上述拉起支付宝窗口的用例,只需要专注于下订单拉起支付的逻辑。
# alipay_test.py
@when("I prepare the order information with CNY")
def prepare_order_information() -> None:
"""
准备订单信息
"""
pass
@when("I place an order of alipay")
def place_an_order() -> None:
"""
下订单
"""
pass
@then("alipay cashier desk will show up")
def assert_alipay_cashier_desk_shows_up() -> None:
"""
展示收银台
"""
pass
Pytest 有钩子支持二次开发,在pytest_runtest_makereport
钩子内。
def pytest_runtest_makereport(item, call) -> None:
"""
统计测试数据
"""
pass
在钩子pytest_runtest_makereport(item, call)
编写数据持久化逻辑。若使用 jenkins,则直接往数据库写数据。若使用自研服务,则调用服务端接口,回传测试数据。
def pytest_sessionfinish(session, exitstatus) -> None:
"""
调用调度服务的接口,上传测试数据
"""
pass
jinja2
库支持编写邮件网页,email
库支持发送邮件。
调度服务和 Web 前端,可以用 jenkins 代替。相应地,开发成本降低,可以快速出效果,但是展示效果要打折扣,数据处理逻辑也更依赖 pytest 钩子的二次开发。
服务端框架推荐:Flask,可以快速出成果。以创建任务为例:
@app.route("/testtask/create", methods=["POST"])
def create_test_task() -> str:
pass
本地机器生成私钥和公钥
$ ssh-keygen -t rsa
把公钥存放到远程机器上
$ scp ~/.ssh/id_rsa.pub zhancc@192.168.94.111:~/.ssh/id_rsa.pub
把公钥添加到远程机器的authorized_keys
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
远程执行命令
ssh zhancc@192.168.94.111 "/Users/<username>/Library/Android/sdk/tools/bin/avdmanager create avd -n <emulator_name> -k system-images;android-22;google_apis;arm64-v8a"
Python 调用命令
import subprocess
def execute_bash(command: str, timeout: int = 10) -> tuple:
"""
执行shell脚本
:param command: 脚本
:param timeout: 超时时间,默认为10秒
:return: stdout, stderr 标准输出和错误
"""
sp = subprocess.Popen(command, shell=True)
return sp.communicate(timeout=timeout)
各种命令调用,用同步任务,性能将会很差,且模块耦合较高。使用 celery 异步任务将会提高性能,以调用远程命令为例:
from utils import Machine
from celery import Celery
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
@celery.task
def execute_remote_command(machine: Machine, command):
"""
执行远程命令
"""
pass
为了支持定时触发测试任务,推荐使用APScheduler
:
from apscheduler.schedulers.background import BlockingScheduler
scheduler = BlockingScheduler()
scheduler.add_executor('processpool')
def trigger_test() -> None:
"""
触发测试任务
"""
pass
scheduler.add_job(
ui_test, args=("iOS_SDK_Client_Test", "mainland", "qa"),
trigger='cron', minute="07", hour="5", timezone=ZoneInfo("Asia/Shanghai")
)
scheduler.add_listener(task_listener, EVENT_JOB_ERROR)
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
try:
scheduler.start()
except (KeyboardInterrupt, SystemExit):
pass
利用 Vue.js 快速实现 Web 后台,在此不过多介绍。
系统版本的兼容性测试:由于模拟器的系统版本众多,理论上可以做 Android 和 iOS 系统版本的兼容性测试。
云模拟器服务:商业案例有:https://appetize.io/demo
今年 9 月份找到公司法务,想把这个发成客户端自动化测试专利,法务说你这个方案与一些已有专利相似度较高。既然发不了,不如就分享给大家,共勉!